From 7e88f9ac4b7faae9243f3feddc7f5452d8581fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20White?= Date: Wed, 3 Sep 2025 16:03:31 -0600 Subject: [PATCH] feat: added dropdowns, categories and some filters --- src/api/CategoriesApi.js | 89 +++- src/api/TagTypeApi.js | 26 + .../AddOrEditFurnitureVariantForm.jsx | 437 +++++++++++------ .../fornitures/FurnitureVariantManagement.jsx | 448 +++++++++--------- 4 files changed, 606 insertions(+), 394 deletions(-) create mode 100644 src/api/TagTypeApi.js diff --git a/src/api/CategoriesApi.js b/src/api/CategoriesApi.js index 5a2310d..f726a11 100644 --- a/src/api/CategoriesApi.js +++ b/src/api/CategoriesApi.js @@ -1,7 +1,8 @@ // src/api/CategoriesApi.js export default class CategoriesApi { 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; } @@ -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() { - const res = await fetch(`${this.root}/Tag/GetAll`, { + const res = await fetch(`${this.baseUrl}/GetAll`, { method: 'GET', 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(); } + // POST /Tag/Create + // payload esperado (min): tenantId, tagName, typeId(_id TagType), slug, displayOrder, icon, parentTagId([]) 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', headers: this.headers(), 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(); } + // PUT /Tag/Update + // payload esperado (min): id(GUID) ó _id(24-hex) según backend, + tenantId, tagName, typeId, icon, etc. 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', headers: this.headers(), 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(); } - // PATCH ChangeStatus: body { id, status } + // PATCH /Tag/ChangeStatus + // Debe mandarse { id: <_id 24-hex>, status: 'Active'|'Inactive' } 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', headers: this.headers(), body: JSON.stringify({ id, status }), }); - if (!res.ok) throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`); - return res.json?.() ?? null; + if (!res.ok) { + 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) { - const res = await fetch(`${this.tagUrl}/Delete`, { + const res = await fetch(`${this.baseUrl}/Delete`, { method: 'DELETE', headers: this.headers(), body: JSON.stringify(payload), }); - if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`); - return res.json(); - } - - // ---- 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()}`); + if (!res.ok) { + throw new Error(`Delete error ${res.status}: ${await res.text()}`); + } return res.json(); } } - diff --git a/src/api/TagTypeApi.js b/src/api/TagTypeApi.js new file mode 100644 index 0000000..bd93cf2 --- /dev/null +++ b/src/api/TagTypeApi.js @@ -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(); + } +} diff --git a/src/private/fornitures/AddOrEditFurnitureVariantForm.jsx b/src/private/fornitures/AddOrEditFurnitureVariantForm.jsx index e1b5cb1..dea202a 100644 --- a/src/private/fornitures/AddOrEditFurnitureVariantForm.jsx +++ b/src/private/fornitures/AddOrEditFurnitureVariantForm.jsx @@ -1,20 +1,49 @@ // src/private/furniture/AddOrEditFurnitureVariantForm.jsx 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 CategoriesApi from '../../api/CategoriesApi'; +import TagTypeApi from '../../api/TagTypeApi'; import { useAuth } from '../../context/AuthContext'; 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 }) { const { user } = useAuth(); 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({ _Id: '', id: '', - modelId: '', + modelId: DEFAULT_MODEL_ID, name: '', color: '', line: '', @@ -27,180 +56,290 @@ export default function AddOrEditFurnitureVariantForm({ initialData, onAdd, onCa status: 'Active', }); - useEffect(() => { - if (initialData) { - setForm({ - _Id: initialData._id || initialData._Id || '', - 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', - }); + const setVal = (path, value) => { + if (path.startsWith('attributes.')) { + const k = path.split('.')[1]; + setForm(prev => ({ ...prev, attributes: { ...prev.attributes, [k]: value } })); } else { - setForm({ - _Id: '', - id: '', - modelId: DEFAULT_MODEL_ID, - name: '', - color: '', - line: '', - stock: 0, - price: 0, - currency: 'USD', - categoryId: '', - providerId: '', - attributes: { material: '', legs: '', origin: '' }, - status: 'Active', - }); + setForm(prev => ({ ...prev, [path]: value })); } + }; + + 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]); - const setVal = (name, value) => setForm((p) => ({ ...p, [name]: value })); - const setAttr = (name, value) => setForm((p) => ({ ...p, attributes: { ...p.attributes, [name]: value } })); + // Si viene GUID/_id/slug => convertir a tagName + 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 () => { try { - if (form._Id) { - // UPDATE - const payload = { - Id: form.id || form._Id, // backend requires Id - _Id: form._Id, - 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.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?.(); + const payload = { + ...form, + stock: Number(form.stock ?? 0), + price: Number(form.price ?? 0), + categoryId: form.categoryId || null, // enviamos tagName + providerId: form.providerId || null, // enviamos tagName + attributes: { + material: form.attributes.material || '', + legs: form.attributes.legs || '', + origin: form.attributes.origin || '', + } + }; + + const isUpdate = Boolean(form.id || form._Id); + const saved = isUpdate + ? await variantApi.updateVariant(payload) + : await variantApi.createVariant(payload); + + onAdd?.(saved); } catch (err) { - console.error('Submit variant failed:', err); + console.error(err); + alert(err?.message || 'Error saving variant'); } }; return ( - + - setVal('modelId', e.target.value)} - disabled={!initialData} - helperText={!initialData ? 'Preset for new variant' : ''} - /> + setVal('modelId', e.target.value)} /> - {form.id && ( - - - - )} - {form._Id && ( - - - - )} - setVal('name', e.target.value)} /> + setVal('name', e.target.value)} /> - - setVal('color', e.target.value)} /> - - - setVal('line', e.target.value)} /> - - - setVal('stock', e.target.value)} /> - - - setVal('price', e.target.value)} /> + {/* Clasificación */} + + setVal('categoryId', e.target.value)} + helperText="Se envía el tagName por ahora" + > + {loadingTags && Cargando…} + {!loadingTags && options.categories.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + - - setVal('currency', e.target.value)} /> - - - setVal('categoryId', e.target.value)} /> - - - setVal('providerId', e.target.value)} /> + + setVal('providerId', e.target.value)} + > + {loadingTags && Cargando…} + {!loadingTags && options.providers.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + + {/* Específicos de variante */} - setAttr('material', e.target.value)} /> - - - setAttr('legs', e.target.value)} /> - - - setAttr('origin', e.target.value)} /> + setVal('color', e.target.value)} + > + {loadingTags && Cargando…} + {!loadingTags && options.colors.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + setVal('status', e.target.value)} + label="Line" + fullWidth + value={form.line} + onChange={(e) => setVal('line', e.target.value)} > + {loadingTags && Cargando…} + {!loadingTags && options.lines.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + + + + + setVal('currency', e.target.value)} + > + {loadingTags && Cargando…} + {!loadingTags && options.currencies.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + + + + {/* Atributos como catálogos */} + + setVal('attributes.material', e.target.value)} + > + {loadingTags && Cargando…} + {!loadingTags && options.materials.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + + + + + setVal('attributes.legs', e.target.value)} + > + {loadingTags && Cargando…} + {!loadingTags && options.legs.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + + + + + setVal('attributes.origin', e.target.value)} + > + {loadingTags && Cargando…} + {!loadingTags && options.origins.filter(t => t.status !== 'Inactive').map(tag => ( + {tag.tagName} + ))} + + + + {/* Números */} + + setVal('stock', e.target.value)} /> + + + setVal('price', e.target.value)} /> + + + {/* Status */} + + setVal('status', e.target.value)}> Active Inactive @@ -213,4 +352,4 @@ export default function AddOrEditFurnitureVariantForm({ initialData, onAdd, onCa ); -} \ No newline at end of file +} diff --git a/src/private/fornitures/FurnitureVariantManagement.jsx b/src/private/fornitures/FurnitureVariantManagement.jsx index b5789a4..a166816 100644 --- a/src/private/fornitures/FurnitureVariantManagement.jsx +++ b/src/private/fornitures/FurnitureVariantManagement.jsx @@ -1,277 +1,285 @@ +// src/private/furniture/FurnitureVariantManagement.jsx 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 { Typography, Button, Dialog, DialogTitle, DialogContent, - IconButton, Box, ToggleButton, ToggleButtonGroup + IconButton, Box, FormControlLabel, Switch, Tooltip } from '@mui/material'; import EditRoundedIcon from '@mui/icons-material/EditRounded'; import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; +import AddRoundedIcon from '@mui/icons-material/AddRounded'; import AddOrEditFurnitureVariantForm from './AddOrEditFurnitureVariantForm'; import FurnitureVariantApi from '../../api/furnitureVariantApi'; +import CategoriesApi from '../../api/CategoriesApi'; +import TagTypeApi from '../../api/TagTypeApi'; import { useAuth } from '../../context/AuthContext'; import useApiToast from '../../hooks/useApiToast'; -const columnsBase = [ - { field: 'modelId', headerName: 'Model Id', width: 260 }, - { field: 'name', headerName: 'Name', width: 220 }, - { field: 'color', headerName: 'Color', width: 160 }, - { field: 'line', headerName: 'Line', width: 160 }, - { field: 'stock', headerName: 'Stock', width: 100, type: 'number' }, - { field: 'price', headerName: 'Price', width: 120, type: 'number', - valueFormatter: (p) => p?.value != null ? Number(p.value).toFixed(2) : '—' - }, - { field: 'currency', headerName: 'Currency', width: 120 }, - { field: 'categoryId', headerName: 'Category Id', width: 280 }, - { field: 'providerId', headerName: 'Provider Id', width: 280 }, - { - field: 'attributes.material', - headerName: 'Material', - width: 160, - valueGetter: (p) => p?.row?.attributes?.material ?? '—' - }, - { - 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 ?? '—' }, -]; +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; +}; + +const TYPE_NAMES = { + category: 'Furniture category', + provider: 'Provider', + color: 'Color', + line: 'Line', + currency: 'Currency', + material: 'Material', + legs: 'Legs', + origin: 'Origin', +}; export default function FurnitureVariantManagement() { const { user } = useAuth(); 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 [statusFilter, setStatusFilter] = useState('Active'); // <- por defecto Active + const [rawRows, setRawRows] = useState([]); const [open, setOpen] = useState(false); - const [editingData, setEditingData] = useState(null); - const [confirmOpen, setConfirmOpen] = useState(false); - const [rowToDelete, setRowToDelete] = useState(null); - const { handleError } = useApiToast(); - const hasLoaded = useRef(false); + const [editRow, setEditRow] = useState(null); + const [showInactive, setShowInactive] = useState(false); + const [loading, setLoading] = useState(true); - useEffect(() => { - apiRef.current = new FurnitureVariantApi(token); - }, [token]); + // Tags + const [loadingTags, setLoadingTags] = useState(true); + 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(() => { - if (!hasLoaded.current) { - loadData(); - hasLoaded.current = true; - } - }, []); - - const loadData = async () => { - try { - const data = await apiRef.current.getAllVariants(); - setRows(Array.isArray(data) ? data : []); - } catch (err) { - 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', + const buildLabelResolver = (typeName) => { + const list = byType[typeName] || []; + return (value) => { + 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 || + t._id === value || + t._id?.$oid === value || + t.slug === value + ); + return found?.tagName || String(value); }; - setEditingData(normalized); - setOpen(true); }; - const handleDeleteClick = (row) => { - setRowToDelete(row); - setConfirmOpen(true); - }; + const labelCategory = useMemo(() => buildLabelResolver(TYPE_NAMES.category), [byType]); + const labelProvider = useMemo(() => buildLabelResolver(TYPE_NAMES.provider), [byType]); + 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 { - if (!apiRef.current || !(rowToDelete?._id || rowToDelete?._Id)) throw new Error('Missing API or id'); - // Soft-delete vía Update (status=Inactive) - const payload = { - id: rowToDelete.id || rowToDelete.Id || '', - _Id: rowToDelete._id || rowToDelete._Id, - modelId: rowToDelete.modelId, - name: rowToDelete.name, - color: rowToDelete.color, - line: rowToDelete.line, - stock: rowToDelete.stock, - price: rowToDelete.price, - currency: rowToDelete.currency, - categoryId: rowToDelete.categoryId, - providerId: rowToDelete.providerId, + setLoading(true); + const data = await api.getAllVariants(); + const normalized = (data || []).map((r, idx) => ({ + id: r.id || r._id || `row-${idx}`, + _Id: r._id || r._Id || '', + modelId: r.modelId ?? '', + name: r.name ?? '', + categoryId: r.categoryId ?? '', + providerId: r.providerId ?? '', + color: r.color ?? '', + line: r.line ?? '', + stock: Number(r.stock ?? 0), + price: parsePrice(r.price), + currency: r.currency ?? 'USD', attributes: { - material: rowToDelete?.attributes?.material ?? '', - legs: rowToDelete?.attributes?.legs ?? '', - origin: rowToDelete?.attributes?.origin ?? '', + material: r?.attributes?.material ?? '', + legs: r?.attributes?.legs ?? '', + origin: r?.attributes?.origin ?? '', }, - status: 'Inactive', - }; - await apiRef.current.updateVariant(payload); - await loadData(); - } catch (e) { - console.error('Delete failed:', e); + status: r.status ?? 'Active', + createdAt: r.createdAt ?? null, + createdBy: r.createdBy ?? null, + })); + setRawRows(normalized); + setRows(normalized.filter(r => showInactive ? true : r.status !== 'Inactive')); + } catch (err) { + console.error(err); + toast.error(err?.message || 'Error loading variants'); } finally { - setConfirmOpen(false); - setRowToDelete(null); + setLoading(false); } }; - // --- FILTRO DE ESTADO --- - const filteredRows = useMemo(() => { - if (statusFilter === 'All') return rows; - const want = String(statusFilter).toLowerCase(); - return rows.filter((r) => String(r?.status ?? 'Active').toLowerCase() === want); - }, [rows, statusFilter]); + useEffect(() => { load(); /* eslint-disable-next-line */ }, []); + useEffect(() => { + setRows(rawRows.filter(r => showInactive ? true : r.status !== 'Inactive')); + }, [showInactive, rawRows]); 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', headerName: '', - width: 130, - renderCell: (params) => ( - - handleEditClick(params)} - > - - - handleDeleteClick(params?.row)} - > - - + sortable: false, + width: 110, + renderCell: (p) => ( + + + { setEditRow(p.row); setOpen(true); }}> + + + + + { + try { + const updated = { ...p.row, status: p.row.status === 'Active' ? 'Inactive' : 'Active' }; + await api.updateVariant(updated); + setRawRows(prev => prev.map(r => r.id === p.row.id ? updated : r)); + } catch (err) { + console.error(err); + toast.error(err?.message || 'Error updating status'); + } + }} + > + + + ) }, - ...columnsBase, ]; return ( - - { setOpen(false); setEditingData(null); }} maxWidth="md" fullWidth> - {editingData ? 'Edit Product' : 'Add Product'} - - { setOpen(false); setEditingData(null); }} - onAdd={async () => { - await loadData(); - setOpen(false); - setEditingData(null); - }} - /> - - - - setConfirmOpen(false)}> - Confirm Delete - - - Are you sure you want to delete {rowToDelete?.name}? - - - - - - - - - {/* Toolbar de filtro */} - - v && setStatusFilter(v)} - size="small" - > - Active - All - Inactive - + + + Furniture Variants + + setShowInactive(v)} />} label="Show Inactive" /> + + - + ({ top: 4, bottom: 4 })} - getRowId={(row) => row._id || row.id || row.modelId} - autoHeight - disableColumnMenu + disableRowSelectionOnClick + loading={loading || loadingTags} + pageSizeOptions={[10, 25, 50]} + initialState={{ + pagination: { paginationModel: { pageSize: 10 } }, + columns: { columnVisibilityModel: { id: false, _Id: false } }, + }} getRowHeight={() => 'auto'} sx={{ '& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center' }, '& .MuiDataGrid-columnHeader': { display: 'flex', alignItems: 'center' }, }} /> - - - + + setOpen(false)} maxWidth="md" fullWidth> + {editRow ? 'Edit Variant' : 'Add Variant'} + + { + 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)} + /> + + ); }