E-commerce Inventory System

Multi-platform inventory synchronization and management system

ระบบจัดการสต็อกและซิงค์ข้อมูลหลายแพลตฟอร์มสำหรับธุรกิจอีคอมเมิร์ซไทย

Project Overview

Client Background

ร้านค้าออนไลน์แฟชั่นชั้นนำ
├─ 3 แพลตฟอร์ม: Shopee, Lazada, Facebook
├─ 500+ SKU สินค้า
├─ 100+ ออเดอร์ต่อวัน
└─ ปัญหา: สต็อกไม่ตรงกันทุกแพลตฟอร์ม

Challenges Solved

❌ ปัญหาเดิม:
├─ อัปเดตสต็อกแยกกันทุกแพลตฟอร์ม
├─ ขายสินค้าหมดแต่ยังขายอยู่
├─ สต็อกเกินทำให้ต้นทุนสูง
├─ ไม่รู้ว่าสินค้าอะไรขายดี
└─ การสั่งซื้อซ้ำซ้อน

✅ วิธีแก้:
├─ Centralized inventory management
├─ Real-time multi-platform sync
├─ Automated reorder system
├─ Sales analytics dashboard
└─ Supplier management integration

Technology Stack

Architecture Diagram

Multiple Platforms → Webhooks → n8n (Railway) → Custom API (Vercel) → Supabase Database
      ↓                ↓              ↓                    ↓              ↓
   Orders           Workflow      Business Logic         Master Data    Analytics
      ↓                ↓              ↓                    ↓              ↓
   Customers → Sync Back ← Dashboard ← Supplier Portal ← Reports

Components Used

Frontend: Next.js + TailwindCSS (Vercel)
Backend: Node.js/TypeScript (Vercel Functions)
Database: Supabase (PostgreSQL)
Automation: n8n (Railway)
APIs: Shopee, Lazada, Facebook Graph
Real-time: Supabase Real-time + Webhooks

Key Features

1. Multi-Platform API Integration

// Platform API abstraction layer
class PlatformAPI {
  constructor(platform, credentials) {
    this.platform = platform;
    this.credentials = credentials;
    this.baseURL = this.getBaseURL();
    this.rateLimiter = new RateLimiter(this.getRateLimits());
  }

  getBaseURL() {
    const urls = {
      shopee: 'https://partner.shopeemobile.com',
      lazada: 'https://api.lazada.com.ph',
      facebook: 'https://graph.facebook.com'
    };
    return urls[this.platform];
  }

  getRateLimits() {
    const limits = {
      shopee: { requests: 1000, window: 60 },
      lazada: { requests: 100, window: 60 },
      facebook: { requests: 200, window: 60 }
    };
    return limits[this.platform];
  }

  async updateProduct(productData) {
    await this.rateLimiter.wait();
    
    switch (this.platform) {
      case 'shopee':
        return await this.updateShopeeProduct(productData);
      case 'lazada':
        return await this.updateLazadaProduct(productData);
      case 'facebook':
        return await this.updateFacebookProduct(productData);
      default:
        throw new Error(`Unsupported platform: ${this.platform}`);
    }
  }

  async updateShopeeProduct(productData) {
    const timestamp = Math.floor(Date.now() / 1000);
    const signature = this.generateShopeeSignature(timestamp);
    
    const payload = {
      item_id: productData.platform_product_id,
      price: productData.price * 100000, // Shopee uses cents
      stock: productData.stock,
      description: productData.description_th,
      item_name: productData.name_th
    };

    const response = await fetch(`${this.baseURL}/api/v2/product/update_item`, {
      method: 'POST',
      headers: {
        'Authorization': this.credentials.access_token,
        'Content-Type': 'application/json',
        'X-Shopee-Signature': signature
      },
      body: JSON.stringify({
        partner_id: this.credentials.partner_id,
        timestamp: timestamp,
        sign: signature,
        ...payload
      })
    });

    return await this.handleResponse(response);
  }

