Compare commits

..

10 Commits

Author SHA1 Message Date
Rodolfo Ruiz
f7adaf1b18 chore: clean code, add delete 2025-09-06 20:45:56 -06:00
Rodolfo Ruiz
b3209a4019 chore: delete implementation in product collection 2025-09-06 20:34:46 -06:00
Rodolfo Ruiz
efdb48919f chore: add the logic to see in view mode the categories as well 2025-09-06 20:08:39 -06:00
Rodolfo Ruiz
6b8d5acc0d chore: add the view button 2025-09-06 20:04:38 -06:00
Rodolfo Ruiz
01a19b9144 chore: improve the ui for Products 2025-09-06 19:53:10 -06:00
Rodolfo Ruiz
74d6a8b269 chore: fix the edit popup 2025-09-06 19:48:21 -06:00
Rodolfo Ruiz
c33de6ada5 chore: show all the columns from db 2025-09-06 19:19:40 -06:00
Rodolfo Ruiz
15107a48bd chore: show all the fields from the getAll 2025-09-06 18:52:45 -06:00
Rodolfo Ruiz
73699009fc chore: fix products columns 2025-09-06 18:44:42 -06:00
Rodolfo Ruiz
55dc96085d chore: add filters 2025-09-06 18:34:09 -06:00
6 changed files with 420 additions and 237 deletions

View File

