feat: added dropdowns, categories and some filters
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
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 (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Model Id"
|
||||
value={form.modelId}
|
||||
onChange={(e) => setVal('modelId', e.target.value)}
|
||||
disabled={!initialData}
|
||||
helperText={!initialData ? 'Preset for new variant' : ''}
|
||||
/>
|
||||
<TextField label="Model Id" fullWidth value={form.modelId} onChange={(e) => setVal('modelId', e.target.value)} />
|
||||
</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}>
|
||||
<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 item xs={12} md={4}>
|
||||
<TextField fullWidth label="Color" value={form.color} onChange={(e) => setVal('color', e.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField fullWidth label="Line" value={form.line} onChange={(e) => setVal('line', e.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={2}>
|
||||
<TextField fullWidth type="number" label="Stock" value={form.stock} onChange={(e) => setVal('stock', e.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={2}>
|
||||
<TextField fullWidth type="number" label="Price" value={form.price} onChange={(e) => setVal('price', e.target.value)} />
|
||||
{/* Clasificación */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
select
|
||||
label="Category"
|
||||
fullWidth
|
||||
value={form.categoryId}
|
||||
onChange={(e) => setVal('categoryId', e.target.value)}
|
||||
helperText="Se envía el tagName por ahora"
|
||||
>
|
||||
{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 item xs={12} md={3}>
|
||||
<TextField fullWidth label="Currency" value={form.currency} onChange={(e) => setVal('currency', e.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField fullWidth label="Category Id" value={form.categoryId} onChange={(e) => setVal('categoryId', e.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={5}>
|
||||
<TextField fullWidth label="Provider Id" value={form.providerId} onChange={(e) => setVal('providerId', e.target.value)} />
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
select
|
||||
label="Provider"
|
||||
fullWidth
|
||||
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>
|
||||
|
||||
{/* Específicos de variante */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField fullWidth label="Material" value={form.attributes.material} onChange={(e) => setAttr('material', e.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField fullWidth label="Legs" value={form.attributes.legs} onChange={(e) => setAttr('legs', e.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField fullWidth label="Origin" value={form.attributes.origin} onChange={(e) => setAttr('origin', e.target.value)} />
|
||||
<TextField
|
||||
select
|
||||
label="Color"
|
||||
fullWidth
|
||||
value={form.color}
|
||||
onChange={(e) => setVal('color', 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 item xs={12} md={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label="Status"
|
||||
value={form.status}
|
||||
onChange={(e) => setVal('status', e.target.value)}
|
||||
label="Line"
|
||||
fullWidth
|
||||
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="Inactive">Inactive</MenuItem>
|
||||
</TextField>
|
||||
|
||||
@@ -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) => (
|
||||
<Box display="flex" alignItems="center" justifyContent="flex-start" height="100%" gap={1}>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: '#DFCCBC',
|
||||
color: '#26201A',
|
||||
'&:hover': { backgroundColor: '#C2B2A4' },
|
||||
borderRadius: 2,
|
||||
p: 1,
|
||||
}}
|
||||
onClick={() => handleEditClick(params)}
|
||||
>
|
||||
<EditRoundedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: '#FBE9E7',
|
||||
color: '#C62828',
|
||||
'&:hover': { backgroundColor: '#EF9A9A' },
|
||||
borderRadius: 2,
|
||||
p: 1,
|
||||
}}
|
||||
onClick={() => handleDeleteClick(params?.row)}
|
||||
>
|
||||
<DeleteRoundedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
sortable: false,
|
||||
width: 110,
|
||||
renderCell: (p) => (
|
||||
<Box display="flex" gap={1}>
|
||||
<Tooltip title="Edit">
|
||||
<IconButton size="small" onClick={() => { setEditRow(p.row); setOpen(true); }}>
|
||||
<EditRoundedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={p.row.status === 'Active' ? 'Deactivate' : 'Activate'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
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');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteRoundedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
...columnsBase,
|
||||
];
|
||||
|
||||
return (
|
||||
<SectionContainer sx={{ width: '100%' }}>
|
||||
<Dialog open={open} onClose={() => { setOpen(false); setEditingData(null); }} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingData ? 'Edit Product' : 'Add Product'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<AddOrEditFurnitureVariantForm
|
||||
initialData={editingData}
|
||||
onCancel={() => { setOpen(false); setEditingData(null); }}
|
||||
onAdd={async () => {
|
||||
await loadData();
|
||||
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>
|
||||
<SectionContainer>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h6">Furniture Variants</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<FormControlLabel control={<Switch checked={showInactive} onChange={(_, v) => setShowInactive(v)} />} label="Show Inactive" />
|
||||
<Button variant="contained" className="button-gold" startIcon={<AddRoundedIcon />} onClick={() => { setEditRow(null); setOpen(true); }}>
|
||||
Add Variant
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: '100%', overflowX: 'auto' }}>
|
||||
<Box sx={{ width: '100%', height: 560 }}>
|
||||
<DataGrid
|
||||
rows={filteredRows}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
pageSize={5}
|
||||
rowsPerPageOptions={[5]}
|
||||
getRowSpacing={() => ({ 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' },
|
||||
}}
|
||||
/>
|
||||
<Box display="flex" justifyContent="flex-end" mt={2}>
|
||||
<Button variant="contained" className="button-gold" onClick={() => setOpen(true)}>
|
||||
Add Variant
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user