  async updateLazadaProduct(productData) {
    const payload = {
      Request: {
        Product: {
          SellerSku: productData.sku,
          Price: productData.price,
          Quantity: productData.stock,
          Description: productData.description_th
        }
      }
    };

    const response = await fetch(`${this.baseURL}/rest/product/update`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.credentials.access_token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });

    return await this.handleResponse(response);
  }

  async updateFacebookProduct(productData) {
    const response = await fetch(
      `${this.baseURL}/${this.credentials.catalog_id}/products`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.credentials.access_token}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          availability: productData.stock > 0 ? 'in stock' : 'out of stock',
          price: productData.price,
          description: productData.description_th,
          name: productData.name_th,
          retailer_id: productData.sku
        })
      }
    );

    return await this.handleResponse(response);
  }

  generateShopeeSignature(timestamp) {
    const crypto = require('crypto');
    const baseString = `${this.credentials.partner_id}${this.endpoint}${timestamp}`;
    return crypto
      .createHmac('sha256', this.credentials.partner_key)
      .update(baseString)
      .digest('hex');
  }

  async handleResponse(response) {
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(`${this.platform} API Error: ${data.error || data.message}`);
    }
    
    return data;
  }
}

// Usage example
const shopeeAPI = new PlatformAPI('shopee', {
  partner_id: process.env.SHOPEE_PARTNER_ID,
  partner_key: process.env.SHOPEE_PARTNER_KEY,
  access_token: process.env.SHOPEE_ACCESS_TOKEN
});

await shopeeAPI.updateProduct({
  platform_product_id: '123456789',
  sku: 'FASHION-001',
  name_th: 'เสื้อยืดคอกลมลายการ์ตูน',
  description_th: 'เสื้อยืดคอกลมผ้าคอตตอน 100% ลายการ์ตูนน่ารัก',
  price: 299,
  stock: 50
});

2. Real-time Inventory Synchronization

// n8n Function Node - Inventory Sync Manager
async function syncInventoryAcrossPlatforms() {
  const syncData = $json;
  
  try {
    // Get master product from Supabase
    const { data: masterProduct, error } = await supabase
      .from('products')
      .select('*')
      .eq('id', syncData.productId)
      .single();
    
    if (error || !masterProduct) {
      throw new Error('Product not found');
    }

    // Initialize platform APIs
    const platforms = {};
    
    if (masterProduct.shopee_product_id) {
      platforms.shopee = new PlatformAPI('shopee', getShopeeCredentials());
    }
    
    if (masterProduct.lazada_product_id) {
      platforms.lazada = new PlatformAPI('lazada', getLazadaCredentials());
    }
    
    if (masterProduct.facebook_product_id) {
      platforms.facebook = new PlatformAPI('facebook', getFacebookCredentials());
    }

    // Sync to all platforms
    const syncResults = {};
    const syncPromises = Object.entries(platforms).map(async ([platform, api]) => {
      try {
        const result = await api.updateProduct({
          platform_product_id: masterProduct[`${platform}_product_id`],
          sku: masterProduct.sku,
          name_th: masterProduct.name_th,
          description_th: masterProduct.description_th,
          price: masterProduct.price,
          stock: masterProduct.stock
        });
        
        syncResults[platform] = {
          success: true,
          data: result,
          timestamp: new Date().toISOString()
        };
        
        console.log(`${platform} sync successful`);
        
      } catch (error) {
        syncResults[platform] = {
          success: false,
          error: error.message,
          timestamp: new Date().toISOString()
        };
        
        console.error(`${platform} sync failed:`, error.message);
        
        // Log for manual review
        await logSyncError(masterProduct.id, platform, error);
      }
    });

    await Promise.allSettled(syncPromises);

    // Update sync status
    await supabase
      .from('products')
      .update({
        last_sync_at: new Date().toISOString(),
        sync_status: Object.values(syncResults).every(r => r.success) ? 'success' : 'partial',
        sync_errors: Object.entries(syncResults)
          .filter(([_, result]) => !result.success)
          .map(([platform, result]) => ({ platform, error: result.error }))
      })
      .eq('id', masterProduct.id);

    // Send notification if there are errors
    const failedPlatforms = Object.entries(syncResults)
      .filter(([_, result]) => !result.success)
      .map(([platform, _]) => platform);
    
    if (failedPlatforms.length > 0) {
      await sendSyncNotification({
        productId: masterProduct.id,
        productName: masterProduct.name_th,
        failedPlatforms: failedPlatforms,
        requiresManualAction: true
      });
    }

    return [{
      json: {
        productId: masterProduct.id,
        productName: masterProduct.name_th,
        syncResults: syncResults,
        summary: {
          total: Object.keys(platforms).length,
          successful: Object.values(syncResults).filter(r => r.success).length,
          failed: failedPlatforms.length
        }
      }
    }];

  } catch (error) {
    console.error('Inventory sync error:', error);
    
    // Log critical error
    await logCriticalError(syncData.productId, 'inventory_sync', error);
    
    return [{
      json: {
        error: error.message,
        productId: syncData.productId,
        requiresManualIntervention: true
      }
    }];
  }
}

