diff --git a/apps/admin/src/components/ProductBomPromptModal.tsx b/apps/admin/src/components/ProductBomPromptModal.tsx
new file mode 100644
index 0000000..11259a5
--- /dev/null
+++ b/apps/admin/src/components/ProductBomPromptModal.tsx
@@ -0,0 +1,332 @@
+import { useState, useMemo } from 'react';
+import {
+ X,
+ Copy,
+ Check,
+ Bot,
+ Sparkles,
+ FileCode,
+ Package,
+ Layers,
+ Code2
+} from 'lucide-react';
+
+interface ProductBomPromptModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ productId: number | string;
+ productName: string;
+ variants: any[];
+ rawMaterials: any[];
+}
+
+export default function ProductBomPromptModal({
+ isOpen,
+ onClose,
+ productId,
+ productName,
+ variants,
+ rawMaterials,
+}: ProductBomPromptModalProps) {
+ const [copied, setCopied] = useState(false);
+ const [activeTab, setActiveTab] = useState<'prompt' | 'context' | 'schema'>('prompt');
+
+ // Generate full prompt text for LLM
+ const generatedPrompt = useMemo(() => {
+ const variantLines = variants.map((v) => {
+ const opts = [v.option1, v.option2, v.option3].filter(Boolean).join(' / ') || 'Base Variant';
+ return ` - Variant ID: ${v.id} | SKU: ${v.sku || '-'} | ตัวเลือก: "${opts}" | ราคาขาย: ฿${Number(v.price || 0).toLocaleString()}`;
+ }).join('\n');
+
+ const rawMaterialLines = rawMaterials.map((rm) => {
+ return ` - [ID: ${rm.id}] "${rm.name}" | หน่วย: ${rm.unit} | ต้นทุน: ฿${Number(rm.pricePerUnit || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })}`;
+ }).join('\n');
+
+ return `คุณคือ Senior Manufacturing & Bill of Materials (BOM) Specialist ของระบบ E-Commerce (DBK Platform)
+
+[บริบทข้อมูลสินค้า]:
+- รหัสสินค้า (Product ID): ${productId}
+- ชื่อสินค้า: "${productName}"
+
+[รายการตัวเลือกสินค้า (Variants ที่ต้องกำหนดสูตร BOM)]:
+${variantLines}
+
+[คลังรายการวัตถุดิบในระบบ (Raw Materials ที่สามารถนำมาผูกสูตรได้)]:
+${rawMaterialLines}
+
+[ภารกิจของคุณ]:
+กรุณาจัดทำสูตรการผลิต (BOM Items) ให้กับแต่ละ Variant ของสินค้า รหัส #${productId} ตามคำสั่ง/สัดส่วนที่ระบุ
+และส่งคืนผลลัพธ์เป็น **Valid JSON Array** ล้วนๆ ตามข้อกำหนดดังต่อไปนี้เท่านั้น:
+
+[ข้อกำหนดผลลัพธ์ JSON Format ที่ต้องส่งกลับ]:
+\`\`\`json
+[
+ {
+ "variantId": <ตัวเลข ID ของ Variant>,
+ "sku": "<รหัส SKU>",
+ "bomItems": [
+ {
+ "rawMaterialId": <ตัวเลข ID ของวัตถุดิบจากคลังด้านบน>,
+ "name": "<ชื่อวัตถุดิบ>",
+ "quantity": <จำนวนปริมาณตัวเลข (ห้ามเป็น string)>
+ }
+ ]
+ }
+]
+\`\`\`
+*(คำสั่ง/สูตรเฉพาะ: ระบุสัดส่วนการผสม วัตถุดิบบรรจุภัณฑ์ และปริมาณที่ต้องการต่อ 1 ชิ้น)*`;
+ }, [productId, productName, variants, rawMaterials]);
+
+ // Context JSON
+ const contextJson = useMemo(() => {
+ return JSON.stringify(
+ {
+ product: {
+ id: Number(productId),
+ name: productName,
+ variantsCount: variants.length,
+ variants: variants.map((v) => ({
+ id: v.id,
+ sku: v.sku,
+ option1: v.option1,
+ option2: v.option2,
+ option3: v.option3,
+ price: v.price,
+ currentBomCount: v.bomItems?.length || 0,
+ })),
+ },
+ rawMaterialsCount: rawMaterials.length,
+ rawMaterials: rawMaterials.map((rm) => ({
+ id: rm.id,
+ name: rm.name,
+ unit: rm.unit,
+ pricePerUnit: rm.pricePerUnit,
+ })),
+ },
+ null,
+ 2
+ );
+ }, [productId, productName, variants, rawMaterials]);
+
+ // Target schema sample
+ const schemaSample = useMemo(() => {
+ const firstVariant = variants[0];
+ const sampleItem = rawMaterials[0];
+ return JSON.stringify(
+ [
+ {
+ variantId: firstVariant ? firstVariant.id : 1,
+ sku: firstVariant?.sku || 'SAMPLE-SKU',
+ bomItems: [
+ {
+ rawMaterialId: sampleItem ? sampleItem.id : 1,
+ name: sampleItem?.name || 'ตัวอย่างวัตถุดิบ',
+ quantity: 1,
+ },
+ ],
+ },
+ ],
+ null,
+ 2
+ );
+ }, [variants, rawMaterials]);
+
+ if (!isOpen) return null;
+
+ const handleCopy = async (textToCopy: string) => {
+ try {
+ await navigator.clipboard.writeText(textToCopy);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal Container */}
+
+ {/* Header */}
+
+
+
+
+
+
+
+
LLM Prompt Generator for BOM
+
+ Product #{productId}
+
+
+
+ สร้างชุดคำสั่ง Prompt พร้อมรหัสสินค้า, Variants และรายการวัตถุดิบทั้งหมดสำหรับส่งให้ LLM
+
+
+
+
+
+
+ {/* Quick Stats Bar */}
+
+
+
+
สินค้า: {productName}
+
+
+
+ Variants: {variants.length} ตัวเลือก
+
+
+
+ วัตถุดิบในคลัง: {rawMaterials.length} รายการ
+
+
+
+ {/* Navigation Tabs */}
+
+
+
+
+
+
+ {/* Content Body */}
+
+ {activeTab === 'prompt' && (
+
+
+
+ คัดลอกข้อความด้านล่างนี้ไปวางใน ChatGPT, Claude หรือ Gemini:
+
+
+
+
+
+
+ )}
+
+ {activeTab === 'context' && (
+
+
+
+ โครงสร้างข้อมูล Product Master, Variants และ Raw Materials ในรูปแบบ JSON:
+
+
+
+
+
+
+ )}
+
+ {activeTab === 'schema' && (
+
+
+
💡 โครงสร้างผลลัพธ์ที่ออกแบบไว้สำหรับรองรับการ Copy วาง (Paste to Import):
+
+ เมื่อ LLM ได้รับข้อมูลและส่งกลับมาในรูปแบบนี้ คุณจะสามารถนำข้อความ JSON มาวางเพื่อบันทึก BOM เข้าสู่ Variant ทั้งหมดได้ทันที
+
+
+
+
+
+ )}
+
+
+ {/* Footer */}
+
+
+ ระบบจัดเตรียมข้อมูลอัตโนมัติจากฐานข้อมูลจริงของสินค้า #{productId}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/src/pages/ProductBomPage.tsx b/apps/admin/src/pages/ProductBomPage.tsx
index 4807f2c..d84ce5e 100644
--- a/apps/admin/src/pages/ProductBomPage.tsx
+++ b/apps/admin/src/pages/ProductBomPage.tsx
@@ -11,9 +11,11 @@ import {
ArrowLeft,
Coins,
Layers,
- Calculator
+ Calculator,
+ Bot
} from 'lucide-react'
import { API_BASE, type Product } from '@homaroma/types'
+import ProductBomPromptModal from '../components/ProductBomPromptModal'
function fmtMoney(val: number | null | undefined): string {
if (val == null || isNaN(val)) return '฿0.00'
@@ -38,6 +40,7 @@ export default function ProductBomPage() {
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(false)
+ const [isPromptModalOpen, setIsPromptModalOpen] = useState(false)
useEffect(() => {
const fetchData = async () => {
@@ -143,9 +146,14 @@ export default function ProductBomPage() {
}
const productName =
- typeof product.name === 'string'
- ? product.name
- : (product.name as any)?.th || (product.name as any)?.en || 'Unknown Product'
+ (typeof product.name === 'string' && product.name.trim())
+ ? product.name.trim()
+ : (product.name as any)?.th ||
+ (product.name as any)?.en ||
+ (typeof product.name === 'object' && Object.values(product.name || {}).find((v) => typeof v === 'string' && (v as string).trim()) as string) ||
+ ((product as any).brand?.name
+ ? `${(product as any).brand?.name} ${(product as any).category?.name || ''}`.trim()
+ : `Product #${product.id}`)
// Summary Metrics Across All Variants
const totalVariantsCount = variants.length
@@ -177,6 +185,15 @@ export default function ProductBomPage() {
+
+
+ {/* LLM Prompt Generator Modal */}
+ setIsPromptModalOpen(false)}
+ productId={id || ''}
+ productName={productName}
+ variants={variants}
+ rawMaterials={rawMaterials}
+ />
)
}
diff --git a/apps/backend/src/routes/products.ts b/apps/backend/src/routes/products.ts
index d29b462..6bf3e9d 100644
--- a/apps/backend/src/routes/products.ts
+++ b/apps/backend/src/routes/products.ts
@@ -471,9 +471,9 @@ export const productsRoute = new Elysia({ prefix: "/products" })
if (!product || (!includeDeleted && product.isDeleted)) return status(404, { message: "Product not found" });
if (lang === 'all') {
- const nameObj: any = {};
- const descObj: any = {};
- const detailsObj: any = {};
+ const nameObj: any = product.name ? { th: product.name } : {};
+ const descObj: any = product.description ? { th: product.description } : {};
+ const detailsObj: any = product.details ? { th: product.details } : {};
product.translations.forEach((t: any) => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;