diff --git a/apps/admin/src/pages/ProductAttributeEditPage.tsx b/apps/admin/src/pages/ProductAttributeEditPage.tsx index 54f12dc..34f1297 100644 --- a/apps/admin/src/pages/ProductAttributeEditPage.tsx +++ b/apps/admin/src/pages/ProductAttributeEditPage.tsx @@ -139,25 +139,51 @@ function emptyDefinition(): EditableDefinition { function parseDefinition(definition: AttributeDefinition): EditableDefinition { const translations = Object.fromEntries( - LANGUAGES.map((lang) => [ - lang, - { - label: - typeof definition.label === 'string' - ? lang === 'th' - ? definition.label - : '' - : definition.label?.[lang] || '', - description: - typeof definition.description === 'string' - ? lang === 'th' - ? definition.description - : '' - : definition.description?.[lang] || '', - }, - ]) + LANGUAGES.map((lang) => { + let label = '' + let description = '' + + if (typeof definition.label === 'string') { + label = lang === 'th' ? definition.label : '' + } else if (definition.label && typeof definition.label === 'object') { + label = (definition.label as any)[lang] || '' + } + + if (typeof definition.description === 'string') { + description = lang === 'th' ? definition.description : '' + } else if (definition.description && typeof definition.description === 'object') { + description = (definition.description as any)[lang] || '' + } + + // Check translations array from API if label is still empty + if (!label && Array.isArray((definition as any).translations)) { + const tr = (definition as any).translations.find((t: any) => t.langCode === lang) + if (tr) { + label = tr.label || '' + description = tr.description || description + } + } + + return [lang, { label, description }] + }) ) + const rawOptions = Array.isArray(definition.options) ? definition.options : [] + const options = rawOptions.map((option: any) => { + if (typeof option === 'string') { + return { value: option, th: option, en: option } + } + if (typeof option === 'object' && option !== null) { + const norm: Record = { ...option } + // Normalize th and en labels from label_th / label_en / label + if (!norm.th) norm.th = norm.label_th || norm.label || norm.value || '' + if (!norm.en) norm.en = norm.label_en || norm.label || norm.value || '' + if (!norm.value) norm.value = norm.key || norm.id || norm.en || norm.th || '' + return norm + } + return {} + }) + return { id: definition.id, code: definition.code, @@ -168,9 +194,7 @@ function parseDefinition(definition: AttributeDefinition): EditableDefinition { sortOrder: definition.sortOrder, categoryId: definition.categoryId ?? null, translations, - options: Array.isArray(definition.options) - ? definition.options.map((option) => (typeof option === 'string' ? { th: option } : option)) - : [], + options, } } @@ -438,11 +462,21 @@ export default function ProductAttributeEditPage() { options: form.type === 'SELECT' ? activeOptions - .map((opt) => - Object.fromEntries( + .map((opt) => { + const cleaned = Object.fromEntries( Object.entries(opt).filter(([, val]) => typeof val === 'string' && val.trim()) ) - ) + if (!cleaned.value && (cleaned.en || cleaned.th)) { + cleaned.value = (cleaned.en || cleaned.th) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, '') + } + if (!cleaned.label_th && cleaned.th) cleaned.label_th = cleaned.th + if (!cleaned.label_en && cleaned.en) cleaned.label_en = cleaned.en + if (!cleaned.label) cleaned.label = cleaned.th || cleaned.en || cleaned.value || '' + return cleaned + }) .filter((opt) => Object.keys(opt).length > 0) : undefined, } @@ -1391,37 +1425,71 @@ Return ONLY a valid JSON Array of objects in this exact structure (no markdown f ) : (
- {form.options.map((opt, idx) => ( -
- #{idx + 1} - { - const nextOptions = [...form.options] - nextOptions[idx] = { ...nextOptions[idx], [activeLang]: e.target.value } - setForm({ ...form, options: nextOptions }) - }} - className="flex-1 px-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none" - placeholder={`Option value in ${activeLang.toUpperCase()} (e.g. CBR 650R, Mono Shock)`} - /> + {form.options.map((opt, idx) => { + const optValue = opt.value || opt.key || '' + const currentLabel = opt[activeLang] || '' + const fallbackLabel = opt.th || opt.en || opt.label || '' - -
- ))} + #{idx + 1} + + {/* System Code / Key */} +
+ + { + const nextOptions = [...form.options] + nextOptions[idx] = { ...nextOptions[idx], value: e.target.value } + setForm({ ...form, options: nextOptions }) + }} + placeholder="code_value" + className="w-full px-3 py-2 bg-black/40 border border-white/10 rounded-lg text-xs font-mono text-cyan-glow placeholder-slate-600 focus:border-cyan-glow/40 outline-none" + title="System code stored in database / API" + /> +
+ + {/* Localized Label */} +
+ + { + const nextOptions = [...form.options] + nextOptions[idx] = { ...nextOptions[idx], [activeLang]: e.target.value } + setForm({ ...form, options: nextOptions }) + }} + className="w-full px-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none" + placeholder={ + fallbackLabel && !currentLabel + ? `${fallbackLabel} (${activeLang === 'th' ? 'TH' : 'Fallback'})` + : `Label in ${activeLang.toUpperCase()}` + } + /> +
+ + +
+ ) + })} )} diff --git a/apps/admin/src/pages/ProductEditPage.tsx b/apps/admin/src/pages/ProductEditPage.tsx index 45937e9..41da517 100644 --- a/apps/admin/src/pages/ProductEditPage.tsx +++ b/apps/admin/src/pages/ProductEditPage.tsx @@ -843,8 +843,19 @@ export default function ProductEditPage() { > {(definition.options || []).map((option, optionIdx) => { - const optionValue = typeof option === 'string' ? option : option.th || option.en || Object.values(option)[0] - const optionLabel = typeof option === 'string' ? option : option[activeLang] || option.th || option.en || Object.values(option)[0] + const optionValue = + typeof option === 'string' + ? option + : option.value || option.key || option.id || option.th || option.en || Object.values(option)[0] + const optionLabel = + typeof option === 'string' + ? option + : option[activeLang] || + option[`label_${activeLang}`] || + option.label || + option.th || + option.en || + optionValue return (