Compare commits
16 Commits
aa62b06c23
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7adaf1b18 | ||
|
|
b3209a4019 | ||
|
|
efdb48919f | ||
|
|
6b8d5acc0d | ||
|
|
01a19b9144 | ||
|
|
74d6a8b269 | ||
|
|
c33de6ada5 | ||
|
|
15107a48bd | ||
|
|
73699009fc | ||
|
|
55dc96085d | ||
|
|
9cdb76273d | ||
|
|
49dead566c | ||
|
|
2fa6b95012 | ||
|
|
f5acde78de | ||
|
|
d9bfaba977 | ||
|
|
f42d08c091 |
@@ -6,8 +6,8 @@ import MenuDrawerPrivate, { OPEN_WIDTH, MINI_WIDTH } from './components/MenuDraw
|
|||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import Dashboard from './private/dashboard/Dashboard';
|
import Dashboard from './private/dashboard/Dashboard';
|
||||||
import UserManagement from './private/users/UserManagement';
|
import UserManagement from './private/users/UserManagement';
|
||||||
import ProductCollections from './private/catalogs/ProductCollections';
|
import ProductCollections from './private/catalogs/products/ProductCollections';
|
||||||
import Categories from './private/categories/Categories';
|
import Categories from './private/catalogs/categories/Categories';
|
||||||
import LoginPage from './private/LoginPage';
|
import LoginPage from './private/LoginPage';
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from './context/AuthContext';
|
import { useAuth } from './context/AuthContext';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default class FurnitureVariantApi {
|
export default class ProductsApi {
|
||||||
constructor(token) {
|
constructor(token) {
|
||||||
this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/FurnitureVariant';
|
this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/FurnitureVariant';
|
||||||
this.token = token;
|
this.token = token;
|
||||||
@@ -52,4 +52,15 @@ export default class FurnitureVariantApi {
|
|||||||
if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`);
|
if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeStatusVariant(payload) {
|
||||||
|
// If your API is change status, reuse updateVariant.
|
||||||
|
const res = await fetch(`${this.baseUrl}/ChangeStatus`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: this.headers(),
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Drawer, List, ListItemButton, ListItemIcon, ListItemText,
|
Drawer, List, ListItemButton, ListItemIcon, ListItemText,
|
||||||
Collapse, IconButton, Tooltip, Box, useMediaQuery, InputBase
|
Collapse, IconButton, Tooltip, Box, useMediaQuery, InputBase
|
||||||
|
|||||||
@@ -1,355 +0,0 @@
|
|||||||
// src/private/furniture/AddOrEditFurnitureVariantForm.jsx
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
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 AddOrEditProductCollectionForm({ initialData, onAdd, onCancel }) {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
|
||||||
|
|
||||||
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: DEFAULT_MODEL_ID,
|
|
||||||
name: '',
|
|
||||||
color: '',
|
|
||||||
line: '',
|
|
||||||
stock: 0,
|
|
||||||
price: 0,
|
|
||||||
currency: 'USD',
|
|
||||||
categoryId: '',
|
|
||||||
providerId: '',
|
|
||||||
attributes: { material: '', legs: '', origin: '' },
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
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(err);
|
|
||||||
alert(err?.message || 'Error saving variant');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<TextField label="Model Id" fullWidth value={form.modelId} onChange={(e) => setVal('modelId', e.target.value)} />
|
|
||||||
</Grid>
|
|
||||||
<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}>
|
|
||||||
<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={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="Inactive">Inactive</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Box display="flex" justifyContent="flex-end" mt={3} gap={1}>
|
|
||||||
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
|
|
||||||
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
import SectionContainer from '../../components/SectionContainer';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { DataGrid } from '@mui/x-data-grid';
|
|
||||||
import {
|
|
||||||
Typography, Button, Dialog, DialogTitle, DialogContent,
|
|
||||||
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 AddOrEditProductCollectionForm from './AddOrEditProductCollectionForm';
|
|
||||||
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 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 ProductCollections() {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
|
||||||
|
|
||||||
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 [rawRows, setRawRows] = useState([]);
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [editRow, setEditRow] = useState(null);
|
|
||||||
const [showInactive, setShowInactive] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// 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]: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// 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 || '',
|
|
||||||
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: r?.attributes?.material ?? '',
|
|
||||||
legs: r?.attributes?.legs ?? '',
|
|
||||||
origin: r?.attributes?.origin ?? '',
|
|
||||||
},
|
|
||||||
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 {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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: '',
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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%', height: 560 }}>
|
|
||||||
<DataGrid
|
|
||||||
rows={rows}
|
|
||||||
columns={columns}
|
|
||||||
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>
|
|
||||||
|
|
||||||
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="md" fullWidth>
|
|
||||||
<DialogTitle>{editRow ? 'Edit Product Collection' : 'Add Product Collection'}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<AddOrEditProductCollectionForm
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material';
|
import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../../context/AuthContext';
|
||||||
import CategoriesApi from '../../api/CategoriesApi';
|
import CategoriesApi from '../../../api/CategoriesApi';
|
||||||
|
import TagTypeApi from '../../../api/TagTypeApi';
|
||||||
import { jwtDecode } from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
function slugify(s) {
|
function slugify(s) {
|
||||||
@@ -25,25 +26,43 @@ function extractTenantId(token) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel, materials: materialsProp = [], initialMaterialNames = [] }) {
|
function formatDateSafe(value) {
|
||||||
|
if (!value) return '—';
|
||||||
|
// Accept Date instance, ISO string, or numeric timestamp
|
||||||
|
const d = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return '—';
|
||||||
|
// Treat placeholder/default dates as empty
|
||||||
|
const year = d.getUTCFullYear();
|
||||||
|
if (year <= 1971) return '—';
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel, materials: materialsProp = [], initialMaterialNames = [], viewOnly = false }) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
||||||
const api = useMemo(() => new CategoriesApi(token), [token]);
|
const api = useMemo(() => new CategoriesApi(token), [token]);
|
||||||
|
const tagTypeApi = useMemo(() => new TagTypeApi(token), [token]);
|
||||||
|
|
||||||
const [types, setTypes] = useState([]);
|
const [types, setTypes] = useState([]);
|
||||||
const [allTags, setAllTags] = useState([]);
|
const [allTags, setAllTags] = useState([]);
|
||||||
|
|
||||||
const tagLabelById = useMemo(() => {
|
const tagLabelById = useMemo(() => {
|
||||||
const map = {};
|
const map = {};
|
||||||
for (const t of allTags) {
|
for (const t of allTags) {
|
||||||
const key = t._id || t.id;
|
const key = t._id;
|
||||||
map[key] = t.tagName || t.name || key;
|
map[key] = t.tagName || t.name || key;
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [allTags]);
|
}, [allTags]);
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
_Id: '',
|
_id: '',
|
||||||
id: '',
|
id: '',
|
||||||
tenantId: '',
|
tenantId: '',
|
||||||
tagName: '',
|
tagName: '',
|
||||||
@@ -59,27 +78,16 @@ const tagLabelById = useMemo(() => {
|
|||||||
updatedBy: null,
|
updatedBy: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// cargar tipos y tags para selects
|
// cargar tipos (Tag Types) y tags para selects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// Always try to load all tags (for materials, lookups, etc.)
|
// Load all tag types from TagTypeApi
|
||||||
const tags = typeof api.getAll === 'function' ? await api.getAll() : [];
|
const typesResp = await tagTypeApi.getAll();
|
||||||
|
|
||||||
// Try multiple method names for types; if none exist, derive from tags
|
|
||||||
let typesResp = [];
|
|
||||||
if (typeof api.getAllTypes === 'function') {
|
|
||||||
typesResp = await api.getAllTypes();
|
|
||||||
} else if (typeof api.getTypes === 'function') {
|
|
||||||
typesResp = await api.getTypes();
|
|
||||||
} else if (Array.isArray(tags)) {
|
|
||||||
// Derive a minimal "types" list from existing tag.typeId values
|
|
||||||
const uniqueTypeIds = [...new Set(tags.map(t => t?.typeId).filter(Boolean))];
|
|
||||||
typesResp = uniqueTypeIds.map(id => ({ id, typeName: id, level: null }));
|
|
||||||
console.warn('CategoriesApi has no getAllTypes/getTypes; derived types from tag.typeId.');
|
|
||||||
}
|
|
||||||
|
|
||||||
setTypes(Array.isArray(typesResp) ? typesResp : []);
|
setTypes(Array.isArray(typesResp) ? typesResp : []);
|
||||||
|
|
||||||
|
// Load all existing tags (used to resolve parentTagId -> labels for Material)
|
||||||
|
const tags = typeof api.getAll === 'function' ? await api.getAll() : [];
|
||||||
setAllTags(Array.isArray(tags) ? tags : []);
|
setAllTags(Array.isArray(tags) ? tags : []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load tag types or tags', e);
|
console.error('Failed to load tag types or tags', e);
|
||||||
@@ -87,7 +95,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
setAllTags([]);
|
setAllTags([]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [api]);
|
}, [tagTypeApi, api]);
|
||||||
|
|
||||||
// When editing: if we received material names from the grid, map them to IDs once allTags are loaded.
|
// When editing: if we received material names from the grid, map them to IDs once allTags are loaded.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -99,9 +107,9 @@ const tagLabelById = useMemo(() => {
|
|||||||
// Build a case-insensitive name -> id map
|
// Build a case-insensitive name -> id map
|
||||||
const nameToId = new Map(
|
const nameToId = new Map(
|
||||||
allTags.map(t => {
|
allTags.map(t => {
|
||||||
const id = t._id || t.id;
|
const _id = t._id;
|
||||||
const label = (t.tagName || t.name || '').toLowerCase();
|
const label = (t.tagName || t.name || '').toLowerCase();
|
||||||
return [label, id];
|
return [label, _id];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -118,11 +126,9 @@ const tagLabelById = useMemo(() => {
|
|||||||
// set inicial
|
// set inicial
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const _Id = initialData._id || initialData._Id || '';
|
|
||||||
const id = initialData.id || initialData.Id || _Id || '';
|
|
||||||
setForm({
|
setForm({
|
||||||
_Id,
|
_id: initialData._id,
|
||||||
id,
|
id: initialData.id,
|
||||||
tenantId: initialData.tenantId || extractTenantId(token) || '',
|
tenantId: initialData.tenantId || extractTenantId(token) || '',
|
||||||
tagName: initialData.tagName || initialData.name || '',
|
tagName: initialData.tagName || initialData.name || '',
|
||||||
typeId: initialData.typeId || '',
|
typeId: initialData.typeId || '',
|
||||||
@@ -138,7 +144,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setForm({
|
setForm({
|
||||||
_Id: '',
|
_id: '',
|
||||||
id: '',
|
id: '',
|
||||||
tenantId: extractTenantId(token) || '',
|
tenantId: extractTenantId(token) || '',
|
||||||
tagName: '',
|
tagName: '',
|
||||||
@@ -156,7 +162,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
}
|
}
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
const isEdit = Boolean(form._Id || form.id);
|
const isEdit = Boolean(form._id);
|
||||||
const isAdd = !isEdit;
|
const isAdd = !isEdit;
|
||||||
|
|
||||||
const setVal = (name, value) => setForm(p => ({ ...p, [name]: value }));
|
const setVal = (name, value) => setForm(p => ({ ...p, [name]: value }));
|
||||||
@@ -164,7 +170,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setVal(name, value);
|
setVal(name, value);
|
||||||
if (name === 'tagName' && !form._Id) {
|
if (name === 'tagName' && !form._id) {
|
||||||
setVal('slug', slugify(value));
|
setVal('slug', slugify(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -179,6 +185,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
if (!tenantId) throw new Error('TenantId not found in token');
|
if (!tenantId) throw new Error('TenantId not found in token');
|
||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
|
id: form.id?.trim() || undefined,
|
||||||
tagName: form.tagName.trim(),
|
tagName: form.tagName.trim(),
|
||||||
typeId: form.typeId,
|
typeId: form.typeId,
|
||||||
parentTagId: form.parentTagId,
|
parentTagId: form.parentTagId,
|
||||||
@@ -189,15 +196,16 @@ const tagLabelById = useMemo(() => {
|
|||||||
tenantId, // requerido por backend (400 si falta)
|
tenantId, // requerido por backend (400 si falta)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (form._Id) {
|
if (form._id) {
|
||||||
// UPDATE
|
const idForUpdate = Boolean(form._id) ? String(form._id) : null;
|
||||||
|
if (!idForUpdate) throw new Error('Missing _id for update');
|
||||||
const payload = {
|
const payload = {
|
||||||
id: form.id || form._Id, // backend acepta GUID; si no hay, mandamos _id
|
_id: idForUpdate,
|
||||||
...base,
|
...base,
|
||||||
};
|
};
|
||||||
|
console.log('[CategoryForm] SUBMIT (edit) with _id:', idForUpdate, 'payload:', payload);
|
||||||
await api.update(payload);
|
await api.update(payload);
|
||||||
} else {
|
} else {
|
||||||
// CREATE
|
|
||||||
await api.create(base);
|
await api.create(base);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,10 +221,9 @@ const tagLabelById = useMemo(() => {
|
|||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
try {
|
try {
|
||||||
// Try to use Mongo _Id (24-hex); if not present, fall back to GUID `id`.
|
const idToUse = form._id;
|
||||||
const hex = typeof form._Id === 'string' && /^[0-9a-f]{24}$/i.test(form._Id) ? form._Id : null;
|
if (!idToUse) throw new Error('Missing _id to delete');
|
||||||
const idToUse = hex || form.id;
|
console.debug('[CategoryForm] DELETE with _id:', idToUse);
|
||||||
if (!idToUse) throw new Error('Missing id to delete');
|
|
||||||
await api.changeStatus({ id: idToUse, status: 'Inactive' });
|
await api.changeStatus({ id: idToUse, status: 'Inactive' });
|
||||||
if (onAdd) {
|
if (onAdd) {
|
||||||
await onAdd();
|
await onAdd();
|
||||||
@@ -230,7 +237,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 2 }}>
|
<Paper sx={{ p: 2 }}>
|
||||||
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
{form._Id ? 'Edit Category' : 'Add Category'}
|
{form._id ? 'Edit Category' : 'Add Category'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{isAdd && (
|
{isAdd && (
|
||||||
@@ -241,6 +248,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
InputProps={{ readOnly: true }}
|
InputProps={{ readOnly: true }}
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -252,23 +260,29 @@ const tagLabelById = useMemo(() => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
required
|
required
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
name="typeId"
|
name="typeId"
|
||||||
label="Type"
|
label="Category"
|
||||||
value={form.typeId}
|
value={form.typeId}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
required
|
required
|
||||||
|
disabled={viewOnly}
|
||||||
>
|
>
|
||||||
{types.map(t => (
|
{types.map((t) => {
|
||||||
<MenuItem key={t.id || t._id} value={t.id || t._id}>
|
const value = t._id;
|
||||||
{t.typeName} ({t.level ?? '-'})
|
const label = t.typeName || value;
|
||||||
|
return (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -293,9 +307,10 @@ const tagLabelById = useMemo(() => {
|
|||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
|
disabled={viewOnly}
|
||||||
>
|
>
|
||||||
{allTags.map((t) => {
|
{allTags.map((t) => {
|
||||||
const value = t._id || t.id;
|
const value = t._id;
|
||||||
const label = t.tagName || t.name || value;
|
const label = t.tagName || t.name || value;
|
||||||
return (
|
return (
|
||||||
<MenuItem key={value} value={value}>
|
<MenuItem key={value} value={value}>
|
||||||
@@ -312,6 +327,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -322,6 +338,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -332,6 +349,7 @@ const tagLabelById = useMemo(() => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
required
|
required
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -342,27 +360,30 @@ const tagLabelById = useMemo(() => {
|
|||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
|
disabled={viewOnly}
|
||||||
>
|
>
|
||||||
<MenuItem value="Active">Active</MenuItem>
|
<MenuItem value="Active">Active</MenuItem>
|
||||||
<MenuItem value="Inactive">Inactive</MenuItem>
|
<MenuItem value="Inactive">Inactive</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
{form._Id || form.id ? (
|
{form._id ? (
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mt: 2 }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mt: 2 }}>
|
||||||
<TextField label="Created At" value={form.createdAt ? new Date(form.createdAt).toLocaleString() : '—'} InputProps={{ readOnly: true }} fullWidth />
|
<TextField label="Created At" value={formatDateSafe(form.createdAt)} InputProps={{ readOnly: true }} fullWidth />
|
||||||
<TextField label="Created By" value={form.createdBy ?? '—'} InputProps={{ readOnly: true }} fullWidth />
|
<TextField label="Created By" value={form.createdBy ?? '—'} InputProps={{ readOnly: true }} fullWidth />
|
||||||
<TextField label="Updated At" value={form.updatedAt ? new Date(form.updatedAt).toLocaleString() : '—'} InputProps={{ readOnly: true }} fullWidth />
|
<TextField label="Updated At" value={formatDateSafe(form.updatedAt)} InputProps={{ readOnly: true }} fullWidth />
|
||||||
<TextField label="Updated By" value={form.updatedBy ?? '—'} InputProps={{ readOnly: true }} fullWidth />
|
<TextField label="Updated By" value={form.updatedBy ?? '—'} InputProps={{ readOnly: true }} fullWidth />
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Box display="flex" justifyContent="space-between" gap={1} mt={3}>
|
<Box display="flex" justifyContent="space-between" gap={1} mt={3}>
|
||||||
{(form._Id || form.id) ? (
|
{form._id && !viewOnly ? (
|
||||||
<Button color="error" onClick={handleDelete}>Delete</Button>
|
<Button color="error" onClick={handleDelete}>Delete</Button>
|
||||||
) : <span />}
|
) : <span />}
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
|
<Button onClick={onCancel} className="button-transparent">{viewOnly ? 'Close' : 'Cancel'}</Button>
|
||||||
|
{!viewOnly && (
|
||||||
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -6,9 +6,10 @@ import {
|
|||||||
import { DataGrid } from '@mui/x-data-grid';
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
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 VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded';
|
||||||
import AddOrEditCategoryForm from './AddOrEditCategoryForm';
|
import AddOrEditCategoryForm from './AddOrEditCategoryForm';
|
||||||
import CategoriesApi from '../../api/CategoriesApi';
|
import CategoriesApi from '../../../api/CategoriesApi';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../../context/AuthContext';
|
||||||
|
|
||||||
export default function Categories() {
|
export default function Categories() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -22,6 +23,7 @@ export default function Categories() {
|
|||||||
const [editingCategory, setEditingCategory] = useState(null);
|
const [editingCategory, setEditingCategory] = useState(null);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
const [rowToDelete, setRowToDelete] = useState(null);
|
const [rowToDelete, setRowToDelete] = useState(null);
|
||||||
|
const [viewOnly, setViewOnly] = useState(false);
|
||||||
const hasLoaded = useRef(false);
|
const hasLoaded = useRef(false);
|
||||||
|
|
||||||
const pageSize = 100; // Número de filas por página
|
const pageSize = 100; // Número de filas por página
|
||||||
@@ -40,21 +42,25 @@ export default function Categories() {
|
|||||||
|
|
||||||
setAllTags(list);
|
setAllTags(list);
|
||||||
|
|
||||||
// Build a map of parentId -> array of child tagNames
|
// Build a map of tagId -> tagName to resolve parent names
|
||||||
const parentToChildren = {};
|
const idToName = {};
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
const parents = Array.isArray(item?.parentTagId) ? item.parentTagId : [];
|
const key = item?._id || item?.id;
|
||||||
for (const pid of parents) {
|
if (key) idToName[key] = item?.tagName || item?.name || '';
|
||||||
if (!parentToChildren[pid]) parentToChildren[pid] = [];
|
|
||||||
if (item?.tagName) parentToChildren[pid].push(item.tagName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich each row with `material` (children whose parentTagId includes this _id)
|
// Enrich each row with `materialNames`: names of the parents referenced by parentTagId
|
||||||
const enriched = list.map((r) => ({
|
const enriched = list.map((r) => {
|
||||||
|
const parents = Array.isArray(r?.parentTagId) ? r.parentTagId : [];
|
||||||
|
const materialNames = parents
|
||||||
|
.map((pid) => idToName[pid])
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
...r,
|
...r,
|
||||||
material: Array.isArray(parentToChildren[r?._id]) ? parentToChildren[r._id].join(', ') : '',
|
materialNames, // array of strings
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
setRows(enriched);
|
setRows(enriched);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -64,16 +70,18 @@ export default function Categories() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
|
setViewOnly(false);
|
||||||
setEditingCategory(null);
|
setEditingCategory(null);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditClick = (params) => {
|
const handleEditClick = (params) => {
|
||||||
|
setViewOnly(false);
|
||||||
const r = params?.row;
|
const r = params?.row;
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
setEditingCategory({
|
setEditingCategory({
|
||||||
_Id: r._id || r._Id || '',
|
_id: String(r._id || ''),
|
||||||
id: r.id || r.Id || '',
|
id: String(r.id || ''),
|
||||||
tagName: r.tagName || r.name || '',
|
tagName: r.tagName || r.name || '',
|
||||||
typeId: r.typeId || '',
|
typeId: r.typeId || '',
|
||||||
parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [],
|
parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [],
|
||||||
@@ -81,9 +89,15 @@ export default function Categories() {
|
|||||||
displayOrder: Number(r.displayOrder ?? 0),
|
displayOrder: Number(r.displayOrder ?? 0),
|
||||||
icon: r.icon || '',
|
icon: r.icon || '',
|
||||||
status: r.status ?? 'Active',
|
status: r.status ?? 'Active',
|
||||||
materialNames: typeof r.material === 'string'
|
materialNames: Array.isArray(r.materialNames)
|
||||||
|
? r.materialNames
|
||||||
|
: (typeof r.material === 'string'
|
||||||
? r.material.split(',').map(s => s.trim()).filter(Boolean)
|
? r.material.split(',').map(s => s.trim()).filter(Boolean)
|
||||||
: Array.isArray(r.material) ? r.material : [],
|
: []),
|
||||||
|
createdAt: r.createdAt ?? null,
|
||||||
|
createdBy: r.createdBy ?? null,
|
||||||
|
updatedAt: r.updatedAt ?? null,
|
||||||
|
updatedBy: r.updatedBy ?? null,
|
||||||
});
|
});
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
@@ -95,7 +109,7 @@ export default function Categories() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pickHexId = (r) =>
|
const pickHexId = (r) =>
|
||||||
[r?._id, r?._Id, r?.id, r?.Id]
|
[r?._id, r?.id]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.find((x) => typeof x === 'string' && /^[0-9a-f]{24}$/i.test(x)) || null;
|
.find((x) => typeof x === 'string' && /^[0-9a-f]{24}$/i.test(x)) || null;
|
||||||
|
|
||||||
@@ -135,7 +149,7 @@ export default function Categories() {
|
|||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
headerName: '',
|
headerName: '',
|
||||||
width: 130,
|
width: 150,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
disableExport: true,
|
disableExport: true,
|
||||||
@@ -154,6 +168,44 @@ export default function Categories() {
|
|||||||
>
|
>
|
||||||
<EditRoundedIcon fontSize="small" />
|
<EditRoundedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#E3F2FD',
|
||||||
|
color: '#1565C0',
|
||||||
|
'&:hover': { backgroundColor: '#BBDEFB' },
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
const r = params?.row;
|
||||||
|
if (!r) return;
|
||||||
|
setEditingCategory({
|
||||||
|
_id: String(r._id || ''),
|
||||||
|
id: String(r.id || ''),
|
||||||
|
tagName: r.tagName || r.name || '',
|
||||||
|
typeId: r.typeId || '',
|
||||||
|
parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [],
|
||||||
|
slug: r.slug || '',
|
||||||
|
displayOrder: Number(r.displayOrder ?? 0),
|
||||||
|
icon: r.icon || '',
|
||||||
|
status: r.status ?? 'Active',
|
||||||
|
materialNames: Array.isArray(r.materialNames)
|
||||||
|
? r.materialNames
|
||||||
|
: (typeof r.material === 'string'
|
||||||
|
? r.material.split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
: []),
|
||||||
|
createdAt: r.createdAt ?? null,
|
||||||
|
createdBy: r.createdBy ?? null,
|
||||||
|
updatedAt: r.updatedAt ?? null,
|
||||||
|
updatedBy: r.updatedBy ?? null,
|
||||||
|
});
|
||||||
|
setViewOnly(true);
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VisibilityRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -173,7 +225,40 @@ export default function Categories() {
|
|||||||
{ field: 'tagName', headerName: 'Name', flex: 1.2, minWidth: 180 },
|
{ field: 'tagName', headerName: 'Name', flex: 1.2, minWidth: 180 },
|
||||||
{ field: 'slug', headerName: 'Slug', flex: 1.0, minWidth: 160 },
|
{ field: 'slug', headerName: 'Slug', flex: 1.0, minWidth: 160 },
|
||||||
{ field: 'icon', headerName: 'Icon', flex: 0.7, minWidth: 250 },
|
{ field: 'icon', headerName: 'Icon', flex: 0.7, minWidth: 250 },
|
||||||
|
|
||||||
|
/*
|
||||||
{ field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 },
|
{ field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 },
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
field: 'materialNames',
|
||||||
|
headerName: 'Material',
|
||||||
|
flex: 1.2,
|
||||||
|
minWidth: 220,
|
||||||
|
renderCell: (params) => {
|
||||||
|
const vals = Array.isArray(params?.row?.materialNames) ? params.row.materialNames : [];
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
{vals.length ? vals.map((m) => (
|
||||||
|
<span
|
||||||
|
key={m}
|
||||||
|
style={{
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: '#DFCCBC',
|
||||||
|
color: '#26201A',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '18px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{m}
|
||||||
|
</span>
|
||||||
|
)) : '—'}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sortable: false,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'createdAt',
|
field: 'createdAt',
|
||||||
headerName: 'Created Date',
|
headerName: 'Created Date',
|
||||||
@@ -211,7 +296,7 @@ export default function Categories() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
||||||
<Typography variant="h6">Categories</Typography>
|
<Typography color='text.primary' variant="h6">Categories</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
@@ -276,6 +361,7 @@ export default function Categories() {
|
|||||||
initialMaterialNames={editingCategory?.materialNames || []}
|
initialMaterialNames={editingCategory?.materialNames || []}
|
||||||
onAdd={handleFormDone}
|
onAdd={handleFormDone}
|
||||||
onCancel={() => { setOpen(false); setEditingCategory(null); }}
|
onCancel={() => { setOpen(false); setEditingCategory(null); }}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -284,8 +370,8 @@ export default function Categories() {
|
|||||||
<DialogTitle>Delete Category</DialogTitle>
|
<DialogTitle>Delete Category</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box sx={{ display: 'flex', gap: 1, mt: 2, mb: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1, mt: 2, mb: 1 }}>
|
||||||
<Button onClick={() => setConfirmOpen(false)}>Cancel</Button>
|
<Button onClick={() => setConfirmOpen(false)} className='button-transparent'>Cancel</Button>
|
||||||
<Button color="error" variant="contained" onClick={confirmDelete}>Delete</Button>
|
<Button color="error" variant="contained" onClick={confirmDelete} className="button-gold">Delete</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
361
src/private/catalogs/products/AddOrEditProductCollectionForm.jsx
Normal file
361
src/private/catalogs/products/AddOrEditProductCollectionForm.jsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
// src/private/furniture/AddOrEditFurnitureVariantForm.jsx
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Box, Button, TextField, MenuItem, CircularProgress } from '@mui/material';
|
||||||
|
import FurnitureVariantApi from '../../../api/ProductsApi';
|
||||||
|
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 AddOrEditProductCollectionForm({ initialData, onAdd, onCancel, viewOnly = false }) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
||||||
|
|
||||||
|
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: DEFAULT_MODEL_ID,
|
||||||
|
name: '',
|
||||||
|
color: '',
|
||||||
|
line: '',
|
||||||
|
stock: 0,
|
||||||
|
price: 0,
|
||||||
|
currency: 'USD',
|
||||||
|
categoryId: '',
|
||||||
|
providerId: '',
|
||||||
|
attributes: { material: '', legs: '', origin: '' },
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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(err);
|
||||||
|
alert(err?.message || 'Error saving variant');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
{/* Name */}
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
fullWidth
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => setVal('name', e.target.value)}
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
disabled={viewOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Category / Provider */}
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Category"
|
||||||
|
fullWidth
|
||||||
|
value={form.categoryId}
|
||||||
|
onChange={(e) => setVal('categoryId', e.target.value)}
|
||||||
|
helperText="Se envía el tagName por ahora"
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Provider"
|
||||||
|
fullWidth
|
||||||
|
value={form.providerId}
|
||||||
|
onChange={(e) => setVal('providerId', e.target.value)}
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
<Box display="flex" gap={2}>
|
||||||
|
{/* Color */}
|
||||||
|
<Box flex={1}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Color"
|
||||||
|
fullWidth
|
||||||
|
value={form.color}
|
||||||
|
onChange={(e) => setVal('color', e.target.value)}
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</Box>
|
||||||
|
{/* Line */}
|
||||||
|
<Box flex={1}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Line"
|
||||||
|
fullWidth
|
||||||
|
value={form.line}
|
||||||
|
onChange={(e) => setVal('line', e.target.value)}
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</Box>
|
||||||
|
{/* Material */}
|
||||||
|
<Box flex={1}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Material"
|
||||||
|
fullWidth
|
||||||
|
value={form.attributes.material}
|
||||||
|
onChange={(e) => setVal('attributes.material', e.target.value)}
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" gap={2}>
|
||||||
|
{/* Price */}
|
||||||
|
<Box flex={1}>
|
||||||
|
<TextField label="Price" type="number" fullWidth value={form.price} onChange={(e) => setVal('price', e.target.value)} disabled={viewOnly} />
|
||||||
|
</Box>
|
||||||
|
{/* Stock */}
|
||||||
|
<Box flex={1}>
|
||||||
|
<TextField label="Stock" type="number" fullWidth value={form.stock} onChange={(e) => setVal('stock', e.target.value)} disabled={viewOnly} />
|
||||||
|
</Box>
|
||||||
|
{/* Currency */}
|
||||||
|
<Box flex={1}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Currency"
|
||||||
|
fullWidth
|
||||||
|
value={form.currency}
|
||||||
|
onChange={(e) => setVal('currency', e.target.value)}
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Attributes */}
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Legs"
|
||||||
|
fullWidth
|
||||||
|
value={form.attributes.legs}
|
||||||
|
onChange={(e) => setVal('attributes.legs', e.target.value)}
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Origin"
|
||||||
|
fullWidth
|
||||||
|
value={form.attributes.origin}
|
||||||
|
onChange={(e) => setVal('attributes.origin', e.target.value)}
|
||||||
|
disabled={viewOnly}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<TextField select label="Status" fullWidth value={form.status} onChange={(e) => setVal('status', e.target.value)} disabled={viewOnly}>
|
||||||
|
<MenuItem value="Active">Active</MenuItem>
|
||||||
|
<MenuItem value="Inactive">Inactive</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" justifyContent="flex-end" mt={3} gap={1}>
|
||||||
|
<Button onClick={onCancel} className="button-transparent">{viewOnly ? 'Close' : 'Cancel'}</Button>
|
||||||
|
{!viewOnly && (
|
||||||
|
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
397
src/private/catalogs/products/ProductCollections.jsx
Normal file
397
src/private/catalogs/products/ProductCollections.jsx
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
import SectionContainer from '../../../components/SectionContainer';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
|
import {
|
||||||
|
Typography, Button, Dialog, DialogTitle, DialogContent,
|
||||||
|
IconButton, Box, ToggleButtonGroup, ToggleButton
|
||||||
|
} 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 VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded';
|
||||||
|
import AddOrEditProductCollectionForm from './AddOrEditProductCollectionForm';
|
||||||
|
import FurnitureVariantApi from '../../../api/ProductsApi';
|
||||||
|
import CategoriesApi from '../../../api/CategoriesApi';
|
||||||
|
import TagTypeApi from '../../../api/TagTypeApi';
|
||||||
|
import { useAuth } from '../../../context/AuthContext';
|
||||||
|
import useApiToast from '../../../hooks/useApiToast';
|
||||||
|
|
||||||
|
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 ProductCollections() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
||||||
|
|
||||||
|
const api = useMemo(() => new FurnitureVariantApi(token), [token]);
|
||||||
|
const tagTypeApi = useMemo(() => new TagTypeApi(token), [token]);
|
||||||
|
const categoriesApi = useMemo(() => new CategoriesApi(token), [token]);
|
||||||
|
|
||||||
|
const { handleError } = useApiToast();
|
||||||
|
|
||||||
|
const [rows, setRows] = useState([]);
|
||||||
|
const [rawRows, setRawRows] = useState([]);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [editRow, setEditRow] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const [statusFilter, setStatusFilter] = useState('All');
|
||||||
|
|
||||||
|
const [viewOnly, setViewOnly] = useState(false);
|
||||||
|
|
||||||
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
|
||||||
|
// 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]: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 || '',
|
||||||
|
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: r?.attributes?.material ?? '',
|
||||||
|
legs: r?.attributes?.legs ?? '',
|
||||||
|
origin: r?.attributes?.origin ?? '',
|
||||||
|
},
|
||||||
|
status: r.status ?? 'Active',
|
||||||
|
createdAt: r.createdAt ?? null,
|
||||||
|
createdBy: r.createdBy ?? null,
|
||||||
|
}));
|
||||||
|
setRawRows(normalized);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
handleError(err, 'Error loading product collections');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { load(); /* eslint-disable-next-line */ }, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (statusFilter === 'All') {
|
||||||
|
setRows(rawRows);
|
||||||
|
} else {
|
||||||
|
const want = statusFilter.toLowerCase();
|
||||||
|
setRows(rawRows.filter(r => String(r.status ?? 'Active').toLowerCase() === want));
|
||||||
|
}
|
||||||
|
}, [statusFilter, rawRows]);
|
||||||
|
|
||||||
|
const handleDeleteClick = (row) => {
|
||||||
|
setEditRow(row);
|
||||||
|
setConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
try {
|
||||||
|
if (!editRow?.id) return;
|
||||||
|
await api.changeStatusVariant({ mongoId: editRow._Id, status: 'Inactive' });
|
||||||
|
await load();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setConfirmOpen(false);
|
||||||
|
setEditRow(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
headerName: '',
|
||||||
|
width: 190,
|
||||||
|
sortable: false,
|
||||||
|
filterable: false,
|
||||||
|
disableExport: true,
|
||||||
|
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={() => { setEditRow(params.row); setViewOnly(false); setOpen(true); }}
|
||||||
|
>
|
||||||
|
<EditRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#E3F2FD',
|
||||||
|
color: '#1565C0',
|
||||||
|
'&:hover': { backgroundColor: '#BBDEFB' },
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => { setEditRow(params.row); setOpen(true); setViewOnly(true); }}
|
||||||
|
>
|
||||||
|
<VisibilityRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#FBE9E7',
|
||||||
|
color: '#C62828',
|
||||||
|
'&:hover': { backgroundColor: '#EF9A9A' },
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => { setViewOnly(false); handleDeleteClick(params?.row); }}
|
||||||
|
>
|
||||||
|
<DeleteRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ field: 'name', headerName: 'Name', flex: 1, minWidth: 160 },
|
||||||
|
{ field: 'categoryId', headerName: 'Category', flex: 1, minWidth: 160 },
|
||||||
|
{ field: 'providerId', headerName: 'Provider', flex: 1, minWidth: 160 },
|
||||||
|
{ field: 'color', headerName: 'Color', flex: 1, minWidth: 160 },
|
||||||
|
{ field: 'line', headerName: 'Line', flex: 1, minWidth: 160 },
|
||||||
|
{
|
||||||
|
field: 'price',
|
||||||
|
headerName: 'Price',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 160,
|
||||||
|
type: 'number',
|
||||||
|
valueGetter: (p) => parsePrice(p?.row?.price),
|
||||||
|
renderCell: (p) => {
|
||||||
|
const val = parsePrice(p?.row?.price);
|
||||||
|
const currency = p?.row?.currency || 'USD';
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(val);
|
||||||
|
} catch {
|
||||||
|
return `${currency} ${val.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ field: 'currency', headerName: 'Currency', flex: 1, minWidth: 160 },
|
||||||
|
{ field: 'stock', headerName: 'Stock', flex: 1, minWidth: 160 },
|
||||||
|
{
|
||||||
|
field: 'attributes',
|
||||||
|
headerName: 'Attributes',
|
||||||
|
minWidth: 300,
|
||||||
|
flex: 1.5,
|
||||||
|
sortable: false,
|
||||||
|
filterable: false,
|
||||||
|
renderCell: (params) => {
|
||||||
|
const a = params?.row?.attributes || {};
|
||||||
|
const chips = Object.entries(a).filter(([, v]) => !!v);
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||||
|
{chips.map(([key, value]) => (
|
||||||
|
<Box
|
||||||
|
key={key}
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: '12px',
|
||||||
|
backgroundColor: '#DFCCBC',
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#26201A',
|
||||||
|
lineHeight: '18px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{key}: {value}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ field: 'status', headerName: 'Status', flex: 1, minWidth: 160 }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100vh - 64px - 64px)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2} flexWrap="wrap" gap={2}>
|
||||||
|
<Typography color='text.primary' variant="h6">Product Collection</Typography>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<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>
|
||||||
|
<Button variant="contained" className="button-gold" startIcon={<AddRoundedIcon />} onClick={() => { setEditRow(null); setViewOnly(false); setOpen(true); }}>
|
||||||
|
Add Product Collection
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
loading={loading || loadingTags}
|
||||||
|
pageSizeOptions={[50, 100, 200]}
|
||||||
|
initialState={{
|
||||||
|
pagination: { paginationModel: { pageSize: 50 } },
|
||||||
|
columns: { columnVisibilityModel: { id: false, _Id: false } },
|
||||||
|
}}
|
||||||
|
getRowHeight={() => 'auto'}
|
||||||
|
columnBuffer={0}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
'& .MuiDataGrid-cell, & .MuiDataGrid-columnHeader': {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-filler': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slots={{
|
||||||
|
noRowsOverlay: () => (
|
||||||
|
<Box sx={{ p: 2 }}>No product collection found. Try switching the status filter to "All".</Box>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="md" fullWidth>
|
||||||
|
<DialogTitle>
|
||||||
|
{viewOnly
|
||||||
|
? 'View Product Collection'
|
||||||
|
: editRow
|
||||||
|
? 'Edit Product Collection'
|
||||||
|
: 'Add Product Collection'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<AddOrEditProductCollectionForm
|
||||||
|
initialData={editRow}
|
||||||
|
viewOnly={viewOnly}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
|
||||||
|
<DialogTitle>Delete Product Collection</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, mt: 2, mb: 1 }}>
|
||||||
|
<Button onClick={() => setConfirmOpen(false)} className='button-transparent'>Cancel</Button>
|
||||||
|
<Button color="error" variant="contained" onClick={confirmDelete} className="button-gold">Delete</Button>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user