feat: added dropdowns, categories and some filters
This commit is contained in:
		| @@ -1,7 +1,8 @@ | |||||||
| // src/api/CategoriesApi.js | // src/api/CategoriesApi.js | ||||||
| export default class CategoriesApi { | export default class CategoriesApi { | ||||||
|   constructor(token) { |   constructor(token) { | ||||||
|     this.root = 'https://inventory-bff.dream-views.com/api/v1'; |     // IMPORTANTE: singular "Tag", no "Tags" | ||||||
|  |     this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/Tag'; | ||||||
|     this.token = token; |     this.token = token; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -13,65 +14,103 @@ export default class CategoriesApi { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // ---- Tag ---- |   // Utilidad: validar ObjectId (24-hex) — el DAL lo exige para ChangeStatus | ||||||
|  |   static isHex24(v) { | ||||||
|  |     return typeof v === 'string' && /^[0-9a-fA-F]{24}$/.test(v); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // (Opcional) Utilidad: validar campos mínimos en create/update para evitar 400 | ||||||
|  |   static ensureFields(obj, fields) { | ||||||
|  |     const missing = fields.filter((k) => obj[k] === undefined || obj[k] === null || obj[k] === ''); | ||||||
|  |     if (missing.length) { | ||||||
|  |       throw new Error(`Missing required field(s): ${missing.join(', ')}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // GET /Tag/GetAll | ||||||
|   async getAll() { |   async getAll() { | ||||||
|     const res = await fetch(`${this.root}/Tag/GetAll`, { |     const res = await fetch(`${this.baseUrl}/GetAll`, { | ||||||
|       method: 'GET', |       method: 'GET', | ||||||
|       headers: this.headers(false), |       headers: this.headers(false), | ||||||
|     }); |     }); | ||||||
|     if (!res.ok) throw new Error(`GetAll error ${res.status}: ${await res.text()}`); |     if (!res.ok) { | ||||||
|  |       throw new Error(`GetAll error ${res.status}: ${await res.text()}`); | ||||||
|  |     } | ||||||
|     return res.json(); |     return res.json(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // POST /Tag/Create | ||||||
|  |   // payload esperado (min): tenantId, tagName, typeId(_id TagType), slug, displayOrder, icon, parentTagId([]) | ||||||
|   async create(payload) { |   async create(payload) { | ||||||
|     const res = await fetch(`${this.root}/Tag/Create`, { |     // Validaciones básicas para evitar 400 comunes | ||||||
|  |     CategoriesApi.ensureFields(payload, ['tenantId', 'tagName', 'typeId', 'icon']); | ||||||
|  |     if (!Array.isArray(payload.parentTagId)) { | ||||||
|  |       payload.parentTagId = []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const res = await fetch(`${this.baseUrl}/Create`, { | ||||||
|       method: 'POST', |       method: 'POST', | ||||||
|       headers: this.headers(), |       headers: this.headers(), | ||||||
|       body: JSON.stringify(payload), |       body: JSON.stringify(payload), | ||||||
|     }); |     }); | ||||||
|     if (!res.ok) throw new Error(`Create error ${res.status}: ${await res.text()}`); |     if (!res.ok) { | ||||||
|  |       throw new Error(`Create error ${res.status}: ${await res.text()}`); | ||||||
|  |     } | ||||||
|     return res.json(); |     return res.json(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // PUT /Tag/Update | ||||||
|  |   // payload esperado (min): id(GUID) ó _id(24-hex) según backend, + tenantId, tagName, typeId, icon, etc. | ||||||
|   async update(payload) { |   async update(payload) { | ||||||
|     const res = await fetch(`${this.root}/Tag/Update`, { |     CategoriesApi.ensureFields(payload, ['tenantId', 'tagName', 'typeId', 'icon']); | ||||||
|  |     const res = await fetch(`${this.baseUrl}/Update`, { | ||||||
|       method: 'PUT', |       method: 'PUT', | ||||||
|       headers: this.headers(), |       headers: this.headers(), | ||||||
|       body: JSON.stringify(payload), |       body: JSON.stringify(payload), | ||||||
|     }); |     }); | ||||||
|     if (!res.ok) throw new Error(`Update error ${res.status}: ${await res.text()}`); |     if (!res.ok) { | ||||||
|  |       throw new Error(`Update error ${res.status}: ${await res.text()}`); | ||||||
|  |     } | ||||||
|     return res.json(); |     return res.json(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // PATCH ChangeStatus: body { id, status } |   // PATCH /Tag/ChangeStatus | ||||||
|  |   // Debe mandarse { id: <_id 24-hex>, status: 'Active'|'Inactive' } | ||||||
|   async changeStatus({ id, status }) { |   async changeStatus({ id, status }) { | ||||||
|     const res = await fetch(`${this.root}/Tag/ChangeStatus`, { |     if (!CategoriesApi.isHex24(id)) { | ||||||
|  |       // Evitar el 500 "String should contain only hexadecimal digits." | ||||||
|  |       throw new Error('ChangeStatus requires a Mongo _id (24-hex) for "id".'); | ||||||
|  |     } | ||||||
|  |     if (!status) { | ||||||
|  |       throw new Error('ChangeStatus requires "status" field.'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const res = await fetch(`${this.baseUrl}/ChangeStatus`, { | ||||||
|       method: 'PATCH', |       method: 'PATCH', | ||||||
|       headers: this.headers(), |       headers: this.headers(), | ||||||
|       body: JSON.stringify({ id, status }), |       body: JSON.stringify({ id, status }), | ||||||
|     }); |     }); | ||||||
|     if (!res.ok) throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`); |     if (!res.ok) { | ||||||
|     return res.json?.() ?? null; |       throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`); | ||||||
|  |     } | ||||||
|  |     // Algunos endpoints devuelven vacío; devolvemos parsed o true | ||||||
|  |     try { | ||||||
|  |       return await res.json(); | ||||||
|  |     } catch { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // DELETE /Tag/Delete (si lo usan; muchos usan soft-delete con ChangeStatus/Update) | ||||||
|   async delete(payload) { |   async delete(payload) { | ||||||
|     const res = await fetch(`${this.tagUrl}/Delete`, { |     const res = await fetch(`${this.baseUrl}/Delete`, { | ||||||
|       method: 'DELETE', |       method: 'DELETE', | ||||||
|       headers: this.headers(), |       headers: this.headers(), | ||||||
|       body: JSON.stringify(payload), |       body: JSON.stringify(payload), | ||||||
|     }); |     }); | ||||||
|     if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`); |     if (!res.ok) { | ||||||
|     return res.json(); |       throw new Error(`Delete error ${res.status}: ${await res.text()}`); | ||||||
|   } |     } | ||||||
|    |  | ||||||
|   // ---- TagType ---- |  | ||||||
|   async getAllTypes() { |  | ||||||
|     const res = await fetch(`${this.root}/TagType/GetAll`, { |  | ||||||
|       method: 'GET', |  | ||||||
|       headers: this.headers(false), |  | ||||||
|     }); |  | ||||||
|     if (!res.ok) throw new Error(`TagType GetAll error ${res.status}: ${await res.text()}`); |  | ||||||
|     return res.json(); |     return res.json(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								src/api/TagTypeApi.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/api/TagTypeApi.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | // src/api/TagTypeApi.js | ||||||
|  | export default class TagTypeApi { | ||||||
|  |   constructor(token) { | ||||||
|  |     this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/TagType'; | ||||||
|  |     this.token = token; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   headers(json = true) { | ||||||
|  |     return { | ||||||
|  |       accept: 'application/json', | ||||||
|  |       ...(json ? { 'Content-Type': 'application/json' } : {}), | ||||||
|  |       ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async getAll() { | ||||||
|  |     const res = await fetch(`${this.baseUrl}/GetAll`, { | ||||||
|  |       method: 'GET', | ||||||
|  |       headers: this.headers(false), | ||||||
|  |     }); | ||||||
|  |     if (!res.ok) { | ||||||
|  |       throw new Error(`TagType.GetAll ${res.status}: ${await res.text()}`); | ||||||
|  |     } | ||||||
|  |     return res.json(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,20 +1,49 @@ | |||||||
| // src/private/furniture/AddOrEditFurnitureVariantForm.jsx | // src/private/furniture/AddOrEditFurnitureVariantForm.jsx | ||||||
| import { useEffect, useMemo, useState } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||||
| import { Box, Button, TextField, MenuItem, Grid } from '@mui/material'; | import { Box, Button, TextField, MenuItem, Grid, CircularProgress } from '@mui/material'; | ||||||
| import FurnitureVariantApi from '../../api/furnitureVariantApi'; | import FurnitureVariantApi from '../../api/furnitureVariantApi'; | ||||||
|  | import CategoriesApi from '../../api/CategoriesApi'; | ||||||
|  | import TagTypeApi from '../../api/TagTypeApi'; | ||||||
| import { useAuth } from '../../context/AuthContext'; | import { useAuth } from '../../context/AuthContext'; | ||||||
|  |  | ||||||
| const DEFAULT_MODEL_ID = '8a23117b-acaf-4d87-b64f-a98e9b414796'; | const DEFAULT_MODEL_ID = '8a23117b-acaf-4d87-b64f-a98e9b414796'; | ||||||
|  |  | ||||||
|  | const TYPE_NAMES = { | ||||||
|  |   category: 'Furniture category', | ||||||
|  |   provider: 'Provider', | ||||||
|  |   color: 'Color', | ||||||
|  |   line: 'Line', | ||||||
|  |   currency: 'Currency', | ||||||
|  |   material: 'Material', | ||||||
|  |   legs: 'Legs', | ||||||
|  |   origin: 'Origin', | ||||||
|  | }; | ||||||
|  |  | ||||||
| export default function AddOrEditFurnitureVariantForm({ initialData, onAdd, onCancel }) { | export default function AddOrEditFurnitureVariantForm({ initialData, onAdd, onCancel }) { | ||||||
|   const { user } = useAuth(); |   const { user } = useAuth(); | ||||||
|   const token = user?.thalosToken || localStorage.getItem('thalosToken'); |   const token = user?.thalosToken || localStorage.getItem('thalosToken'); | ||||||
|   const api = useMemo(() => (new FurnitureVariantApi(token)), [token]); |  | ||||||
|  |   const variantApi = useMemo(() => new FurnitureVariantApi(token), [token]); | ||||||
|  |   const tagTypeApi = useMemo(() => new TagTypeApi(token), [token]); | ||||||
|  |   const categoriesApi = useMemo(() => new CategoriesApi(token), [token]); | ||||||
|  |  | ||||||
|  |   const [loadingTags, setLoadingTags] = useState(true); | ||||||
|  |   const [typeMap, setTypeMap] = useState({}); | ||||||
|  |   const [options, setOptions] = useState({ | ||||||
|  |     categories: [], | ||||||
|  |     providers: [], | ||||||
|  |     colors: [], | ||||||
|  |     lines: [], | ||||||
|  |     currencies: [], | ||||||
|  |     materials: [], | ||||||
|  |     legs: [], | ||||||
|  |     origins: [], | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   const [form, setForm] = useState({ |   const [form, setForm] = useState({ | ||||||
|     _Id: '', |     _Id: '', | ||||||
|     id: '', |     id: '', | ||||||
|     modelId: '', |     modelId: DEFAULT_MODEL_ID, | ||||||
|     name: '', |     name: '', | ||||||
|     color: '', |     color: '', | ||||||
|     line: '', |     line: '', | ||||||
| @@ -27,180 +56,290 @@ export default function AddOrEditFurnitureVariantForm({ initialData, onAdd, onCa | |||||||
|     status: 'Active', |     status: 'Active', | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   useEffect(() => { |   const setVal = (path, value) => { | ||||||
|     if (initialData) { |     if (path.startsWith('attributes.')) { | ||||||
|       setForm({ |       const k = path.split('.')[1]; | ||||||
|         _Id: initialData._id || initialData._Id || '', |       setForm(prev => ({ ...prev, attributes: { ...prev.attributes, [k]: value } })); | ||||||
|         id: initialData.id || initialData.Id || initialData._id || initialData._Id || '', |  | ||||||
|         modelId: initialData.modelId ?? '', |  | ||||||
|         name: initialData.name ?? '', |  | ||||||
|         color: initialData.color ?? '', |  | ||||||
|         line: initialData.line ?? '', |  | ||||||
|         stock: initialData.stock ?? 0, |  | ||||||
|         price: initialData.price ?? 0, |  | ||||||
|         currency: initialData.currency ?? 'USD', |  | ||||||
|         categoryId: initialData.categoryId ?? '', |  | ||||||
|         providerId: initialData.providerId ?? '', |  | ||||||
|         attributes: { |  | ||||||
|           material: initialData?.attributes?.material ?? '', |  | ||||||
|           legs: initialData?.attributes?.legs ?? '', |  | ||||||
|           origin: initialData?.attributes?.origin ?? '', |  | ||||||
|         }, |  | ||||||
|         status: initialData.status ?? 'Active', |  | ||||||
|       }); |  | ||||||
|     } else { |     } else { | ||||||
|       setForm({ |       setForm(prev => ({ ...prev, [path]: value })); | ||||||
|         _Id: '', |  | ||||||
|         id: '', |  | ||||||
|         modelId: DEFAULT_MODEL_ID, |  | ||||||
|         name: '', |  | ||||||
|         color: '', |  | ||||||
|         line: '', |  | ||||||
|         stock: 0, |  | ||||||
|         price: 0, |  | ||||||
|         currency: 'USD', |  | ||||||
|         categoryId: '', |  | ||||||
|         providerId: '', |  | ||||||
|         attributes: { material: '', legs: '', origin: '' }, |  | ||||||
|         status: 'Active', |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const parsePrice = (p) => { | ||||||
|  |     if (p == null) return 0; | ||||||
|  |     if (typeof p === 'number') return p; | ||||||
|  |     if (typeof p === 'string') return Number(p) || 0; | ||||||
|  |     if (typeof p === 'object' && p.$numberDecimal) return Number(p.$numberDecimal) || 0; | ||||||
|  |     return 0; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // Cargar TagTypes + Tags | ||||||
|  |   useEffect(() => { | ||||||
|  |     let mounted = true; | ||||||
|  |     (async () => { | ||||||
|  |       try { | ||||||
|  |         const [types, tags] = await Promise.all([tagTypeApi.getAll(), categoriesApi.getAll()]); | ||||||
|  |  | ||||||
|  |         const tmap = {}; | ||||||
|  |         types?.forEach(t => { | ||||||
|  |           if (!t?.typeName || !t?._id) return; | ||||||
|  |           tmap[t.typeName] = t._id; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         const byType = (tname) => { | ||||||
|  |           const tid = tmap[tname]; | ||||||
|  |           if (!tid) return []; | ||||||
|  |           return (tags || []) | ||||||
|  |             .filter(tag => tag?.typeId === tid) | ||||||
|  |             .map(tag => ({ ...tag })); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (mounted) { | ||||||
|  |           setTypeMap(tmap); | ||||||
|  |           setOptions({ | ||||||
|  |             categories: byType(TYPE_NAMES.category), | ||||||
|  |             providers: byType(TYPE_NAMES.provider), | ||||||
|  |             colors: byType(TYPE_NAMES.color), | ||||||
|  |             lines: byType(TYPE_NAMES.line), | ||||||
|  |             currencies: byType(TYPE_NAMES.currency), | ||||||
|  |             materials: byType(TYPE_NAMES.material), | ||||||
|  |             legs: byType(TYPE_NAMES.legs), | ||||||
|  |             origins: byType(TYPE_NAMES.origin), | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } catch (err) { | ||||||
|  |         console.error('Failed loading TagTypes/Tags', err); | ||||||
|  |       } finally { | ||||||
|  |         if (mounted) setLoadingTags(false); | ||||||
|  |       } | ||||||
|  |     })(); | ||||||
|  |     return () => { mounted = false; }; | ||||||
|  |   }, [tagTypeApi, categoriesApi]); | ||||||
|  |  | ||||||
|  |   // Sembrar datos al editar | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!initialData) return; | ||||||
|  |     setForm({ | ||||||
|  |       _Id: initialData._Id ?? initialData._id ?? '', | ||||||
|  |       id: initialData.id ?? '', | ||||||
|  |       modelId: initialData.modelId ?? DEFAULT_MODEL_ID, | ||||||
|  |       name: initialData.name ?? '', | ||||||
|  |       color: initialData.color ?? '', | ||||||
|  |       line: initialData.line ?? '', | ||||||
|  |       stock: Number(initialData.stock ?? 0), | ||||||
|  |       price: parsePrice(initialData.price), | ||||||
|  |       currency: initialData.currency ?? 'USD', | ||||||
|  |       categoryId: initialData.categoryId ?? '', | ||||||
|  |       providerId: initialData.providerId ?? '', | ||||||
|  |       attributes: { | ||||||
|  |         material: initialData?.attributes?.material ?? '', | ||||||
|  |         legs: initialData?.attributes?.legs ?? '', | ||||||
|  |         origin: initialData?.attributes?.origin ?? '', | ||||||
|  |       }, | ||||||
|  |       status: initialData.status ?? 'Active', | ||||||
|  |     }); | ||||||
|   }, [initialData]); |   }, [initialData]); | ||||||
|  |  | ||||||
|   const setVal = (name, value) => setForm((p) => ({ ...p, [name]: value })); |   // Si viene GUID/_id/slug => convertir a tagName | ||||||
|   const setAttr = (name, value) => setForm((p) => ({ ...p, attributes: { ...p.attributes, [name]: value } })); |   useEffect(() => { | ||||||
|  |     if (loadingTags) return; | ||||||
|  |  | ||||||
|  |     const toTagNameIfNeeded = (value, list) => { | ||||||
|  |       if (!value) return ''; | ||||||
|  |       if (list.some(t => t.tagName === value)) return value; // ya es tagName | ||||||
|  |       const found = list.find(t => t.id === value || t._id === value || t._id?.$oid === value || t.slug === value); | ||||||
|  |       return found?.tagName || value; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     setForm(prev => ({ | ||||||
|  |       ...prev, | ||||||
|  |       categoryId: toTagNameIfNeeded(prev.categoryId, options.categories), | ||||||
|  |       providerId: toTagNameIfNeeded(prev.providerId, options.providers), | ||||||
|  |       color: toTagNameIfNeeded(prev.color, options.colors), | ||||||
|  |       line: toTagNameIfNeeded(prev.line, options.lines), | ||||||
|  |       currency: toTagNameIfNeeded(prev.currency, options.currencies), | ||||||
|  |       attributes: { | ||||||
|  |         ...prev.attributes, | ||||||
|  |         material: toTagNameIfNeeded(prev.attributes?.material, options.materials), | ||||||
|  |         legs: toTagNameIfNeeded(prev.attributes?.legs, options.legs), | ||||||
|  |         origin: toTagNameIfNeeded(prev.attributes?.origin, options.origins), | ||||||
|  |       } | ||||||
|  |     })); | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps | ||||||
|  |   }, [loadingTags, options]); | ||||||
|  |  | ||||||
|   const handleSubmit = async () => { |   const handleSubmit = async () => { | ||||||
|     try { |     try { | ||||||
|       if (form._Id) { |       const payload = { | ||||||
|         // UPDATE |         ...form, | ||||||
|         const payload = { |         stock: Number(form.stock ?? 0), | ||||||
|           Id: form.id || form._Id, // backend requires Id |         price: Number(form.price ?? 0), | ||||||
|           _Id: form._Id, |         categoryId: form.categoryId || null,  // enviamos tagName | ||||||
|           modelId: form.modelId, |         providerId: form.providerId || null,  // enviamos tagName | ||||||
|           name: form.name, |         attributes: { | ||||||
|           color: form.color, |           material: form.attributes.material || '', | ||||||
|           line: form.line, |           legs: form.attributes.legs || '', | ||||||
|           stock: Number(form.stock) || 0, |           origin: form.attributes.origin || '', | ||||||
|           price: Number(form.price) || 0, |         } | ||||||
|           currency: form.currency, |       }; | ||||||
|           categoryId: form.categoryId, |  | ||||||
|           providerId: form.providerId, |       const isUpdate = Boolean(form.id || form._Id); | ||||||
|           attributes: { |       const saved = isUpdate | ||||||
|             material: form.attributes.material, |         ? await variantApi.updateVariant(payload) | ||||||
|             legs: form.attributes.legs, |         : await variantApi.createVariant(payload); | ||||||
|             origin: form.attributes.origin, |  | ||||||
|           }, |       onAdd?.(saved); | ||||||
|           status: form.status, |  | ||||||
|         }; |  | ||||||
|         await api.updateVariant(payload); |  | ||||||
|       } else { |  | ||||||
|         // CREATE |  | ||||||
|         const payload = { |  | ||||||
|           modelId: form.modelId, |  | ||||||
|           name: form.name, |  | ||||||
|           color: form.color, |  | ||||||
|           line: form.line, |  | ||||||
|           stock: Number(form.stock) || 0, |  | ||||||
|           price: Number(form.price) || 0, |  | ||||||
|           currency: form.currency, |  | ||||||
|           categoryId: form.categoryId, |  | ||||||
|           providerId: form.providerId, |  | ||||||
|           attributes: { |  | ||||||
|             material: form.attributes.material, |  | ||||||
|             legs: form.attributes.legs, |  | ||||||
|             origin: form.attributes.origin, |  | ||||||
|           }, |  | ||||||
|           status: form.status, |  | ||||||
|         }; |  | ||||||
|         await api.createVariant(payload); |  | ||||||
|       } |  | ||||||
|       onAdd?.(); |  | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       console.error('Submit variant failed:', err); |       console.error(err); | ||||||
|  |       alert(err?.message || 'Error saving variant'); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ py: 2 }}> |     <Box> | ||||||
|       <Grid container spacing={2}> |       <Grid container spacing={2}> | ||||||
|         <Grid item xs={12} md={6}> |         <Grid item xs={12} md={6}> | ||||||
|           <TextField |           <TextField label="Model Id" fullWidth value={form.modelId} onChange={(e) => setVal('modelId', e.target.value)} /> | ||||||
|             fullWidth |  | ||||||
|             label="Model Id" |  | ||||||
|             value={form.modelId} |  | ||||||
|             onChange={(e) => setVal('modelId', e.target.value)} |  | ||||||
|             disabled={!initialData} |  | ||||||
|             helperText={!initialData ? 'Preset for new variant' : ''} |  | ||||||
|           /> |  | ||||||
|         </Grid> |         </Grid> | ||||||
|         {form.id && ( |  | ||||||
|           <Grid item xs={12} md={6}> |  | ||||||
|             <TextField |  | ||||||
|               fullWidth |  | ||||||
|               label="Id" |  | ||||||
|               value={form.id} |  | ||||||
|               disabled |  | ||||||
|               helperText="Record identifier (read-only)" |  | ||||||
|             /> |  | ||||||
|           </Grid> |  | ||||||
|         )} |  | ||||||
|         {form._Id && ( |  | ||||||
|           <Grid item xs={12} md={6}> |  | ||||||
|             <TextField |  | ||||||
|               fullWidth |  | ||||||
|               label="_Id" |  | ||||||
|               value={form._Id} |  | ||||||
|               disabled |  | ||||||
|               helperText="Mongo identifier (read-only)" |  | ||||||
|             /> |  | ||||||
|           </Grid> |  | ||||||
|         )} |  | ||||||
|         <Grid item xs={12} md={6}> |         <Grid item xs={12} md={6}> | ||||||
|           <TextField fullWidth label="Name" value={form.name} onChange={(e) => setVal('name', e.target.value)} /> |           <TextField label="Name" fullWidth value={form.name} onChange={(e) => setVal('name', e.target.value)} /> | ||||||
|         </Grid> |         </Grid> | ||||||
|  |  | ||||||
|         <Grid item xs={12} md={4}> |         {/* Clasificación */} | ||||||
|           <TextField fullWidth label="Color" value={form.color} onChange={(e) => setVal('color', e.target.value)} /> |         <Grid item xs={12} md={6}> | ||||||
|         </Grid> |           <TextField | ||||||
|         <Grid item xs={12} md={4}> |             select | ||||||
|           <TextField fullWidth label="Line" value={form.line} onChange={(e) => setVal('line', e.target.value)} /> |             label="Category" | ||||||
|         </Grid> |             fullWidth | ||||||
|         <Grid item xs={12} md={2}> |             value={form.categoryId} | ||||||
|           <TextField fullWidth type="number" label="Stock" value={form.stock} onChange={(e) => setVal('stock', e.target.value)} /> |             onChange={(e) => setVal('categoryId', e.target.value)} | ||||||
|         </Grid> |             helperText="Se envía el tagName por ahora" | ||||||
|         <Grid item xs={12} md={2}> |           > | ||||||
|           <TextField fullWidth type="number" label="Price" value={form.price} onChange={(e) => setVal('price', e.target.value)} /> |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.categories.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|         </Grid> |         </Grid> | ||||||
|  |  | ||||||
|         <Grid item xs={12} md={3}> |         <Grid item xs={12} md={6}> | ||||||
|           <TextField fullWidth label="Currency" value={form.currency} onChange={(e) => setVal('currency', e.target.value)} /> |           <TextField | ||||||
|         </Grid> |             select | ||||||
|         <Grid item xs={12} md={4}> |             label="Provider" | ||||||
|           <TextField fullWidth label="Category Id" value={form.categoryId} onChange={(e) => setVal('categoryId', e.target.value)} /> |             fullWidth | ||||||
|         </Grid> |             value={form.providerId} | ||||||
|         <Grid item xs={12} md={5}> |             onChange={(e) => setVal('providerId', e.target.value)} | ||||||
|           <TextField fullWidth label="Provider Id" value={form.providerId} onChange={(e) => setVal('providerId', e.target.value)} /> |           > | ||||||
|  |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.providers.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|         </Grid> |         </Grid> | ||||||
|  |  | ||||||
|  |         {/* Específicos de variante */} | ||||||
|         <Grid item xs={12} md={4}> |         <Grid item xs={12} md={4}> | ||||||
|           <TextField fullWidth label="Material" value={form.attributes.material} onChange={(e) => setAttr('material', e.target.value)} /> |           <TextField | ||||||
|         </Grid> |             select | ||||||
|         <Grid item xs={12} md={4}> |             label="Color" | ||||||
|           <TextField fullWidth label="Legs" value={form.attributes.legs} onChange={(e) => setAttr('legs', e.target.value)} /> |             fullWidth | ||||||
|         </Grid> |             value={form.color} | ||||||
|         <Grid item xs={12} md={4}> |             onChange={(e) => setVal('color', e.target.value)} | ||||||
|           <TextField fullWidth label="Origin" value={form.attributes.origin} onChange={(e) => setAttr('origin', e.target.value)} /> |           > | ||||||
|  |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.colors.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|         </Grid> |         </Grid> | ||||||
|  |  | ||||||
|         <Grid item xs={12} md={4}> |         <Grid item xs={12} md={4}> | ||||||
|           <TextField |           <TextField | ||||||
|             fullWidth |  | ||||||
|             select |             select | ||||||
|             label="Status" |             label="Line" | ||||||
|             value={form.status} |             fullWidth | ||||||
|             onChange={(e) => setVal('status', e.target.value)} |             value={form.line} | ||||||
|  |             onChange={(e) => setVal('line', e.target.value)} | ||||||
|           > |           > | ||||||
|  |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.lines.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|  |         </Grid> | ||||||
|  |  | ||||||
|  |         <Grid item xs={12} md={4}> | ||||||
|  |           <TextField | ||||||
|  |             select | ||||||
|  |             label="Currency" | ||||||
|  |             fullWidth | ||||||
|  |             value={form.currency} | ||||||
|  |             onChange={(e) => setVal('currency', e.target.value)} | ||||||
|  |           > | ||||||
|  |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.currencies.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|  |         </Grid> | ||||||
|  |  | ||||||
|  |         {/* Atributos como catálogos */} | ||||||
|  |         <Grid item xs={12} md={4}> | ||||||
|  |           <TextField | ||||||
|  |             select | ||||||
|  |             label="Material" | ||||||
|  |             fullWidth | ||||||
|  |             value={form.attributes.material} | ||||||
|  |             onChange={(e) => setVal('attributes.material', e.target.value)} | ||||||
|  |           > | ||||||
|  |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.materials.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|  |         </Grid> | ||||||
|  |  | ||||||
|  |         <Grid item xs={12} md={4}> | ||||||
|  |           <TextField | ||||||
|  |             select | ||||||
|  |             label="Legs" | ||||||
|  |             fullWidth | ||||||
|  |             value={form.attributes.legs} | ||||||
|  |             onChange={(e) => setVal('attributes.legs', e.target.value)} | ||||||
|  |           > | ||||||
|  |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.legs.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|  |         </Grid> | ||||||
|  |  | ||||||
|  |         <Grid item xs={12} md={4}> | ||||||
|  |           <TextField | ||||||
|  |             select | ||||||
|  |             label="Origin" | ||||||
|  |             fullWidth | ||||||
|  |             value={form.attributes.origin} | ||||||
|  |             onChange={(e) => setVal('attributes.origin', e.target.value)} | ||||||
|  |           > | ||||||
|  |             {loadingTags && <MenuItem value="" disabled><CircularProgress size={16} /> Cargando…</MenuItem>} | ||||||
|  |             {!loadingTags && options.origins.filter(t => t.status !== 'Inactive').map(tag => ( | ||||||
|  |               <MenuItem key={tag._id?.$oid || tag._id || tag.id} value={tag.tagName}>{tag.tagName}</MenuItem> | ||||||
|  |             ))} | ||||||
|  |           </TextField> | ||||||
|  |         </Grid> | ||||||
|  |  | ||||||
|  |         {/* Números */} | ||||||
|  |         <Grid item xs={12} md={4}> | ||||||
|  |           <TextField label="Stock" type="number" fullWidth value={form.stock} onChange={(e) => setVal('stock', e.target.value)} /> | ||||||
|  |         </Grid> | ||||||
|  |         <Grid item xs={12} md={4}> | ||||||
|  |           <TextField label="Price" type="number" fullWidth value={form.price} onChange={(e) => setVal('price', e.target.value)} /> | ||||||
|  |         </Grid> | ||||||
|  |  | ||||||
|  |         {/* Status */} | ||||||
|  |         <Grid item xs={12} md={4}> | ||||||
|  |           <TextField select label="Status" fullWidth value={form.status} onChange={(e) => setVal('status', e.target.value)}> | ||||||
|             <MenuItem value="Active">Active</MenuItem> |             <MenuItem value="Active">Active</MenuItem> | ||||||
|             <MenuItem value="Inactive">Inactive</MenuItem> |             <MenuItem value="Inactive">Inactive</MenuItem> | ||||||
|           </TextField> |           </TextField> | ||||||
| @@ -213,4 +352,4 @@ export default function AddOrEditFurnitureVariantForm({ initialData, onAdd, onCa | |||||||
|       </Box> |       </Box> | ||||||
|     </Box> |     </Box> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,277 +1,285 @@ | |||||||
|  | // src/private/furniture/FurnitureVariantManagement.jsx | ||||||
| import SectionContainer from '../../components/SectionContainer'; | import SectionContainer from '../../components/SectionContainer'; | ||||||
| import { useEffect, useRef, useState, useMemo } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||||
| import { DataGrid } from '@mui/x-data-grid'; | import { DataGrid } from '@mui/x-data-grid'; | ||||||
| import { | import { | ||||||
|   Typography, Button, Dialog, DialogTitle, DialogContent, |   Typography, Button, Dialog, DialogTitle, DialogContent, | ||||||
|   IconButton, Box, ToggleButton, ToggleButtonGroup |   IconButton, Box, FormControlLabel, Switch, Tooltip | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import EditRoundedIcon from '@mui/icons-material/EditRounded'; | import EditRoundedIcon from '@mui/icons-material/EditRounded'; | ||||||
| import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; | import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; | ||||||
|  | import AddRoundedIcon from '@mui/icons-material/AddRounded'; | ||||||
| import AddOrEditFurnitureVariantForm from './AddOrEditFurnitureVariantForm'; | import AddOrEditFurnitureVariantForm from './AddOrEditFurnitureVariantForm'; | ||||||
| import FurnitureVariantApi from '../../api/furnitureVariantApi'; | import FurnitureVariantApi from '../../api/furnitureVariantApi'; | ||||||
|  | import CategoriesApi from '../../api/CategoriesApi'; | ||||||
|  | import TagTypeApi from '../../api/TagTypeApi'; | ||||||
| import { useAuth } from '../../context/AuthContext'; | import { useAuth } from '../../context/AuthContext'; | ||||||
| import useApiToast from '../../hooks/useApiToast'; | import useApiToast from '../../hooks/useApiToast'; | ||||||
|  |  | ||||||
| const columnsBase = [ | const parsePrice = (p) => { | ||||||
|   { field: 'modelId', headerName: 'Model Id', width: 260 }, |   if (p == null) return 0; | ||||||
|   { field: 'name', headerName: 'Name', width: 220 }, |   if (typeof p === 'number') return p; | ||||||
|   { field: 'color', headerName: 'Color', width: 160 }, |   if (typeof p === 'string') return Number(p) || 0; | ||||||
|   { field: 'line', headerName: 'Line', width: 160 }, |   if (typeof p === 'object' && p.$numberDecimal) return Number(p.$numberDecimal) || 0; | ||||||
|   { field: 'stock', headerName: 'Stock', width: 100, type: 'number' }, |   return 0; | ||||||
|   { field: 'price', headerName: 'Price', width: 120, type: 'number', | }; | ||||||
|     valueFormatter: (p) => p?.value != null ? Number(p.value).toFixed(2) : '—' |  | ||||||
|   }, | const TYPE_NAMES = { | ||||||
|   { field: 'currency', headerName: 'Currency', width: 120 }, |   category: 'Furniture category', | ||||||
|   { field: 'categoryId', headerName: 'Category Id', width: 280 }, |   provider: 'Provider', | ||||||
|   { field: 'providerId', headerName: 'Provider Id', width: 280 }, |   color: 'Color', | ||||||
|   { |   line: 'Line', | ||||||
|     field: 'attributes.material', |   currency: 'Currency', | ||||||
|     headerName: 'Material', |   material: 'Material', | ||||||
|     width: 160, |   legs: 'Legs', | ||||||
|     valueGetter: (p) => p?.row?.attributes?.material ?? '—' |   origin: 'Origin', | ||||||
|   }, | }; | ||||||
|   { |  | ||||||
|     field: 'attributes.legs', |  | ||||||
|     headerName: 'Legs', |  | ||||||
|     width: 160, |  | ||||||
|     valueGetter: (p) => p?.row?.attributes?.legs ?? '—' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     field: 'attributes.origin', |  | ||||||
|     headerName: 'Origin', |  | ||||||
|     width: 160, |  | ||||||
|     valueGetter: (p) => p?.row?.attributes?.origin ?? '—' |  | ||||||
|   }, |  | ||||||
|   { field: 'status', headerName: 'Status', width: 120 }, |  | ||||||
|   { |  | ||||||
|     field: 'createdAt', |  | ||||||
|     headerName: 'Created At', |  | ||||||
|     width: 180, |  | ||||||
|     valueFormatter: (p) => p?.value ? new Date(p.value).toLocaleString() : '—' |  | ||||||
|   }, |  | ||||||
|   { field: 'createdBy', headerName: 'Created By', width: 160, valueGetter: (p) => p?.row?.createdBy ?? '—' }, |  | ||||||
|   { |  | ||||||
|     field: 'updatedAt', |  | ||||||
|     headerName: 'Updated At', |  | ||||||
|     width: 180, |  | ||||||
|     valueFormatter: (p) => p?.value ? new Date(p.value).toLocaleString() : '—' |  | ||||||
|   }, |  | ||||||
|   { field: 'updatedBy', headerName: 'Updated By', width: 160, valueGetter: (p) => p?.row?.updatedBy ?? '—' }, |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export default function FurnitureVariantManagement() { | export default function FurnitureVariantManagement() { | ||||||
|   const { user } = useAuth(); |   const { user } = useAuth(); | ||||||
|   const token = user?.thalosToken || localStorage.getItem('thalosToken'); |   const token = user?.thalosToken || localStorage.getItem('thalosToken'); | ||||||
|   const apiRef = useRef(null); |  | ||||||
|  |   const api = useMemo(() => new FurnitureVariantApi(token), [token]); | ||||||
|  |   const tagTypeApi = useMemo(() => new TagTypeApi(token), [token]); | ||||||
|  |   const categoriesApi = useMemo(() => new CategoriesApi(token), [token]); | ||||||
|  |  | ||||||
|  |   const toast = useApiToast(); | ||||||
|  |  | ||||||
|   const [rows, setRows] = useState([]); |   const [rows, setRows] = useState([]); | ||||||
|   const [statusFilter, setStatusFilter] = useState('Active'); // <- por defecto Active |   const [rawRows, setRawRows] = useState([]); | ||||||
|   const [open, setOpen] = useState(false); |   const [open, setOpen] = useState(false); | ||||||
|   const [editingData, setEditingData] = useState(null); |   const [editRow, setEditRow] = useState(null); | ||||||
|   const [confirmOpen, setConfirmOpen] = useState(false); |   const [showInactive, setShowInactive] = useState(false); | ||||||
|   const [rowToDelete, setRowToDelete] = useState(null); |   const [loading, setLoading] = useState(true); | ||||||
|   const { handleError } = useApiToast(); |  | ||||||
|   const hasLoaded = useRef(false); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |   // Tags | ||||||
|     apiRef.current = new FurnitureVariantApi(token); |   const [loadingTags, setLoadingTags] = useState(true); | ||||||
|   }, [token]); |   const [typeMap, setTypeMap] = useState({}); | ||||||
|  |   const [byType, setByType] = useState({ | ||||||
|  |     [TYPE_NAMES.category]: [], | ||||||
|  |     [TYPE_NAMES.provider]: [], | ||||||
|  |     [TYPE_NAMES.color]: [], | ||||||
|  |     [TYPE_NAMES.line]: [], | ||||||
|  |     [TYPE_NAMES.currency]: [], | ||||||
|  |     [TYPE_NAMES.material]: [], | ||||||
|  |     [TYPE_NAMES.legs]: [], | ||||||
|  |     [TYPE_NAMES.origin]: [], | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   useEffect(() => { |   const buildLabelResolver = (typeName) => { | ||||||
|     if (!hasLoaded.current) { |     const list = byType[typeName] || []; | ||||||
|       loadData(); |     return (value) => { | ||||||
|       hasLoaded.current = true; |       if (!value && value !== 0) return '—'; | ||||||
|     } |       if (list.some(t => t.tagName === value)) return value; // ya es tagName | ||||||
|   }, []); |       const found = list.find(t => | ||||||
|  |         t.id === value || | ||||||
|   const loadData = async () => { |         t._id === value || | ||||||
|     try { |         t._id?.$oid === value || | ||||||
|       const data = await apiRef.current.getAllVariants(); |         t.slug === value | ||||||
|       setRows(Array.isArray(data) ? data : []); |       ); | ||||||
|     } catch (err) { |       return found?.tagName || String(value); | ||||||
|       console.error('Error loading variants:', err); |  | ||||||
|       handleError(err, 'Failed to load furniture variants'); |  | ||||||
|       setRows([]); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const handleEditClick = (params) => { |  | ||||||
|     if (!params?.row) return; |  | ||||||
|     const r = params.row; |  | ||||||
|     const normalized = { |  | ||||||
|       _id: r._id || r._Id || '', |  | ||||||
|       _Id: r._id || r._Id || '', |  | ||||||
|       id: r.id || r.Id || '', |  | ||||||
|       modelId: r.modelId ?? '', |  | ||||||
|       name: r.name ?? '', |  | ||||||
|       color: r.color ?? '', |  | ||||||
|       line: r.line ?? '', |  | ||||||
|       stock: r.stock ?? 0, |  | ||||||
|       price: r.price ?? 0, |  | ||||||
|       currency: r.currency ?? 'USD', |  | ||||||
|       categoryId: r.categoryId ?? '', |  | ||||||
|       providerId: r.providerId ?? '', |  | ||||||
|       attributes: { |  | ||||||
|         material: r?.attributes?.material ?? '', |  | ||||||
|         legs: r?.attributes?.legs ?? '', |  | ||||||
|         origin: r?.attributes?.origin ?? '', |  | ||||||
|       }, |  | ||||||
|       status: r.status ?? 'Active', |  | ||||||
|     }; |     }; | ||||||
|     setEditingData(normalized); |  | ||||||
|     setOpen(true); |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleDeleteClick = (row) => { |   const labelCategory = useMemo(() => buildLabelResolver(TYPE_NAMES.category), [byType]); | ||||||
|     setRowToDelete(row); |   const labelProvider = useMemo(() => buildLabelResolver(TYPE_NAMES.provider), [byType]); | ||||||
|     setConfirmOpen(true); |   const labelColor = useMemo(() => buildLabelResolver(TYPE_NAMES.color), [byType]); | ||||||
|   }; |   const labelLine = useMemo(() => buildLabelResolver(TYPE_NAMES.line), [byType]); | ||||||
|  |   const labelCurrency = useMemo(() => buildLabelResolver(TYPE_NAMES.currency), [byType]); | ||||||
|  |   const labelMaterial = useMemo(() => buildLabelResolver(TYPE_NAMES.material), [byType]); | ||||||
|  |   const labelLegs = useMemo(() => buildLabelResolver(TYPE_NAMES.legs), [byType]); | ||||||
|  |   const labelOrigin = useMemo(() => buildLabelResolver(TYPE_NAMES.origin), [byType]); | ||||||
|  |  | ||||||
|   const handleConfirmDelete = async () => { |   // Cargar TagTypes + Tags | ||||||
|  |   useEffect(() => { | ||||||
|  |     let mounted = true; | ||||||
|  |     (async () => { | ||||||
|  |       try { | ||||||
|  |         const [types, tags] = await Promise.all([tagTypeApi.getAll(), categoriesApi.getAll()]); | ||||||
|  |         const tmap = {}; | ||||||
|  |         types?.forEach(t => { | ||||||
|  |           if (!t?.typeName || !t?._id) return; | ||||||
|  |           tmap[t.typeName] = t._id; | ||||||
|  |         }); | ||||||
|  |         const group = (tname) => { | ||||||
|  |           const tid = tmap[tname]; | ||||||
|  |           if (!tid) return []; | ||||||
|  |           return (tags || []).filter(tag => tag?.typeId === tid); | ||||||
|  |         }; | ||||||
|  |         if (mounted) { | ||||||
|  |           setTypeMap(tmap); | ||||||
|  |           setByType({ | ||||||
|  |             [TYPE_NAMES.category]: group(TYPE_NAMES.category), | ||||||
|  |             [TYPE_NAMES.provider]: group(TYPE_NAMES.provider), | ||||||
|  |             [TYPE_NAMES.color]: group(TYPE_NAMES.color), | ||||||
|  |             [TYPE_NAMES.line]: group(TYPE_NAMES.line), | ||||||
|  |             [TYPE_NAMES.currency]: group(TYPE_NAMES.currency), | ||||||
|  |             [TYPE_NAMES.material]: group(TYPE_NAMES.material), | ||||||
|  |             [TYPE_NAMES.legs]: group(TYPE_NAMES.legs), | ||||||
|  |             [TYPE_NAMES.origin]: group(TYPE_NAMES.origin), | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } catch (e) { | ||||||
|  |         console.error('Failed loading TagTypes/Tags', e); | ||||||
|  |       } finally { | ||||||
|  |         if (mounted) setLoadingTags(false); | ||||||
|  |       } | ||||||
|  |     })(); | ||||||
|  |     return () => { mounted = false; }; | ||||||
|  |   }, [tagTypeApi, categoriesApi]); | ||||||
|  |  | ||||||
|  |   // Cargar variants | ||||||
|  |   const load = async () => { | ||||||
|     try { |     try { | ||||||
|       if (!apiRef.current || !(rowToDelete?._id || rowToDelete?._Id)) throw new Error('Missing API or id'); |       setLoading(true); | ||||||
|       // Soft-delete vía Update (status=Inactive) |       const data = await api.getAllVariants(); | ||||||
|       const payload = { |       const normalized = (data || []).map((r, idx) => ({ | ||||||
|         id: rowToDelete.id || rowToDelete.Id || '', |         id: r.id || r._id || `row-${idx}`, | ||||||
|         _Id: rowToDelete._id || rowToDelete._Id, |         _Id: r._id || r._Id || '', | ||||||
|         modelId: rowToDelete.modelId, |         modelId: r.modelId ?? '', | ||||||
|         name: rowToDelete.name, |         name: r.name ?? '', | ||||||
|         color: rowToDelete.color, |         categoryId: r.categoryId ?? '', | ||||||
|         line: rowToDelete.line, |         providerId: r.providerId ?? '', | ||||||
|         stock: rowToDelete.stock, |         color: r.color ?? '', | ||||||
|         price: rowToDelete.price, |         line: r.line ?? '', | ||||||
|         currency: rowToDelete.currency, |         stock: Number(r.stock ?? 0), | ||||||
|         categoryId: rowToDelete.categoryId, |         price: parsePrice(r.price), | ||||||
|         providerId: rowToDelete.providerId, |         currency: r.currency ?? 'USD', | ||||||
|         attributes: { |         attributes: { | ||||||
|           material: rowToDelete?.attributes?.material ?? '', |           material: r?.attributes?.material ?? '', | ||||||
|           legs: rowToDelete?.attributes?.legs ?? '', |           legs: r?.attributes?.legs ?? '', | ||||||
|           origin: rowToDelete?.attributes?.origin ?? '', |           origin: r?.attributes?.origin ?? '', | ||||||
|         }, |         }, | ||||||
|         status: 'Inactive', |         status: r.status ?? 'Active', | ||||||
|       }; |         createdAt: r.createdAt ?? null, | ||||||
|       await apiRef.current.updateVariant(payload); |         createdBy: r.createdBy ?? null, | ||||||
|       await loadData(); |       })); | ||||||
|     } catch (e) { |       setRawRows(normalized); | ||||||
|       console.error('Delete failed:', e); |       setRows(normalized.filter(r => showInactive ? true : r.status !== 'Inactive')); | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error(err); | ||||||
|  |       toast.error(err?.message || 'Error loading variants'); | ||||||
|     } finally { |     } finally { | ||||||
|       setConfirmOpen(false); |       setLoading(false); | ||||||
|       setRowToDelete(null); |  | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // --- FILTRO DE ESTADO --- |   useEffect(() => { load(); /* eslint-disable-next-line */ }, []); | ||||||
|   const filteredRows = useMemo(() => { |   useEffect(() => { | ||||||
|     if (statusFilter === 'All') return rows; |     setRows(rawRows.filter(r => showInactive ? true : r.status !== 'Inactive')); | ||||||
|     const want = String(statusFilter).toLowerCase(); |   }, [showInactive, rawRows]); | ||||||
|     return rows.filter((r) => String(r?.status ?? 'Active').toLowerCase() === want); |  | ||||||
|   }, [rows, statusFilter]); |  | ||||||
|  |  | ||||||
|   const columns = [ |   const columns = [ | ||||||
|  |     { field: 'modelId', headerName: 'Model Id', width: 220 }, | ||||||
|  |     { field: 'name', headerName: 'Name', width: 200 }, | ||||||
|  |     { field: 'categoryId', headerName: 'Category', width: 170, valueGetter: (p) => labelCategory(p?.row?.categoryId) }, | ||||||
|  |     { field: 'providerId', headerName: 'Provider', width: 170, valueGetter: (p) => labelProvider(p?.row?.providerId) }, | ||||||
|  |     { field: 'color', headerName: 'Color', width: 130, valueGetter: (p) => labelColor(p?.row?.color) }, | ||||||
|  |     { field: 'line', headerName: 'Line', width: 130, valueGetter: (p) => labelLine(p?.row?.line) }, | ||||||
|  |     { | ||||||
|  |       field: 'price', | ||||||
|  |       headerName: 'Price', | ||||||
|  |       width: 130, | ||||||
|  |       type: 'number', | ||||||
|  |       valueGetter: (p) => parsePrice(p?.row?.price), | ||||||
|  |       renderCell: (p) => { | ||||||
|  |         const currency = labelCurrency(p?.row?.currency || 'USD'); | ||||||
|  |         const val = parsePrice(p?.row?.price); | ||||||
|  |         try { | ||||||
|  |           return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency || 'USD' }).format(val); | ||||||
|  |         } catch { | ||||||
|  |           return `${currency} ${val.toFixed(2)}`; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { field: 'currency', headerName: 'Currency', width: 120, valueGetter: (p) => labelCurrency(p?.row?.currency) }, | ||||||
|  |     { field: 'stock', headerName: 'Stock', width: 100, type: 'number', valueGetter: (p) => Number(p?.row?.stock ?? 0) }, | ||||||
|  |     { field: 'attributes.material', headerName: 'Material', width: 150, valueGetter: (p) => labelMaterial(p?.row?.attributes?.material) }, | ||||||
|  |     { field: 'attributes.legs', headerName: 'Legs', width: 140, valueGetter: (p) => labelLegs(p?.row?.attributes?.legs) }, | ||||||
|  |     { field: 'attributes.origin', headerName: 'Origin', width: 150, valueGetter: (p) => labelOrigin(p?.row?.attributes?.origin) }, | ||||||
|  |     { field: 'status', headerName: 'Status', width: 120 }, | ||||||
|     { |     { | ||||||
|       field: 'actions', |       field: 'actions', | ||||||
|       headerName: '', |       headerName: '', | ||||||
|       width: 130, |       sortable: false, | ||||||
|       renderCell: (params) => ( |       width: 110, | ||||||
|         <Box display="flex" alignItems="center" justifyContent="flex-start" height="100%" gap={1}> |       renderCell: (p) => ( | ||||||
|           <IconButton |         <Box display="flex" gap={1}> | ||||||
|             size="small" |           <Tooltip title="Edit"> | ||||||
|             sx={{ |             <IconButton size="small" onClick={() => { setEditRow(p.row); setOpen(true); }}> | ||||||
|               backgroundColor: '#DFCCBC', |               <EditRoundedIcon fontSize="small" /> | ||||||
|               color: '#26201A', |             </IconButton> | ||||||
|               '&:hover': { backgroundColor: '#C2B2A4' }, |           </Tooltip> | ||||||
|               borderRadius: 2, |           <Tooltip title={p.row.status === 'Active' ? 'Deactivate' : 'Activate'}> | ||||||
|               p: 1, |             <IconButton | ||||||
|             }} |               size="small" | ||||||
|             onClick={() => handleEditClick(params)} |               onClick={async () => { | ||||||
|           > |                 try { | ||||||
|             <EditRoundedIcon fontSize="small" /> |                   const updated = { ...p.row, status: p.row.status === 'Active' ? 'Inactive' : 'Active' }; | ||||||
|           </IconButton> |                   await api.updateVariant(updated); | ||||||
|           <IconButton |                   setRawRows(prev => prev.map(r => r.id === p.row.id ? updated : r)); | ||||||
|             size="small" |                 } catch (err) { | ||||||
|             sx={{ |                   console.error(err); | ||||||
|               backgroundColor: '#FBE9E7', |                   toast.error(err?.message || 'Error updating status'); | ||||||
|               color: '#C62828', |                 } | ||||||
|               '&:hover': { backgroundColor: '#EF9A9A' }, |               }} | ||||||
|               borderRadius: 2, |             > | ||||||
|               p: 1, |               <DeleteRoundedIcon fontSize="small" /> | ||||||
|             }} |             </IconButton> | ||||||
|             onClick={() => handleDeleteClick(params?.row)} |           </Tooltip> | ||||||
|           > |  | ||||||
|             <DeleteRoundedIcon fontSize="small" /> |  | ||||||
|           </IconButton> |  | ||||||
|         </Box> |         </Box> | ||||||
|       ) |       ) | ||||||
|     }, |     }, | ||||||
|     ...columnsBase, |  | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <SectionContainer sx={{ width: '100%' }}> |     <SectionContainer> | ||||||
|       <Dialog open={open} onClose={() => { setOpen(false); setEditingData(null); }} maxWidth="md" fullWidth> |       <Box display="flex" alignItems="center" justifyContent="space-between" mb={2}> | ||||||
|         <DialogTitle>{editingData ? 'Edit Product' : 'Add Product'}</DialogTitle> |         <Typography variant="h6">Furniture Variants</Typography> | ||||||
|         <DialogContent> |         <Box display="flex" alignItems="center" gap={2}> | ||||||
|           <AddOrEditFurnitureVariantForm |           <FormControlLabel control={<Switch checked={showInactive} onChange={(_, v) => setShowInactive(v)} />} label="Show Inactive" /> | ||||||
|             initialData={editingData} |           <Button variant="contained" className="button-gold" startIcon={<AddRoundedIcon />} onClick={() => { setEditRow(null); setOpen(true); }}> | ||||||
|             onCancel={() => { setOpen(false); setEditingData(null); }} |             Add Variant | ||||||
|             onAdd={async () => { |           </Button> | ||||||
|               await loadData(); |         </Box> | ||||||
|               setOpen(false); |  | ||||||
|               setEditingData(null); |  | ||||||
|             }} |  | ||||||
|           /> |  | ||||||
|         </DialogContent> |  | ||||||
|       </Dialog> |  | ||||||
|  |  | ||||||
|       <Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}> |  | ||||||
|         <DialogTitle>Confirm Delete</DialogTitle> |  | ||||||
|         <DialogContent> |  | ||||||
|           <Typography> |  | ||||||
|             Are you sure you want to delete <strong>{rowToDelete?.name}</strong>? |  | ||||||
|           </Typography> |  | ||||||
|           <Box mt={2} display="flex" justifyContent="flex-end" gap={1}> |  | ||||||
|             <Button onClick={() => setConfirmOpen(false)} className="button-transparent">Cancel</Button> |  | ||||||
|             <Button variant="contained" onClick={handleConfirmDelete} className="button-gold">Delete</Button> |  | ||||||
|           </Box> |  | ||||||
|         </DialogContent> |  | ||||||
|       </Dialog> |  | ||||||
|  |  | ||||||
|       {/* Toolbar de filtro */} |  | ||||||
|       <Box mt={1} mb={1} display="flex" justifyContent="flex-end"> |  | ||||||
|         <ToggleButtonGroup |  | ||||||
|           value={statusFilter} |  | ||||||
|           exclusive |  | ||||||
|           onChange={(_, v) => v && setStatusFilter(v)} |  | ||||||
|           size="small" |  | ||||||
|         > |  | ||||||
|           <ToggleButton value="Active">Active</ToggleButton> |  | ||||||
|           <ToggleButton value="All">All</ToggleButton> |  | ||||||
|           <ToggleButton value="Inactive">Inactive</ToggleButton> |  | ||||||
|         </ToggleButtonGroup> |  | ||||||
|       </Box> |       </Box> | ||||||
|  |  | ||||||
|       <Box sx={{ width: '100%', overflowX: 'auto' }}> |       <Box sx={{ width: '100%', height: 560 }}> | ||||||
|         <DataGrid |         <DataGrid | ||||||
|           rows={filteredRows} |           rows={rows} | ||||||
|           columns={columns} |           columns={columns} | ||||||
|           pageSize={5} |           disableRowSelectionOnClick | ||||||
|           rowsPerPageOptions={[5]} |           loading={loading || loadingTags} | ||||||
|           getRowSpacing={() => ({ top: 4, bottom: 4 })} |           pageSizeOptions={[10, 25, 50]} | ||||||
|           getRowId={(row) => row._id || row.id || row.modelId} |           initialState={{ | ||||||
|           autoHeight |             pagination: { paginationModel: { pageSize: 10 } }, | ||||||
|           disableColumnMenu |             columns: { columnVisibilityModel: { id: false, _Id: false } }, | ||||||
|  |           }} | ||||||
|           getRowHeight={() => 'auto'} |           getRowHeight={() => 'auto'} | ||||||
|           sx={{ |           sx={{ | ||||||
|             '& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center' }, |             '& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center' }, | ||||||
|             '& .MuiDataGrid-columnHeader': { display: 'flex', alignItems: 'center' }, |             '& .MuiDataGrid-columnHeader': { display: 'flex', alignItems: 'center' }, | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|         <Box display="flex" justifyContent="flex-end" mt={2}> |  | ||||||
|           <Button variant="contained" className="button-gold" onClick={() => setOpen(true)}> |  | ||||||
|             Add Variant |  | ||||||
|           </Button> |  | ||||||
|         </Box> |  | ||||||
|       </Box> |       </Box> | ||||||
|  |  | ||||||
|  |       <Dialog open={open} onClose={() => setOpen(false)} maxWidth="md" fullWidth> | ||||||
|  |         <DialogTitle>{editRow ? 'Edit Variant' : 'Add Variant'}</DialogTitle> | ||||||
|  |         <DialogContent> | ||||||
|  |           <AddOrEditFurnitureVariantForm | ||||||
|  |             initialData={editRow} | ||||||
|  |             onAdd={(saved) => { | ||||||
|  |               setOpen(false); | ||||||
|  |               if (editRow) { | ||||||
|  |                 setRawRows(prev => prev.map(r => (r.id === editRow.id ? { ...saved } : r))); | ||||||
|  |               } else { | ||||||
|  |                 setRawRows(prev => [{ ...saved, id: saved.id || saved._id || `row-${Date.now()}` }, ...prev]); | ||||||
|  |               } | ||||||
|  |             }} | ||||||
|  |             onCancel={() => setOpen(false)} | ||||||
|  |           /> | ||||||
|  |         </DialogContent> | ||||||
|  |       </Dialog> | ||||||
|     </SectionContainer> |     </SectionContainer> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user