import * as fs from "fs"; import * as path from "path"; const PROD_API = "https://api.homaroma.shop/api"; interface MasterVariant { id: number; sku: string; barcode: string; price: number; stock: number; optionsStr: string; productName: string; categoryName: string; brandName: string; bomCost: number; displayName: string; } interface MarketplaceProduct { id: number; channel: string; productName: string; variation: string; marketplaceSku: string; masterVariantId: number | null; totalSoldQty: number; totalRevenue: number; } async function main() { console.log("Fetching Master Variants and Marketplace Mappings from Production..."); const [varRes, prodRes] = await Promise.all([ fetch(`${PROD_API}/tools/marketplace-products/master-variants`), fetch(`${PROD_API}/tools/marketplace-products?channel=all&status=mapped&limit=3000`), ]); const variants = ((await varRes.json()).variants || []) as MasterVariant[]; const mappedMarketplace = ((await prodRes.json()).items || []) as MarketplaceProduct[]; console.log(`Total Master Variants: ${variants.length}`); console.log(`Mapped Marketplace Products: ${mappedMarketplace.length}`); // Count marketplace mappings pointing to each masterVariantId const mappingCount = new Map(); for (const m of mappedMarketplace) { if (!m.masterVariantId) continue; const curr = mappingCount.get(m.masterVariantId) || { count: 0, soldQty: 0, revenue: 0 }; curr.count++; curr.soldQty += m.totalSoldQty || 0; curr.revenue += m.totalRevenue || 0; mappingCount.set(m.masterVariantId, curr); } // Group by category const categories: Record = {}; for (const v of variants) { const cat = v.categoryName || "ไม่ระบุหมวดหมู่"; if (!categories[cat]) { categories[cat] = { total: 0, withBom: 0, withoutBom: 0, missingList: [] }; } categories[cat].total++; if (v.bomCost > 0) { categories[cat].withBom++; } else { categories[cat].withoutBom++; categories[cat].missingList.push(v); } } const totalVariants = variants.length; const totalWithBom = variants.filter((v) => v.bomCost > 0).length; const totalWithoutBom = variants.filter((v) => v.bomCost === 0).length; let md = `# สรุปรายการสินค้า Product Master ที่ยังไม่มีสูตร BOM (Bill of Materials)\n\n`; md += `* **วันที่จัดทำรายงาน:** ${new Date().toISOString()}\n`; md += `* **ระบบอ้างอิง:** Production Database (\`${PROD_API}\`)\n`; md += `* **จำนวน Product Master Variant ทั้งหมด:** **${totalVariants}** รายการ\n`; md += `* **มีสูตร BOM แล้ว:** **${totalWithBom}** รายการ (${((totalWithBom / totalVariants) * 100).toFixed(1)}%)\n`; md += `* **ยังไม่มีสูตร BOM:** **${totalWithoutBom}** รายการ (${((totalWithoutBom / totalVariants) * 100).toFixed(1)}%)\n\n`; md += `> **ข้อสังเกต:** สินค้าในหมวด **"น้ำมันหอมระเหยโอโซน (Oil Ozone)"** ทั้งหมด 180 รายการ (45 กลิ่น x 4 ขนาด) มีสูตร BOM ครบถ้วน 100% แล้ว ส่วนสินค้าที่ยังไม่มีสูตร BOM มี 4 หมวดหมู่อยู่ด้านล่างนี้\n\n`; // Summary Table md += `## 1. ภาพรวมสถานะ BOM แยกตามหมวดหมู่ (Category Overview)\n\n`; md += `| หมวดหมู่ (Category) | จำนวนทั้งหมด | มี BOM แล้ว | ยังไม่มี BOM | สถานะความพร้อม |\n`; md += `|:---|:---:|:---:|:---:|:---:|\n`; for (const [cat, data] of Object.entries(categories)) { const pct = ((data.withBom / data.total) * 100).toFixed(1); const badge = data.withoutBom === 0 ? "✅ ครบ 100%" : data.withBom > 0 ? `⚠️ มีแล้ว ${pct}%` : "⏳ รอสร้าง BOM"; md += `| **${cat}** | ${data.total} | ${data.withBom} | **${data.withoutBom}** | ${badge} |\n`; } md += `| **รวมทั้งหมด** | **${totalVariants}** | **${totalWithBom}** | **${totalWithoutBom}** | **${((totalWithBom / totalVariants) * 100).toFixed(1)}%** |\n\n`; md += `---\n\n`; // Detailed lists per category md += `## 2. รายละเอียดสินค้าที่ยังไม่มีสูตร BOM แยกตามหมวดหมู่\n\n`; // Category priority order const order = [ "ก้านไม้หอมปรับอากาศ", "น้ำหอมปรับอากาศในรถยนต์", "สเปรย์ปรับอากาศ", "น้ำมันหอมระเหยอโรม่า", ]; for (const catName of order) { const data = categories[catName]; if (!data || data.missingList.length === 0) continue; // Group by size const sizeGroups: Record = {}; for (const item of data.missingList) { sizeGroups[item.optionsStr] = (sizeGroups[item.optionsStr] || 0) + 1; } const sizeStr = Object.entries(sizeGroups) .map(([sz, cnt]) => `${sz} (${cnt} กลิ่น)`) .join(", "); md += `### 📁 หมวดหมู่: ${catName} (${data.missingList.length} รายการ)\n\n`; md += `* **ขนาดบรรจุ:** ${sizeStr}\n`; md += `* **จำนวนที่ผูกกับ Marketplace แล้ว:** `; let linkedCount = 0; let linkedRevenue = 0; for (const item of data.missingList) { const stats = mappingCount.get(item.id); if (stats) { linkedCount += stats.count; linkedRevenue += stats.revenue; } } md += `**${linkedCount}** รายการขายบน Marketplace (ยอดขายรวมที่รอคำนวณต้นทุน: **฿${linkedRevenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}**)\n\n`; md += `| Variant ID | Master SKU | ชื่อสินค้า Master | ขนาด | ราคาขายปลีก | จำนวน Marketplace ที่ผูก |\n`; md += `|:---|:---|:---|:---:|:---:|:---:|\n`; for (const item of data.missingList) { const stats = mappingCount.get(item.id); const linkText = stats ? `🔗 ผูกแล้ว (${stats.count} SKU, ขายได้ ฿${stats.revenue.toFixed(0)})` : `-`; md += `| ${item.id} | \`${item.sku}\` | ${item.productName} | ${item.optionsStr} | ฿${item.price.toFixed(2)} | ${linkText} |\n`; } md += `\n`; } md += `---\n\n`; md += `## 3. คำแนะนำในการสร้าง BOM ลำดับถัดไป (Next Steps & Recommendations)\n\n`; md += `1. **ก้านไม้หอมปรับอากาศ (Reed Diffuser - 135 รายการ):** แนะนำทำเป็นลำดับแรก เนื่องจากมียอดขายและการผูกบน Marketplace (Shopee/Lazada) มากที่สุด วัตถุดิบหลักประกอบด้วย: น้ำหอม Base Reed Diffuser, ก้านไม้หวาย (Diffuser Sticks), ขวดแก้ว, ฝาจุก\n`; md += `2. **น้ำหอมปรับอากาศในรถยนต์ (Car Diffuser - 45 รายการ):** มีขนาดเดียวคือ 25ml ประกอบด้วย: หัวเชื้อน้ำหอม, ขวดแขวนฝาไม้, เชือกแขวน, จุกพลาสติกกันรั่ว\n`; md += `3. **สเปรย์ปรับอากาศ (Room Spray - 45 รายการ):** มีขนาดเดียวคือ 85ml ประกอบด้วย: เบสสเปรย์ (Alcohol/Water Solution), หัวเชื้อน้ำหอม, ขวดพลาสติก 85ml, หัวฉีดสเปรย์\n`; md += `4. **น้ำมันหอมระเหยอโรม่า (Aroma Oil - 179 รายการ):** สูตรจะใกล้เคียงกับ Oil Ozone แต่ใช้ตัวทำละลายต่างกัน (Aroma Oil ใช้สำหรับเตาอโรม่า/Burner ไม่ต้องใช้น้ำมันละลายน้ำ)\n`; const outPath = path.resolve(process.cwd(), "PRODUCT_MASTER_WITHOUT_BOM.md"); fs.writeFileSync(outPath, md, "utf8"); console.log(`Saved report to: ${outPath}`); } main().catch(console.error);