// Handle order from any platform
async function handleIncomingOrder(orderData) {
  const platform = orderData.platform;
  const items = orderData.items;

  try {
    // Process each item
    for (const item of items) {
      // Find master product
      const { data: product } = await supabase
        .from('products')
        .select('*')
        .eq(`${platform}_product_id`, item.platform_product_id)
        .single();

      if (!product) {
        console.warn(`Product not found for ${platform} ID: ${item.platform_product_id}`);
        continue;
      }

      // Update inventory
      const newStock = Math.max(0, product.stock - item.quantity);
      
      await supabase
        .from('products')
        .update({ 
          stock: newStock,
          updated_at: new Date().toISOString()
        })
        .eq('id', product.id);

      // Trigger sync to other platforms
      await triggerInventorySync(product.id);

      // Check if reorder needed
      if (newStock <= product.reorder_level) {
        await triggerReorderProcess(product.id, newStock);
      }

      // Log inventory change
      await supabase
        .from('inventory_logs')
        .insert({
          product_id: product.id,
          change_type: 'sale',
          quantity_change: -item.quantity,
          previous_stock: product.stock,
          new_stock: newStock,
          platform: platform,
          order_id: orderData.orderId,
          created_at: new Date().toISOString()
        });
    }

    // Save order to database
    await saveOrder(orderData);

    // Update analytics
    await updateSalesAnalytics(orderData);

    return { success: true };

  } catch (error) {
    console.error('Order processing error:', error);
    throw error;
  }
}

3. Automated Reorder System

// Smart reorder system
class ReorderManager {
  constructor() {
    this.suppliers = new Map();
    this.loadSuppliers();
  }

  async loadSuppliers() {
    const { data: suppliers } = await supabase
      .from('suppliers')
      .select('*');
    
    suppliers.forEach(supplier => {
      this.suppliers.set(supplier.id, supplier);
    });
  }

  async checkReorderNeeds() {
    // Get products that need reordering
    const { data: products } = await supabase
      .from('products')
      .select('*')
      .lte('stock', 'reorder_level')
      .eq('auto_reorder', true);

    const reorderActions = [];

    for (const product of products) {
      const reorderAction = await this.createReorderAction(product);
      if (reorderAction) {
        reorderActions.push(reorderAction);
      }
    }

    return reorderActions;
  }