@@ -1,7 +1,7 @@
export default class ProductsApi {
constructor(token) {
this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/FurnitureVariant';
this.token = token;
this.token = token;
}
headers(json = true) {
@@ -52,4 +52,15 @@ export default class ProductsApi {
if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`);
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();
}
}

View File

@@ -43,7 +43,7 @@ function formatDateSafe(value) {
}).format(d);
}
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel, materials: materialsProp = [], initialMaterialNames = [] }) {
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel, materials: materialsProp = [], initialMaterialNames = [], viewOnly = false }) {
const { user } = useAuth();
const token = user?.thalosToken || localStorage.getItem('thalosToken');
const api = useMemo(() => new CategoriesApi(token), [token]);
@@ -52,14 +52,14 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel, ma
const [types, setTypes] = useState([]);
const [allTags, setAllTags] = useState([]);
const tagLabelById = useMemo(() => {
const map = {};
for (const t of allTags) {
const key = t._id;
map[key] = t.tagName || t.name || key;
}
return map;
}, [allTags]);
const tagLabelById = useMemo(() => {
const map = {};
for (const t of allTags) {
const key = t._id;
map[key] = t.tagName || t.name || key;
}
return map;
}, [allTags]);
const [form, setForm] = useState({
_id: '',
@@ -248,6 +248,7 @@ const tagLabelById = useMemo(() => {
fullWidth
sx={{ mb: 2 }}
InputProps={{ readOnly: true }}
disabled={viewOnly}
/>
)}
@@ -259,6 +260,7 @@ const tagLabelById = useMemo(() => {
fullWidth
sx={{ mb: 2 }}
required
disabled={viewOnly}
/>
<TextField
@@ -270,6 +272,7 @@ const tagLabelById = useMemo(() => {
fullWidth
sx={{ mb: 2 }}
required
disabled={viewOnly}
>
{types.map((t) => {
const value = t._id;
@@ -304,6 +307,7 @@ const tagLabelById = useMemo(() => {
}}
fullWidth
sx={{ mb: 2 }}
disabled={viewOnly}
>
{allTags.map((t) => {
const value = t._id;
@@ -323,6 +327,7 @@ const tagLabelById = useMemo(() => {
onChange={handleChange}
fullWidth
sx={{ mb: 2 }}
disabled={viewOnly}
/>
<TextField
@@ -333,6 +338,7 @@ const tagLabelById = useMemo(() => {
onChange={handleChange}
fullWidth
sx={{ mb: 2 }}
disabled={viewOnly}
/>
<TextField
@@ -343,6 +349,7 @@ const tagLabelById = useMemo(() => {
fullWidth
sx={{ mb: 2 }}
required
disabled={viewOnly}
/>
<TextField
@@ -353,6 +360,7 @@ const tagLabelById = useMemo(() => {
select
fullWidth
sx={{ mb: 2 }}
disabled={viewOnly}
>
<MenuItem value="Active">Active</MenuItem>
<MenuItem value="Inactive">Inactive</MenuItem>
@@ -368,12 +376,14 @@ const tagLabelById = useMemo(() => {
) : null}
<Box display="flex" justifyContent="space-between" gap={1} mt={3}>
{form._id ? (
{form._id && !viewOnly ? (
<Button color="error" onClick={handleDelete}>Delete</Button>
) : <span />}
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
<Button onClick={onCancel} className="button-transparent">{viewOnly ? 'Close' : 'Cancel'}</Button>
{!viewOnly && (
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
)}
</Box>
</Box>
</Paper>

View File

@@ -6,6 +6,7 @@ import {
import { DataGrid } from '@mui/x-data-grid';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
import VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded';
import AddOrEditCategoryForm from './AddOrEditCategoryForm';
import CategoriesApi from '../../../api/CategoriesApi';
import { useAuth } from '../../../context/AuthContext';
@@ -22,6 +23,7 @@ export default function Categories() {
const [editingCategory, setEditingCategory] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [rowToDelete, setRowToDelete] = useState(null);
const [viewOnly, setViewOnly] = useState(false);
const hasLoaded = useRef(false);
const pageSize = 100; // Número de filas por página
@@ -68,11 +70,13 @@ export default function Categories() {
};
const handleAddClick = () => {
setViewOnly(false);
setEditingCategory(null);
setOpen(true);
};
const handleEditClick = (params) => {
setViewOnly(false);
const r = params?.row;
if (!r) return;
setEditingCategory({
@@ -145,7 +149,7 @@ export default function Categories() {
{
field: 'actions',
headerName: '',
width: 130,
width: 150,
sortable: false,
filterable: false,
disableExport: true,
@@ -164,6 +168,44 @@ export default function Categories() {
>
<EditRoundedIcon fontSize="small" />
</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
size="small"
sx={{
@@ -254,7 +296,7 @@ export default function Categories() {
}}
>
<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 }}>
<ToggleButtonGroup
@@ -319,6 +361,7 @@ export default function Categories() {
initialMaterialNames={editingCategory?.materialNames || []}
onAdd={handleFormDone}
onCancel={() => { setOpen(false); setEditingCategory(null); }}
viewOnly={viewOnly}
/>
</DialogContent>
</Dialog>
@@ -327,8 +370,8 @@ export default function Categories() {
<DialogTitle>Delete Category</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', gap: 1, mt: 2, mb: 1 }}>
<Button onClick={() => setConfirmOpen(false)}>Cancel</Button>
<Button color="error" variant="contained" onClick={confirmDelete}>Delete</Button>
<Button onClick={() => setConfirmOpen(false)} className='button-transparent'>Cancel</Button>
<Button color="error" variant="contained" onClick={confirmDelete} className="button-gold">Delete</Button>
</Box>
</DialogContent>
</Dialog>

View File

@@ -1,6 +1,6 @@
// src/private/furniture/AddOrEditFurnitureVariantForm.jsx
import { useEffect, useMemo, useState } from 'react';
import { Box, Button, TextField, MenuItem, Grid, CircularProgress } from '@mui/material';
import { Box, Button, TextField, MenuItem, CircularProgress } from '@mui/material';
import FurnitureVariantApi from '../../../api/ProductsApi';
import CategoriesApi from '../../../api/CategoriesApi';
import TagTypeApi from '../../../api/TagTypeApi';
@@ -19,7 +19,7 @@ const TYPE_NAMES = {
origin: 'Origin',
};
export default function AddOrEditProductCollectionForm({ initialData, onAdd, onCancel }) {
export default function AddOrEditProductCollectionForm({ initialData, onAdd, onCancel, viewOnly = false }) {
const { user } = useAuth();
const token = user?.thalosToken || localStorage.getItem('thalosToken');
@@ -197,158 +197,164 @@ export default function AddOrEditProductCollectionForm({ initialData, onAdd, onC
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>
<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}
/>
{/* 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>
{/* 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>
<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>
<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>
{/* 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>
<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>
<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>
{/* 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 */}
<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>
<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">Cancel</Button>
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
<Button onClick={onCancel} className="button-transparent">{viewOnly ? 'Close' : 'Cancel'}</Button>
{!viewOnly && (
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
)}
</Box>
</Box>
);

View File

@@ -3,11 +3,12 @@ import { useEffect, useMemo, useState } from 'react';
import { DataGrid } from '@mui/x-data-grid';
import {
Typography, Button, Dialog, DialogTitle, DialogContent,
IconButton, Box, FormControlLabel, Switch, Tooltip
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';
@@ -42,15 +43,20 @@ export default function ProductCollections() {
const tagTypeApi = useMemo(() => new TagTypeApi(token), [token]);
const categoriesApi = useMemo(() => new CategoriesApi(token), [token]);
const toast = useApiToast();
const { handleError } = 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);
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({});
@@ -80,15 +86,6 @@ export default function ProductCollections() {
};
};
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;
@@ -154,10 +151,9 @@ export default function ProductCollections() {
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');
handleError(err, 'Error loading product collections');
} finally {
setLoading(false);
}
@@ -165,108 +161,215 @@ export default function ProductCollections() {
useEffect(() => { load(); /* eslint-disable-next-line */ }, []);
useEffect(() => {
setRows(rawRows.filter(r => showInactive ? true : r.status !== 'Inactive'));
}, [showInactive, rawRows]);
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: '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: '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',
width: 130,
flex: 1,
minWidth: 160,
type: 'number',
valueGetter: (p) => parsePrice(p?.row?.price),
renderCell: (p) => {
const currency = labelCurrency(p?.row?.currency || 'USD');
const val = parsePrice(p?.row?.price);
const currency = p?.row?.currency || 'USD';
try {
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency || 'USD' }).format(val);
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).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: 'currency', headerName: 'Currency', flex: 1, minWidth: 160 },
{ field: 'stock', headerName: 'Stock', flex: 1, minWidth: 160 },
{
field: 'actions',
headerName: '',
field: 'attributes',
headerName: 'Attributes',
minWidth: 300,
flex: 1.5,
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>
)
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 (
<SectionContainer>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Typography variant="h6">Furniture Variants</Typography>
<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}>
<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
<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={{ width: '100%', height: 560 }}>
<Box sx={{ flex: 1, minHeight: 0 }}>
<DataGrid
rows={rows}
columns={columns}
disableRowSelectionOnClick
loading={loading || loadingTags}
pageSizeOptions={[10, 25, 50]}
pageSizeOptions={[50, 100, 200]}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
pagination: { paginationModel: { pageSize: 50 } },
columns: { columnVisibilityModel: { id: false, _Id: false } },
}}
getRowHeight={() => 'auto'}
columnBuffer={0}
sx={{
'& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center' },
'& .MuiDataGrid-columnHeader': { display: 'flex', alignItems: 'center' },
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>{editRow ? 'Edit Product Collection' : 'Add Product Collection'}</DialogTitle>
<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) {
@@ -279,6 +382,16 @@ export default function ProductCollections() {
/>
</DialogContent>
</Dialog>
</SectionContainer>
<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>
);
}

View File

@@ -102,7 +102,7 @@ export default function UserManagement() {
status: r.status ?? 'Active',
companies: Array.isArray(r.companies) ? r.companies : [],
projects: Array.isArray(r.projects) ? r.projects : [],
};
};
setEditingData(normalized);
setOpen(true);
};