This commit is contained in:
Your Name committed 2026-10-18 13:55:18 +07:00
1 parent 4fcdd8a816
commit f49776f9cb
14 files changed
+4012 -21

No files matched your search

+169
View File
@@ -0,0 +1,169 @@
import * as fs from "fs";
import * as path from "path";
const PROD_API = "https://api.homaroma.shop/api";
interface RawMaterial {
id: number;
name: string;
unit: string;
pricePerUnit: number;
}
interface Variant {
id: number;
sku: string;
options: string;
price: number;
hasBom: boolean;
bomItemCount: number;
totalBomCost: number;
}
interface Product {
id: number;
name: string;
brand: string | null;
category: string | null;
totalVariants: number;
configuredVariants: number;
bomStatus: string;
variants: Variant[];
}
async function main() {
console.log("1. Fetching all products from production...");
const statusRes = await fetch(`${PROD_API}/products/bom-status?limit=250`);
if (!statusRes.ok) throw new Error(`Failed to fetch products: ${statusRes.statusText}`);
const statusData = (await statusRes.json()) as { products: Product[] };
// Filter only Oil Ozone products
const allProducts = statusData.products || [];
const oilOzoneProducts = allProducts.filter(
(p) =>
p.name.includes("Oil Ozone") ||
(p.category && p.category.includes("โอโซน")) ||
p.variants.some((v) => v.sku && v.sku.includes("-1-"))
);
console.log(`Found ${oilOzoneProducts.length} Oil Ozone products.`);
// 2. Fetch all raw materials from production
console.log("2. Fetching available raw materials from production...");
const rmRes = await fetch(`${PROD_API}/raw-materials`);
if (!rmRes.ok) throw new Error(`Failed to fetch raw materials: ${rmRes.statusText}`);
const rawMaterials = (await rmRes.json()) as RawMaterial[];
console.log(`Found ${rawMaterials.length} raw materials on production.`);
// Identify common raw materials
const dpg = rawMaterials.find((r) => r.name.trim().toLowerCase() === "dpg");
const b30 = rawMaterials.find((r) => r.name.includes("Oilozone 30ml") || r.name.includes("ขวดพร้อมฝา Oilozone 30ml"));
const b60 = rawMaterials.find((r) => r.name.includes("Oilozone 60ml") || r.name.includes("ขวดพร้อมฝา Oilozone 60ml"));
const b85 = rawMaterials.find((r) => r.name.includes("Oilozone 85ml") || r.name.includes("ขวดพร้อมฝา Oilozone 85ml"));
const b120 = rawMaterials.find((r) => r.name.includes("Oilozone 120ml") || r.name.includes("ขวดพร้อมฝา Oilozone 120ml"));
// Build match plan for each product
const productPlan = oilOzoneProducts.map((p, idx) => {
// Extract scent name e.g. "ซากุระ Oil Ozone" -> "ซากุระ"
const scentName = p.name.replace(/Oil Ozone/gi, "").trim();
// Look for matching fragrance oil in raw materials
const matchedFragrance = rawMaterials.find((rm) => {
const rmName = rm.name.trim();
return (
rmName === `หัวน้ำหอม กลิ่น${scentName}` ||
rmName === `หัวน้ำหอม ${scentName}` ||
rmName.includes(`กลิ่น${scentName}`) ||
(rmName.includes(scentName) && rmName.includes("หัวน้ำหอม"))
);
});
return {
index: idx + 1,
productId: p.id,
productName: p.name,
scentName,
category: p.category,
bomStatus: p.bomStatus,
configuredVariants: p.configuredVariants,
totalVariants: p.totalVariants,
variants: p.variants.map((v) => ({
id: v.id,
sku: v.sku,
option: v.options,
price: v.price,
hasBom: v.hasBom,
totalBomCost: v.totalBomCost,
marginPercent: v.price > 0 && v.totalBomCost > 0
? Number((((v.price - v.totalBomCost) / v.price) * 100).toFixed(1))
: null,
})),
matchedFragrance: matchedFragrance
? {
id: matchedFragrance.id,
name: matchedFragrance.name,
pricePerUnit: matchedFragrance.pricePerUnit,
unit: matchedFragrance.unit,
}
: null,
packagingReady: !!(dpg && b30 && b60 && b85 && b120),
};
});
// 3. Save JSON to local root folder
const jsonPath = path.resolve(process.cwd(), "oil_ozone_products.json");
fs.writeFileSync(jsonPath, JSON.stringify(productPlan, null, 2), "utf-8");
console.log(`\n✓ Saved JSON data to: ${jsonPath}`);
// 4. Generate Markdown Ledger
let md = `# รายการสินค้ากลุ่ม Oil Ozone สำหรับกำหนดสูตร BOM (Production)\n\n`;
md += `> **เป้าหมาย:** กำหนดสูตร BOM อัตราส่วน หัวน้ำหอม 8% + DPG 92% + ขวดและฝา 4 บาท (ทุกขนาด: 30ml, 60ml, 85ml, 120ml)\n`;
md += `> **อัปเดตล่าสุด:** ${new Date().toLocaleString("th-TH")}\n`;
md += `> **จำนวนสินค้าทั้งหมด:** ${productPlan.length} รายการ\n\n`;
md += `## 📦 สรุปสถานะภาพรวม\n`;
const configured = productPlan.filter((p) => p.bomStatus === "CONFIGURED").length;
const missing = productPlan.filter((p) => p.bomStatus === "MISSING").length;
const partial = productPlan.filter((p) => p.bomStatus === "PARTIAL").length;
const fragranceFound = productPlan.filter((p) => p.matchedFragrance !== null).length;
md += `- **ตั้งค่าเสร็จแล้ว (CONFIGURED):** ${configured} / ${productPlan.length} รายการ (100%)\n`;
md += `- **ยังไม่ได้ตั้งค่า (MISSING):** ${missing} รายการ\n`;
md += `- **ตั้งค่าบางส่วน (PARTIAL):** ${partial} รายการ\n`;
md += `- **พบวัตถุดิบหัวน้ำหอมตรงกลิ่น:** ${fragranceFound} / ${productPlan.length} รายการ\n\n`;
md += `## 📋 ตารางรายละเอียดต้นทุน BOM และอัตรากำไร (Margin)\n\n`;
md += `| # | ID | ชื่อสินค้า | หัวน้ำหอม (ราคา/ml) | ต้นทุน 30ml (กำไร%) | ต้นทุน 60ml (กำไร%) | ต้นทุน 85ml (กำไร%) | ต้นทุน 120ml (กำไร%) | สถานะ |\n`;
md += `| :-: | :-: | :--- | :--- | :-: | :-: | :-: | :-: | :-: |\n`;
for (const p of productPlan) {
const statusBadge =
p.bomStatus === "CONFIGURED"
? "✅ พร้อมขาย"
: p.bomStatus === "PARTIAL"
? "⚠️ บางส่วน"
: "❌ ขาด BOM";
const fragText = p.matchedFragrance
? `[ID: ${p.matchedFragrance.id}] ${p.matchedFragrance.name} (฿${p.matchedFragrance.pricePerUnit})`
: `<span style="color:red">⚠️ ไม่พบหัวน้ำหอม</span>`;
const v30 = p.variants.find((v) => (v.option || "").includes("30") || (v.sku || "").includes("30ML"));
const v60 = p.variants.find((v) => (v.option || "").includes("60") || (v.sku || "").includes("60ML"));
const v85 = p.variants.find((v) => (v.option || "").includes("85") || (v.sku || "").includes("85ML"));
const v120 = p.variants.find((v) => (v.option || "").includes("120") || (v.sku || "").includes("120ML"));
const formatCost = (v?: (typeof p.variants)[0]) => {
if (!v || !v.hasBom) return "-";
return `฿${v.totalBomCost.toFixed(2)} (${v.marginPercent}%)`;
};
md += `| ${p.index} | **#${p.productId}** | [${p.productName}](https://admin.homaroma.shop/products/${p.productId}/bom) | ${fragText} | ${formatCost(v30)} | ${formatCost(v60)} | ${formatCost(v85)} | ${formatCost(v120)} | ${statusBadge} |\n`;
}
const mdPath = path.resolve(process.cwd(), "OIL_OZONE_BOM_LEDGER.md");
fs.writeFileSync(mdPath, md, "utf-8");
console.log(`✓ Saved Markdown ledger to: ${mdPath}`);
}
main().catch(console.error);