  async createReorderAction(product) {
    const supplier = this.suppliers.get(product.supplier_id);
    if (!supplier) {
      console.warn(`No supplier found for product ${product.id}`);
      return null;
    }

    // Calculate optimal reorder quantity
    const reorderQuantity = this.calculateReorderQuantity(product);
    
    // Check budget constraints
    const totalCost = reorderQuantity * product.cost_price;
    const budgetAvailable = await this.checkBudget(totalCost);
    
    if (!budgetAvailable) {
      console.log(`Budget insufficient for product ${product.id}`);
      return null;
    }

    // Create purchase order
    const purchaseOrder = {
      product_id: product.id,
      supplier_id: product.supplier_id,
      quantity: reorderQuantity,
      unit_price: product.cost_price,
      total_cost: totalCost,
      urgency: this.calculateUrgency(product),
      status: 'pending_approval',
      created_at: new Date().toISOString()
    };

    // Save purchase order
    const { data: savedPO, error } = await supabase
      .from('purchase_orders')
      .insert(purchaseOrder)
      .select()
      .single();

    if (error) throw error;

    // Notify supplier
    await this.notifySupplier(supplier, savedPO, product);

    // Notify manager if needed
    if (totalCost > 10000) { // High value orders need approval
      await this.notifyManager(savedPO, product);
    }

    return {
      purchaseOrder: savedPO,
      product: product,
      supplier: supplier,
      action: 'reorder_created'
    };
  }

  calculateReorderQuantity(product) {
    // Calculate based on:
    // - Sales velocity (last 30 days)
    // - Seasonal trends
    // - Lead time
    // - Storage capacity
    
    const salesVelocity = this.getSalesVelocity(product.id, 30);
    const leadTimeDays = this.getSupplierLeadTime(product.supplier_id);
    const safetyStock = Math.ceil(salesVelocity * leadTimeDays * 1.5);
    
    // Economic Order Quantity (EOQ) formula
    const holdingCost = product.cost_price * 0.25; // 25% annual holding cost
    const orderCost = 500; // Fixed order cost
    const annualDemand = salesVelocity * 365;
    
    const eoq = Math.sqrt((2 * orderCost * annualDemand) / holdingCost);
    
    // Round up to nearest pack size
    const packSize = product.pack_size || 1;
    return Math.ceil(eoq / packSize) * packSize;
  }

  calculateUrgency(product) {
    const stockRatio = product.stock / product.reorder_level;
    
    if (stockRatio <= 0.2) return 'critical';
    if (stockRatio <= 0.5) return 'high';
    if (stockRatio <= 0.8) return 'medium';
    return 'low';
  }

  async notifySupplier(supplier, purchaseOrder, product) {
    const emailContent = `
      เรียน คุณ${supplier.contact_name},
      
      ขอสั่งซื้อสินค้าใหม่:
      
      รหัสสินค้า: ${product.sku}
      ชื่อสินค้า: ${product.name_th}
      จำนวน: ${purchaseOrder.quantity} ชิ้น
      ราคาต่อหน่วย: ฿${product.cost_price}
      รวม: ฿${purchaseOrder.total_cost}
      ความเร่งด่วน: ${purchaseOrder.urgency}
      
      กรุณายืนยันภายใน 24 ชั่วโมง
      และแจ้งวันที่จัดส่งโดยเร็วที่สุด
      
      ขอบคุณครับ
      แผนกจัดซื้อ ShantiLink
    `;

    await sendEmail({
      to: supplier.contact_email,
      subject: `ใบสั่งซื้อ #${purchaseOrder.id} - ${product.name_th}`,
      body: emailContent
    });
  }

  async notifyManager(purchaseOrder, product) {
    const message = `
      🛒 ใบสั่งซื้อรออนุมัติ (มูลค่าสูง)
      
      รหัส: #${purchaseOrder.id}
      สินค้า: ${product.name_th}
      จำนวน: ${purchaseOrder.quantity}
      มูลค่า: ฿${purchaseOrder.total_cost}
      ความเร่งด่วน: ${purchaseOrder.urgency}
      
      ต้องการการอนุมัติภายใน 4 ชั่วโมง
    `;

    await sendLINEMessage(process.env.MANAGER_LINE_ID, message);
  }
}

