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 && }
+ {!loadingTags && options.categories.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
-
- setVal('currency', e.target.value)} />
-
-
- setVal('categoryId', e.target.value)} />
-
-
- setVal('providerId', e.target.value)} />
+
+ setVal('providerId', e.target.value)}
+ >
+ {loadingTags && }
+ {!loadingTags && options.providers.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
+ {/* 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 && }
+ {!loadingTags && options.colors.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
setVal('status', e.target.value)}
+ label="Line"
+ fullWidth
+ value={form.line}
+ onChange={(e) => setVal('line', e.target.value)}
>
+ {loadingTags && }
+ {!loadingTags && options.lines.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
+
+
+
+ setVal('currency', e.target.value)}
+ >
+ {loadingTags && }
+ {!loadingTags && options.currencies.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
+
+
+ {/* Atributos como catálogos */}
+
+ setVal('attributes.material', e.target.value)}
+ >
+ {loadingTags && }
+ {!loadingTags && options.materials.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
+
+
+
+ setVal('attributes.legs', e.target.value)}
+ >
+ {loadingTags && }
+ {!loadingTags && options.legs.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
+
+
+
+ setVal('attributes.origin', e.target.value)}
+ >
+ {loadingTags && }
+ {!loadingTags && options.origins.filter(t => t.status !== 'Inactive').map(tag => (
+
+ ))}
+
+
+
+ {/* Números */}
+
+ setVal('stock', e.target.value)} />
+
+
+ setVal('price', e.target.value)} />
+
+
+ {/* Status */}
+
+ setVal('status', e.target.value)}>
@@ -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 (
-
-
-
-
-
- {/* Toolbar de filtro */}
-
- v && setStatusFilter(v)}
- size="small"
- >
- Active
- All
- Inactive
-
+
+
+ Furniture Variants
+
+ setShowInactive(v)} />} label="Show Inactive" />
+ } onClick={() => { setEditRow(null); setOpen(true); }}>
+ Add Variant
+
+
-
+
({ 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' },
}}
/>
-
-
-
+
+
);
}