feat(bom): add Get Prompt modal for LLM and fix product name resolution (#11)
### Summary of Changes - Added 'Get Prompt' button and ProductBomPromptModal dialog on /products/:id/bom. - Formats product ID, variants, and raw material inventory into a structured prompt for LLMs. - Fixed backend bug in GET /products/:id?lang=all that wiped product.name when no translations existed. - Added multi-level fallback in frontend to ensure product name never shows 'Unknown Product'. --------- Co-authored-by: Your Name <you@example.com> Reviewed-on: #11 Co-authored-by: antigravity-bot <bot@kittisak.com>
This commit was merged in pull request #11.
This commit is contained in:
1 parent
a647c9682d
commit
d8f6d8139f
3 files changed
+366
-7
No files matched your search
@@ -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 (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-night-950/80 backdrop-blur-sm transition-opacity"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal Container */}
|
||||||
|
<div className="relative w-full max-w-4xl bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden max-h-[92vh] animate-zoom-in">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-5 border-b border-white/5 bg-night-950/40">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-2xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400">
|
||||||
|
<Bot className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-bold text-white">LLM Prompt Generator for BOM</h2>
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-full bg-purple-500/10 text-purple-300 border border-purple-500/20 font-mono font-bold">
|
||||||
|
Product #{productId}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400">
|
||||||
|
สร้างชุดคำสั่ง Prompt พร้อมรหัสสินค้า, Variants และรายการวัตถุดิบทั้งหมดสำหรับส่งให้ LLM
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Stats Bar */}
|
||||||
|
<div className="grid grid-cols-3 gap-4 px-6 py-3 bg-night-950/30 border-b border-white/5 text-xs">
|
||||||
|
<div className="flex items-center gap-2 text-slate-400">
|
||||||
|
<Package className="w-4 h-4 text-cyan-400" />
|
||||||
|
<span>สินค้า: <strong className="text-white">{productName}</strong></span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-slate-400">
|
||||||
|
<Layers className="w-4 h-4 text-purple-400" />
|
||||||
|
<span>Variants: <strong className="text-white">{variants.length} ตัวเลือก</strong></span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-slate-400">
|
||||||
|
<Code2 className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>วัตถุดิบในคลัง: <strong className="text-white">{rawMaterials.length} รายการ</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Tabs */}
|
||||||
|
<div className="flex border-b border-white/5 px-6 pt-2 bg-night-950/20">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('prompt')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
|
||||||
|
activeTab === 'prompt'
|
||||||
|
? 'border-purple-500 text-purple-400'
|
||||||
|
: 'border-transparent text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
<span>ชุดคำสั่ง Prompt สำหรับ LLM</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('context')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
|
||||||
|
activeTab === 'context'
|
||||||
|
? 'border-purple-500 text-purple-400'
|
||||||
|
: 'border-transparent text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileCode className="w-4 h-4" />
|
||||||
|
<span>ข้อมูลดิบ (Context JSON)</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('schema')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
|
||||||
|
activeTab === 'schema'
|
||||||
|
? 'border-purple-500 text-purple-400'
|
||||||
|
: 'border-transparent text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Code2 className="w-4 h-4" />
|
||||||
|
<span>Target Output Format</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Body */}
|
||||||
|
<div className="p-6 overflow-y-auto space-y-4 flex-1">
|
||||||
|
{activeTab === 'prompt' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
คัดลอกข้อความด้านล่างนี้ไปวางใน <strong>ChatGPT, Claude หรือ Gemini</strong>:
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopy(generatedPrompt)}
|
||||||
|
className="px-4 py-2 rounded-xl bg-purple-500/10 hover:bg-purple-500/20 text-purple-300 border border-purple-500/30 text-xs font-bold flex items-center gap-2 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
|
||||||
|
<span>{copied ? 'คัดลอก Prompt สำเร็จ!' : 'คัดลอก Prompt ทั้งหมด'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
readOnly
|
||||||
|
value={generatedPrompt}
|
||||||
|
rows={16}
|
||||||
|
className="w-full font-mono text-xs bg-night-950/80 border border-white/10 rounded-2xl p-5 text-slate-200 focus:outline-none resize-y leading-relaxed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'context' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
โครงสร้างข้อมูล Product Master, Variants และ Raw Materials ในรูปแบบ JSON:
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopy(contextJson)}
|
||||||
|
className="px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 border border-white/10 text-xs font-bold flex items-center gap-2 transition-all"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
|
||||||
|
<span>{copied ? 'คัดลอกแล้ว' : 'คัดลอก Context JSON'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
readOnly
|
||||||
|
value={contextJson}
|
||||||
|
rows={16}
|
||||||
|
className="w-full font-mono text-xs bg-night-950/80 border border-white/10 rounded-2xl p-5 text-slate-300 focus:outline-none resize-y leading-relaxed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'schema' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 rounded-2xl bg-cyan-950/20 border border-cyan-500/20 text-xs text-slate-300 space-y-1">
|
||||||
|
<p className="font-bold text-cyan-300">💡 โครงสร้างผลลัพธ์ที่ออกแบบไว้สำหรับรองรับการ Copy วาง (Paste to Import):</p>
|
||||||
|
<p className="text-slate-400">
|
||||||
|
เมื่อ LLM ได้รับข้อมูลและส่งกลับมาในรูปแบบนี้ คุณจะสามารถนำข้อความ JSON มาวางเพื่อบันทึก BOM เข้าสู่ Variant ทั้งหมดได้ทันที
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
readOnly
|
||||||
|
value={schemaSample}
|
||||||
|
rows={12}
|
||||||
|
className="w-full font-mono text-xs bg-night-950/80 border border-white/10 rounded-2xl p-5 text-emerald-400 focus:outline-none resize-y leading-relaxed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/5 bg-night-950/40 flex items-center justify-between">
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
ระบบจัดเตรียมข้อมูลอัตโนมัติจากฐานข้อมูลจริงของสินค้า #{productId}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-2.5 rounded-xl text-sm font-medium text-slate-400 hover:text-white hover:bg-white/5 transition-all"
|
||||||
|
>
|
||||||
|
ปิดหน้าต่าง
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopy(generatedPrompt)}
|
||||||
|
className="btn-primary text-sm px-6 py-2.5 flex items-center gap-2 shadow-lg shadow-purple-500/20 bg-purple-600 hover:bg-purple-500 text-white"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||||
|
<span>{copied ? 'คัดลอกสำเร็จแล้ว' : 'คัดลอก Prompt ไปใช้งาน'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,9 +11,11 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Coins,
|
Coins,
|
||||||
Layers,
|
Layers,
|
||||||
Calculator
|
Calculator,
|
||||||
|
Bot
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { API_BASE, type Product } from '@homaroma/types'
|
import { API_BASE, type Product } from '@homaroma/types'
|
||||||
|
import ProductBomPromptModal from '../components/ProductBomPromptModal'
|
||||||
|
|
||||||
function fmtMoney(val: number | null | undefined): string {
|
function fmtMoney(val: number | null | undefined): string {
|
||||||
if (val == null || isNaN(val)) return '฿0.00'
|
if (val == null || isNaN(val)) return '฿0.00'
|
||||||
@@ -38,6 +40,7 @@ export default function ProductBomPage() {
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
|
const [isPromptModalOpen, setIsPromptModalOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -143,9 +146,14 @@ export default function ProductBomPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productName =
|
const productName =
|
||||||
typeof product.name === 'string'
|
(typeof product.name === 'string' && product.name.trim())
|
||||||
? product.name
|
? product.name.trim()
|
||||||
: (product.name as any)?.th || (product.name as any)?.en || 'Unknown Product'
|
: (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
|
// Summary Metrics Across All Variants
|
||||||
const totalVariantsCount = variants.length
|
const totalVariantsCount = variants.length
|
||||||
@@ -177,6 +185,15 @@ export default function ProductBomPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPromptModalOpen(true)}
|
||||||
|
className="px-4 py-2.5 rounded-xl bg-purple-500/10 hover:bg-purple-500/20 text-purple-300 border border-purple-500/30 text-sm font-bold flex items-center gap-2 transition-all shadow-sm"
|
||||||
|
title="สร้าง Prompt สำหรับส่งให้ LLM คิดและตอบกลับเป็นสูตร BOM"
|
||||||
|
>
|
||||||
|
<Bot className="w-4 h-4 text-purple-400" />
|
||||||
|
<span>Get Prompt</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/products')}
|
onClick={() => navigate('/products')}
|
||||||
className="px-5 py-2.5 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 text-sm font-bold text-slate-300 transition-all"
|
className="px-5 py-2.5 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 text-sm font-bold text-slate-300 transition-all"
|
||||||
@@ -534,6 +551,16 @@ export default function ProductBomPage() {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* LLM Prompt Generator Modal */}
|
||||||
|
<ProductBomPromptModal
|
||||||
|
isOpen={isPromptModalOpen}
|
||||||
|
onClose={() => setIsPromptModalOpen(false)}
|
||||||
|
productId={id || ''}
|
||||||
|
productName={productName}
|
||||||
|
variants={variants}
|
||||||
|
rawMaterials={rawMaterials}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -471,9 +471,9 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
|||||||
if (!product || (!includeDeleted && product.isDeleted)) return status(404, { message: "Product not found" });
|
if (!product || (!includeDeleted && product.isDeleted)) return status(404, { message: "Product not found" });
|
||||||
|
|
||||||
if (lang === 'all') {
|
if (lang === 'all') {
|
||||||
const nameObj: any = {};
|
const nameObj: any = product.name ? { th: product.name } : {};
|
||||||
const descObj: any = {};
|
const descObj: any = product.description ? { th: product.description } : {};
|
||||||
const detailsObj: any = {};
|
const detailsObj: any = product.details ? { th: product.details } : {};
|
||||||
product.translations.forEach((t: any) => {
|
product.translations.forEach((t: any) => {
|
||||||
nameObj[t.langCode] = t.name;
|
nameObj[t.langCode] = t.name;
|
||||||
descObj[t.langCode] = t.description;
|
descObj[t.langCode] = t.description;
|
||||||
|
|||||||
Reference in new issue
Block a user