4. Analytics Dashboard

// E-commerce Analytics Dashboard
export default function EcommerceDashboard() {
  const [analytics, setAnalytics] = useState({
    overview: {},
    topProducts: [],
    lowStock: [],
    salesByPlatform: {},
    inventoryTurnover: [],
    profitMargins: {}
  });
  const [dateRange, setDateRange] = useState({
    start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
    end: new Date()
  });
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadAnalytics();
    
    // Real-time updates
    const subscription = supabase
      .channel('ecommerce-analytics')
      .on('postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'orders' },
        () => loadAnalytics()
      )
      .on('postgres_changes',
        { event: 'UPDATE', schema: 'public', table: 'products' },
        () => loadAnalytics()
      )
      .subscribe();

    return () => subscription.unsubscribe();
  }, [dateRange]);

  async function loadAnalytics() {
    setLoading(true);
    
    try {
      const [
        overviewResult,
        topProductsResult,
        lowStockResult,
        platformResult,
        turnoverResult,
        marginResult
      ] = await Promise.all([
        // Overview metrics
        supabase
          .rpc('get_ecommerce_overview', {
            start_date: dateRange.start.toISOString(),
            end_date: dateRange.end.toISOString()
          }),
        
        // Top selling products
        supabase
          .rpc('get_top_products', {
            start_date: dateRange.start.toISOString(),
            limit: 10
          }),
        
        // Low stock alerts
        supabase
          .from('products')
          .select(`
            id, sku, name_th, stock, reorder_level, cost_price, selling_price
          `)
          .lt('stock', 'reorder_level * 1.5')
          .order('stock', { ascending: true })
          .limit(20),
        
        // Sales by platform
        supabase
          .rpc('get_sales_by_platform', {
            start_date: dateRange.start.toISOString(),
            end_date: dateRange.end.toISOString()
          }),
        
        // Inventory turnover
        supabase
          .rpc('get_inventory_turnover', {
            days: 30
          }),
        
        // Profit margins
        supabase
          .rpc('get_profit_margins', {
            start_date: dateRange.start.toISOString(),
            end_date: dateRange.end.toISOString()
          })
      ]);

      setAnalytics({
        overview: overviewResult.data[0] || {},
        topProducts: topProductsResult.data || [],
        lowStock: lowStockResult.data || [],
        salesByPlatform: platformResult.data || {},
        inventoryTurnover: turnoverResult.data || [],
        profitMargins: marginResult.data || {}
      });

    } catch (error) {
      console.error('Load analytics error:', error);
    } finally {
      setLoading(false);
    }
  }

  if (loading) {
    return <LoadingSpinner />;
  }

  return (
    <div className="min-h-screen bg-gray-50 p-6">
      <div className="max-w-7xl mx-auto">
        {/* Header */}
        <div className="flex justify-between items-center mb-8">
          <h1 className="text-3xl font-bold">E-commerce Analytics</h1>
          <DateRangePicker 
            value={dateRange}
            onChange={setDateRange}
          />
        </div>

        {/* Overview Cards */}
        <div className="grid grid-cols-4 gap-6 mb-8">
          <MetricCard
            title="Total Revenue"
            value={`฿${analytics.overview.total_revenue?.toLocaleString() || 0}`}
            change={analytics.overview.revenue_change || 0}
            icon="💰"
          />
          <MetricCard
            title="Total Orders"
            value={analytics.overview.total_orders || 0}
            change={analytics.overview.orders_change || 0}
            icon="📦"
          />
          <MetricCard
            title="Average Order Value"
            value={`฿${analytics.overview.avg_order_value?.toLocaleString() || 0}`}
            change={analytics.overview.aov_change || 0}
            icon="📊"
          />
          <MetricCard
            title="Profit Margin"
            value={`${analytics.overview.profit_margin || 0}%`}
            change={analytics.overview.margin_change || 0}
            icon="📈"
          />
        </div>

        {/* Charts Row */}
        <div className="grid grid-cols-2 gap-6 mb-8">
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-xl font-bold mb-4">Sales by Platform</h2>
            <PlatformSalesChart data={analytics.salesByPlatform} />
          </div>
          
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-xl font-bold mb-4">Top Products</h2>
            <TopProductsChart data={analytics.topProducts} />
          </div>
        </div>

        {/* Tables Row */}
        <div className="grid grid-cols-2 gap-6">
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-xl font-bold mb-4">Low Stock Alerts</h2>
            <LowStockTable 
              items={analytics.lowStock}
              onReorder={handleReorder}
            />
          </div>
          
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-xl font-bold mb-4">Inventory Turnover</h2>
            <InventoryTurnoverTable data={analytics.inventoryTurnover} />
          </div>
        </div>
      </div>
    </div>
  );
}

