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