From 101143f26ae0dc8cbbbb16674fbd4d04aea8a1bc Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 24 Sep 2026 20:01:24 +0700 Subject: [PATCH] feat(products): implement JSON import with auto-provisioning, UI modal, and documentation schema --- PRODUCT_JSON_IMPORT_SCHEMA.md | 134 ++++++++++ apps/admin/src/pages/ProductsPage.tsx | 290 ++++++++++++++++++++- apps/backend/src/routes/products.ts | 346 +++++++++++++------------- push_all.sh | 110 ++++++++ sample-products-import.json | 75 ++++++ 5 files changed, 768 insertions(+), 187 deletions(-) create mode 100644 PRODUCT_JSON_IMPORT_SCHEMA.md create mode 100644 push_all.sh create mode 100644 sample-products-import.json diff --git a/PRODUCT_JSON_IMPORT_SCHEMA.md b/PRODUCT_JSON_IMPORT_SCHEMA.md new file mode 100644 index 0000000..f6b5e5a --- /dev/null +++ b/PRODUCT_JSON_IMPORT_SCHEMA.md @@ -0,0 +1,134 @@ +# คู่มือโครงสร้าง JSON สำหรับนำเข้าสินค้า (Product JSON Import Specification) + +ระบบนำเข้าสินค้าของ HomAroma รองรับการ Import ผ่านไฟล์ `.json` หรือวาง Payload JSON Array ได้โดยตรงผ่าน: +1. **หน้าจอ Admin:** เมนู `/products` -> กดปุ่ม **"Import JSON"** +2. **API Endpoint:** `POST /api/products/bulk` หรือ `POST /api/products` + +--- + +## 1. รูปแบบโครงสร้าง JSON (JSON Schema) + +Payload หลักจะต้องเป็น **JSON Array** ของสินค้า โดยรองรับสินค้า 2 รูปแบบหลัก: + +### แบบที่ 1: สินค้าเดี่ยว (Simple Product - ไม่มีตัวเลือกย่อย) +เหมาะสำหรับสินค้าที่มีขนาดเดียว ราคาเดียว ไม่ต้องแตก Variant: + +```json +{ + "name": "ก้านไม้หอมปรับอากาศ กลิ่นมะลิไทย (Reed Diffuser Jasmine)", + "categoryName": "Reed Diffuser", + "brandName": "HomAroma", + "price": 490, + "stock": 100, + "sku": "HA-RD-JAS-100", + "barcode": "8859123456801", + "description": "ก้านไม้กระจายความหอมกลิ่นดอกมะลิธรรมชาติ หอมละมุน 4-6 สัปดาห์", + "details": "ปริมาตร: 100ml\nรวมก้านไม้ไฟเบอร์ 6 ก้าน", + "images": [ + "https://example.com/uploads/diffuser-1.jpg", + "https://example.com/uploads/diffuser-2.jpg" + ] +} +``` + +> **ระบบ Auto-Handling:** เมื่อใส่ `price`, `stock`, `sku`, `barcode` ไว้ที่ระดับบนสุด ระบบจะสร้าง Default Base Variant ให้โดยอัตโนมัติ + +--- + +### แบบที่ 2: สินค้าที่มีหลายตัวเลือกย่อย (Variable Product) +เหมาะสำหรับสินค้าที่มี ขนาด, กลิ่น, หรือสี ให้เลือก: + +```json +{ + "name": "น้ำมันหอมระเหย ลาเวนเดอร์ 100% Pure Essential Oil", + "categoryName": "Aroma Oil", + "brandName": "HomAroma", + "description": "น้ำมันหอมระเหยบริสุทธิ์แท้ 100% สกัดจากดอกลาเวนเดอร์ประเทศฝรั่งเศส", + "details": "วิธีใช้: หยด 3-5 หยดลงในเตาอโรมา\nขนาด: 10ml, 30ml", + "options": [ + { + "name": "ขนาด", + "values": ["10ml", "30ml"] + } + ], + "variants": [ + { + "option1": "10ml", + "price": 390, + "stock": 50, + "sku": "HA-LAV-10ML", + "barcode": "8859123456701" + }, + { + "option1": "30ml", + "price": 890, + "stock": 30, + "sku": "HA-LAV-30ML", + "barcode": "8859123456702" + } + ], + "images": [ + "https://example.com/uploads/lav-1.jpg" + ] +} +``` + +--- + +### แบบที่ 3: รองรับหลายภาษา (Multilingual) +สามารถใส่ข้อความหลายภาษาใน `translations` หรือใส่เป็น Object ที่ฟิลด์ `name`, `description`, `details` ได้โดยตรง: + +```json +{ + "name": "น้ำมันหอมระเหย ยูคาลิปตัส", + "categoryName": "Aroma Oil", + "brandName": "HomAroma", + "price": 320, + "stock": 60, + "sku": "HA-EUC-10ML", + "translations": { + "th": { + "name": "น้ำมันหอมระเหย ยูคาลิปตัส 100%", + "description": "ช่วยให้หายใจโล่ง สดชื่น ปลอดโปร่ง", + "details": "ขนาด: 10ml" + }, + "en": { + "name": "Eucalyptus Pure Essential Oil 100%", + "description": "Refreshes breathing and clarifies the mind.", + "details": "Size: 10ml" + } + } +} +``` + +--- + +## 2. พจนานุกรมฟิลด์ (Data Dictionary) + +| ฟิลด์ | ชนิดข้อมูล | จำเป็น? | คำอธิบายและข้อกำหนด | +| :--- | :--- | :---: | :--- | +| `name` | `string` \| `object` | **ใช่** | ชื่อสินค้า (เช่น `"Aroma Oil"` หรือ `{"th": "...", "en": "..."}`) | +| `categoryName` | `string` | ทางเลือก | ชื่อหมวดหมู่ (หากยังไม่มีในระบบ ระบบจะ **Auto-Create** ให้ทันที) | +| `categoryId` | `number` | ทางเลือก | รหัสหมวดหมู่เดิมในระบบ (ใช้แทน `categoryName` ได้) | +| `brandName` | `string` | ทางเลือก | ชื่อแบรนด์ (หากยังไม่มีในระบบ ระบบจะ **Auto-Create** ให้ทันที) | +| `brandId` | `number` | ทางเลือก | รหัสแบรนด์เดิมในระบบ (ใช้แทน `brandName` ได้) | +| `description` | `string` \| `object` | ไม่ | คำอธิบายสินค้าโดยย่อ | +| `details` | `string` \| `object` | ไม่ | รายละเอียดสินค้า วิธีการใช้งาน คุณสมบัติ | +| `price` | `number` | ไม่* | ราคาจำหน่าย (กรณีสินค้าเดี่ยวไม่มี Variants) | +| `stock` | `number` | ไม่* | จำนวนสต็อกคงเหลือ (กรณีสินค้าเดี่ยวไม่มี Variants) | +| `sku` | `string` | ไม่ | รหัสสินค้า SKU | +| `barcode` | `string` | ไม่ | รหัสบาร์โค้ดสินค้า (สำหรับยิงขายที่ POS) | +| `images` | `array` | ไม่ | รายการรูปภาพ (รองรับทั้ง Array URL `["https://..."]` หรือ `[{"url": "...", "order": 0}]`) | +| `options` | `array` | ไม่* | รายการตัวเลือก สูงสุด 3 ตัวเลือก เช่น `[{"name": "ขนาด", "values": ["10ml", "30ml"]}]` | +| `variants` | `array` | ไม่* | รายการสินค้าย่อย โดย `option1`, `option2`, `option3` จะแมปตามลำดับของ `options` | +| `translations` | `object` | ไม่ | แผนที่ภาษา เช่น `{"th": {...}, "en": {...}, "ja": {...}}` | + +--- + +## 3. กลไกอัตโนมัติของ Backend (Auto-Provisioning Engine) + +เมื่อส่ง JSON เข้าสู่ `POST /api/products/bulk`: +1. **Auto-Category & Auto-Brand:** หากส่ง `categoryName` หรือ `brandName` ที่ยังไม่มีในระบบ ระบบจะสร้าง Category/Brand ใหม่ให้โดยอัตโนมัติ ไม่เกิด Error +2. **Auto-Default Base Variant:** หากไม่ส่ง `variants` มา แต่ระบุ `price` หรือ `stock` หรือ `sku` ที่ระดับบนสุด ระบบจะแปลงเป็น Single Base Variant ให้อัตโนมัติ ทำให้สินค้าพร้อมขายและตัดสต็อกใน POS ได้ทันที +3. **Auto-Normalize Images:** แปลงทั้ง String URLs และ Image Objects ให้อยู่ในฟอร์แมตฐานข้อมูลพร้อมกำหนด Order ให้อัตโนมัติ +4. **Auto-Seed CMS Detail Sections:** ระบบจะสร้าง Section มาตรฐานสำหรับหน้าเว็บ Next.js SSR (Product Main Card, Description, Specifications, Reviews, Related Products) ให้ทันที ทำให้หน้าบ้านแสดงผลได้สมบูรณ์แบบโดยไม่ต้องเข้าไปตั้งค่าทีละชิ้น diff --git a/apps/admin/src/pages/ProductsPage.tsx b/apps/admin/src/pages/ProductsPage.tsx index b70e50b..b23e133 100644 --- a/apps/admin/src/pages/ProductsPage.tsx +++ b/apps/admin/src/pages/ProductsPage.tsx @@ -12,12 +12,16 @@ import { X, UploadCloud, Loader2, - LayoutTemplate + LayoutTemplate, + FileJson, + Download, + CheckCircle2 } from 'lucide-react' import { useNavigate, useSearchParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { API_BASE, getCategoryImage, formatPrice, type Category, type Product } from '@homaroma/types' + interface PaginationMeta { total: number; page: number; @@ -46,6 +50,146 @@ export default function ProductsPage() { const limitParam = Number(searchParams.get('limit')) const limit = validLimits.includes(limitParam) ? limitParam : 10 const [meta, setMeta] = useState(null) + + // JSON Import State + const [isImportModalOpen, setIsImportModalOpen] = useState(false) + const [importJsonText, setImportJsonText] = useState('') + const [importLoading, setImportLoading] = useState(false) + const [importError, setImportError] = useState(null) + const [importSuccessMessage, setImportSuccessMessage] = useState(null) + + const sampleProductsJson = [ + { + "name": "น้ำมันหอมระเหย ลาเวนเดอร์ 100% Pure Essential Oil", + "categoryName": "Aroma Oil", + "brandName": "HomAroma", + "description": "น้ำมันหอมระเหยบริสุทธิ์แท้ 100% สกัดจากดอกลาเวนเดอร์ประเทศฝรั่งเศส ช่วยผ่อนคลายและหลับสบาย", + "details": "วิธีใช้: หยด 3-5 หยดลงในเครื่องพ่นอโรมา\nขนาด: 10ml, 30ml", + "options": [ + { + "name": "ขนาด", + "values": ["10ml", "30ml"] + } + ], + "variants": [ + { + "option1": "10ml", + "price": 390, + "stock": 50, + "sku": "HA-LAV-10ML", + "barcode": "8859123456701" + }, + { + "option1": "30ml", + "price": 890, + "stock": 30, + "sku": "HA-LAV-30ML", + "barcode": "8859123456702" + } + ], + "images": [ + "https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?w=800" + ], + "translations": { + "th": { + "name": "น้ำมันหอมระเหย ลาเวนเดอร์ 100% Pure Essential Oil", + "description": "น้ำมันหอมระเหยบริสุทธิ์แท้ 100% สกัดจากดอกลาเวนเดอร์ประเทศฝรั่งเศส ช่วยผ่อนคลายและหลับสบาย" + }, + "en": { + "name": "Lavender Pure Essential Oil 100%", + "description": "100% pure natural French lavender essential oil for relaxation and sleep." + } + } + }, + { + "name": "ก้านไม้หอมปรับอากาศ กลิ่นมะลิ (Reed Diffuser Jasmine)", + "categoryName": "Reed Diffuser", + "brandName": "HomAroma", + "description": "ก้านไม้กระจายความหอมกลิ่นมะลิไทยแท้ หอมละมุนยาวนาน 4-6 สัปดาห์", + "details": "ปริมาตร: 100ml\nรวมก้านไม้ไฟเบอร์ 6 ก้าน", + "price": 490, + "stock": 100, + "sku": "HA-RD-JAS-100", + "barcode": "8859123456801", + "images": [ + "https://images.unsplash.com/photo-1617897903246-719242758050?w=800" + ] + } + ] + + const handleDownloadSampleJson = () => { + const jsonStr = JSON.stringify(sampleProductsJson, null, 2) + const blob = new Blob([jsonStr], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = 'products-template.json' + link.click() + URL.revokeObjectURL(url) + } + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = (event) => { + const content = event.target?.result as string + setImportJsonText(content) + setImportError(null) + } + reader.readAsText(file) + } + + const handleImportSubmit = async () => { + setImportError(null) + setImportSuccessMessage(null) + if (!importJsonText.trim()) { + setImportError('กรุณากรอกหรือเลือกไฟล์ JSON') + return + } + + let parsedData: any + try { + parsedData = JSON.parse(importJsonText) + } catch (err: any) { + setImportError(`รูปแบบ JSON ไม่ถูกต้อง (Syntax Error): ${err.message}`) + return + } + + const payloadArray = Array.isArray(parsedData) ? parsedData : [parsedData] + if (payloadArray.length === 0) { + setImportError('ไม่พบรายการสินค้าใน JSON') + return + } + + setImportLoading(true) + try { + const res = await fetch(`${API_BASE}/products/bulk`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payloadArray) + }) + + const data = await res.json() + if (res.ok && data.success) { + setImportSuccessMessage(`นำเข้าสินค้าสำเร็จทั้งหมด ${data.count || payloadArray.length} รายการ!`) + fetchData() + setTimeout(() => { + setIsImportModalOpen(false) + setImportJsonText('') + setImportSuccessMessage(null) + }, 1500) + } else { + setImportError(data.message || 'เกิดข้อผิดพลาดในการนำเข้าสินค้า') + } + } catch (err: any) { + console.error(err) + setImportError(err.message || 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์') + } finally { + setImportLoading(false) + } + } + const fixUrl = (url: string) => { if (!url) return '' @@ -317,15 +461,26 @@ export default function ProductsPage() { {meta ? `Displaying ${products.length} of ${meta.total} products` : t('common.loading', 'Loading products...')}

- +
+ + +
+ {/* Filters Bar */}
@@ -751,6 +906,125 @@ export default function ProductsPage() {
)} + {/* JSON Import Modal */} + + {isImportModalOpen && ( +
+
+ {/* Header */} +
+
+
+ +
+
+

นำเข้าสินค้าผ่าน JSON (JSON Import)

+

รองรับทั้งสินค้าเดี่ยว (Simple) และสินค้าหลายตัวเลือก (Variants)

+
+
+ +
+ + {/* Actions Bar */} +
+ + + +
+ + {/* JSON Textarea */} +
+
+ วางโค้ด JSON ที่นี่ (JSON Array): + {importJsonText && ( + + )} +
+