function LowStockTable({ items, onReorder }) {
  return (
    <div className="overflow-x-auto">
      <table className="min-w-full">
        <thead className="bg-gray-50">
          <tr>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              SKU
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Product
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Stock
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Reorder Level
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Action
            </th>
          </tr>
        </thead>
        <tbody className="divide-y divide-gray-200">
          {items.map((item, index) => (
            <tr key={item.id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
              <td className="px-4 py-3 text-sm font-medium text-gray-900">
                {item.sku}
              </td>
              <td className="px-4 py-3 text-sm text-gray-900">
                {item.name_th}
              </td>
              <td className="px-4 py-3 text-sm">
                <span className={`font-medium ${
                  item.stock <= item.reorder_level ? 'text-red-600' : 'text-yellow-600'
                }`}>
                  {item.stock}
                </span>
              </td>
              <td className="px-4 py-3 text-sm text-gray-500">
                {item.reorder_level}
              </td>
              <td className="px-4 py-3 text-sm">
                <button
                  onClick={() => onReorder(item.id)}
                  className="text-blue-600 hover:text-blue-900 font-medium"
                >
                  Reorder
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

5. Supplier Portal

// Supplier Management Portal
export default function SupplierPortal() {
  const [suppliers, setSuppliers] = useState([]);
  const [purchaseOrders, setPurchaseOrders] = useState([]);
  const [activeTab, setActiveTab] = useState('orders');

  useEffect(() => {
    loadSuppliers();
    loadPurchaseOrders();
  }, []);

  async function loadSuppliers() {
    const { data } = await supabase
      .from('suppliers')
      .select(`
        *,
        products (id, name_th, sku),
        purchase_orders (id, total_cost, status, created_at)
      `);
    
    setSuppliers(data || []);
  }

  async function loadPurchaseOrders() {
    const { data } = await supabase
      .from('purchase_orders')
      .select(`
        *,
        suppliers (name, contact_email),
        products (name_th, sku)
      `)
      .order('created_at', { ascending: false })
      .limit(50);
    
    setPurchaseOrders(data || []);
  }

  return (
    <div className="min-h-screen bg-gray-50 p-6">
      <div className="max-w-7xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">Supplier Management</h1>

        {/* Tabs */}
        <div className="bg-white rounded-lg shadow mb-6">
          <div className="border-b">
            <nav className="flex">
              <button
                onClick={() => setActiveTab('orders')}
                className={`px-6 py-3 font-medium ${
                  activeTab === 'orders'
                    ? 'text-blue-600 border-b-2 border-blue-600'
                    : 'text-gray-600 hover:text-gray-800'
                }`}
              >
                Purchase Orders
              </button>
              <button
                onClick={() => setActiveTab('suppliers')}
                className={`px-6 py-3 font-medium ${
                  activeTab === 'suppliers'
                    ? 'text-blue-600 border-b-2 border-blue-600'
                    : 'text-gray-600 hover:text-gray-800'
                }`}
              >
                Suppliers
              </button>
              <button
                onClick={() => setActiveTab('analytics')}
                className={`px-6 py-3 font-medium ${
                  activeTab === 'analytics'
                    ? 'text-blue-600 border-b-2 border-blue-600'
                    : 'text-gray-600 hover:text-gray-800'
                }`}
              >
                Analytics
              </button>
            </nav>
          </div>

          <div className="p-6">
            {activeTab === 'orders' && (
              <PurchaseOrdersTable orders={purchaseOrders} />
            )}
            {activeTab === 'suppliers' && (
              <SuppliersTable suppliers={suppliers} />
            )}
            {activeTab === 'analytics' && (
              <SupplierAnalytics />
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

function PurchaseOrdersTable({ orders }) {
  const statusColors = {
    pending_approval: 'bg-yellow-100 text-yellow-800',
    approved: 'bg-blue-100 text-blue-800',
    confirmed: 'bg-green-100 text-green-800',
    shipped: 'bg-purple-100 text-purple-800',
    received: 'bg-gray-100 text-gray-800',
    cancelled: 'bg-red-100 text-red-800'
  };

  return (
    <div className="overflow-x-auto">
      <table className="min-w-full">
        <thead className="bg-gray-50">
          <tr>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              PO Number
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Supplier
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Product
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Quantity
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Total Cost
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Status
            </th>
            <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
              Actions
            </th>
          </tr>
        </thead>
        <tbody className="divide-y divide-gray-200">
          {orders.map((order) => (
            <tr key={order.id}>
              <td className="px-4 py-3 text-sm font-medium text-gray-900">
                #{order.id}
              </td>
              <td className="px-4 py-3 text-sm text-gray-900">
                {order.suppliers.name}
              </td>
              <td className="px-4 py-3 text-sm text-gray-900">
                {order.products.name_th}
              </td>
              <td className="px-4 py-3 text-sm text-gray-900">
                {order.quantity}
              </td>
              <td className="px-4 py-3 text-sm font-medium text-gray-900">
                ฿{order.total_cost.toLocaleString()}
              </td>
              <td className="px-4 py-3 text-sm">
                <span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColors[order.status]}`}>
                  {order.status.replace('_', ' ')}
                </span>
              </td>
              <td className="px-4 py-3 text-sm">
                <div className="flex gap-2">
                  <button className="text-blue-600 hover:text-blue-900">
                    View
                  </button>
                  {order.status === 'pending_approval' && (
                    <button className="text-green-600 hover:text-green-900">
                      Approve
                    </button>
                  )}
                  {order.status === 'confirmed' && (
                    <button className="text-purple-600 hover:text-purple-900">
                      Mark Received
                    </button>
                  )}
                </div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Implementation Timeline

Phase 1: Foundation (Week 1-2)

✅ เสร็จสิ้น:
├─ Setup Supabase database schema
├─ Create platform API integrations
├─ Implement basic inventory sync
├─ Order processing workflow
└─ Initial dashboard

Phase 2: Core Features (Week 3-4)

✅ เสร็จสิ้น:
├─ Real-time synchronization
├─ Automated reorder system
├─ Supplier management
├─ Analytics dashboard
└─ Error handling & recovery

Phase 3: Advanced Features (Week 5-6)

✅ เสร็จสิ้น:
├─ Advanced analytics
├─ Multi-warehouse support
├─ Purchase order automation
├─ Supplier portal
└─ Mobile app for staff

Results & Metrics

Before vs After

📊 ผลลัพธ์หลัง 1 เดือน:

Inventory Management:
├─ Stockouts: 15/เดือน ↓ 1/เดือน
├─ Overstock: ฿200,000 ↓ ฿50,000
├─ Sync errors: 50/วัน ↓ 2/วัน
└─ Manual updates: 8 ชั่วโมง/วัน ↓ 30 นาที/วัน

Sales Performance:
├─ Order fulfillment: 85% ↑ 99%
├─ Customer satisfaction: 70% ↑ 95%
├─ Revenue: +35%
└─ Profit margin: 25% ↑ 32%

Operational Impact

📈 ผลกระทบการดำเนินงาน:
├─ ประหยัดเวลาอัปเดต: 95%
├─ ลดข้อผิดพลาด: 96%
├─ เพิ่มประสิทธิภาพทีม: 300%
├─ ลดต้นทุนการจัดการ: 60%
└─ ตัดสินใจได้เร็วขึ้น: 400%

Cost Analysis

Investment Breakdown

💰 การลงทุน (ครั้งเดียว):
├─ System development: ฿85,000
├─ API integrations: ฿25,000
├─ Supplier portal: ฿20,000
├─ Analytics system: ฿20,000
└─ Total: ฿150,000

💸 ค่าใช้จ่ายรายเดือน:
├─ n8n hosting: ฿800
├─ Database: ฿1,200
├─ Frontend: ฿400
├─ API calls: ฿600
└─ Total: ฿3,000/เดือน

ROI Calculation

📈 Return on Investment:
├─ Reduced stockout costs: ฿150,000/เดือน
├─ Inventory optimization: ฿75,000/เดือน
├─ Labor savings: ฿40,000/เดือน
├─ Increased sales: ฿100,000/เดือน
├─ Net monthly gain: ฿362,000
├─ Payback period: 0.4 เดือน
└─ Annual ROI: 2,896%

Technical Challenges & Solutions

Challenge 1: API Rate Limiting

❌ ปัญหา:
├─ Shopee: 1000 requests/minute
├─ Lazada: 100 requests/minute
├─ Facebook: 200 requests/minute
└─ การ sync พร้อมกันทำให้โดน block

✅ วิธีแก้:
├─ Intelligent rate limiting
├─ Queue-based processing
├─ Priority-based updates
├─ Batch operations
└─ Exponential backoff retry

Challenge 2: Data Consistency

❌ ปัญหา:
├─ การขายพร้อมกันหลายแพลตฟอร์ม
├─ Network latency ทำให้ข้อมูลไม่ sync
├─ การยกเลิกออเดอร์ฉุกเฉิน
└─ การอัปเดตคละกัน

✅ วิธีแก้:
├─ Database transactions
├─ Optimistic locking
├─ Event-driven architecture
├─ Conflict resolution
└─ Audit logging

Lessons Learned

Technical Lessons

✅ Do's:
├─ ใช้ queue system สำหรับ background jobs
├─ Implement comprehensive error handling
├─ Monitor API usage อย่างต่อเนื่อง
├─ ทดสอบกับข้อมูลจริงตั้งแต่แรก
└─ มี fallback mechanism พร้อม

❌ Don'ts:
├─ สมมติว่า API เสถียรเสมอ
├─ ไม่คิดถึง edge cases
├─ ลืม data validation
├─ ใช้ synchronous operations
└─ ไม่มี monitoring system

Business Lessons

✅ Success Factors:
├─ เข้าใจ pain points ของธุรกิจ
├─ สร้างระบบที่ scalable
├─ วัดผลและปรับปรุงอย่างต่อเนื่อง
├─ สร้างความสัมพันธ์กับ supplier
└─ ฝึกอบรมทีมอย่างดี

Future Enhancements

Phase 4: Advanced Features (Planned)

🚀 คุณสมบัติที่จะเพิ่ม:
├─ AI-powered demand forecasting
├─ Multi-currency support
├─ Cross-border e-commerce
├─ Advanced supplier scoring
├─ Automated negotiations
└─ Blockchain for traceability

ต้องการระบบจัดการสต็อกแบบนี้หรือไม่? ติดต่อเราได้ที่ ShantiLink.com 💬

ดูตัวอย่างอื่นๆ: