features: services and filters implemented

This commit is contained in:
2025-09-01 19:09:48 -06:00
parent 2dc6cf9fcb
commit 24f82889e1
4 changed files with 290 additions and 182 deletions

View File

@@ -1,8 +1,7 @@
// src/api/CategoriesApi.js // src/api/CategoriesApi.js
export default class CategoriesApi { export default class CategoriesApi {
constructor(token) { constructor(token) {
this.tagUrl = 'https://inventory-bff.dream-views.com/api/v1/Tag'; // <— singular this.root = 'https://inventory-bff.dream-views.com/api/v1';
this.tagTypeUrl = 'https://inventory-bff.dream-views.com/api/v1/TagType';
this.token = token; this.token = token;
} }
@@ -14,9 +13,9 @@ export default class CategoriesApi {
}; };
} }
// TAGS // ---- Tag ----
async getAll() { async getAll() {
const res = await fetch(`${this.tagUrl}/GetAll`, { const res = await fetch(`${this.root}/Tag/GetAll`, {
method: 'GET', method: 'GET',
headers: this.headers(false), headers: this.headers(false),
}); });
@@ -24,8 +23,8 @@ export default class CategoriesApi {
return res.json(); return res.json();
} }
async create(payload) { // CreateTagRequest async create(payload) {
const res = await fetch(`${this.tagUrl}/Create`, { const res = await fetch(`${this.root}/Tag/Create`, {
method: 'POST', method: 'POST',
headers: this.headers(), headers: this.headers(),
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -34,8 +33,8 @@ export default class CategoriesApi {
return res.json(); return res.json();
} }
async update(payload) { // UpdateTagRequest async update(payload) {
const res = await fetch(`${this.tagUrl}/Update`, { const res = await fetch(`${this.root}/Tag/Update`, {
method: 'PUT', method: 'PUT',
headers: this.headers(), headers: this.headers(),
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -44,14 +43,15 @@ export default class CategoriesApi {
return res.json(); return res.json();
} }
async changeStatus(payload) { // { id, status } // PATCH ChangeStatus: body { id, status }
const res = await fetch(`${this.tagUrl}/ChangeStatus`, { async changeStatus({ id, status }) {
const res = await fetch(`${this.root}/Tag/ChangeStatus`, {
method: 'PATCH', method: 'PATCH',
headers: this.headers(), headers: this.headers(),
body: JSON.stringify(payload), body: JSON.stringify({ id, status }),
}); });
if (!res.ok) throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`); if (!res.ok) throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`);
return res.json(); return res.json?.() ?? null;
} }
async delete(payload) { async delete(payload) {
@@ -64,13 +64,14 @@ export default class CategoriesApi {
return res.json(); return res.json();
} }
// TAG TYPES (para <select> de typeId) // ---- TagType ----
async getAllTypes() { async getAllTypes() {
const res = await fetch(`${this.tagTypeUrl}/GetAll`, { const res = await fetch(`${this.root}/TagType/GetAll`, {
method: 'GET', method: 'GET',
headers: this.headers(false), headers: this.headers(false),
}); });
if (!res.ok) throw new Error(`GetAllTypes error ${res.status}: ${await res.text()}`); if (!res.ok) throw new Error(`TagType GetAll error ${res.status}: ${await res.text()}`);
return res.json(); return res.json();
} }
} }

View File

@@ -1,15 +1,31 @@
// src/private/categories/AddOrEditCategoryForm.jsx
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material';
Box, Button, Paper, TextField, Typography,
FormControl, InputLabel, Select, MenuItem, Chip, OutlinedInput
} from '@mui/material';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import CategoriesApi from '../../api/CategoriesApi'; import CategoriesApi from '../../api/CategoriesApi';
import { jwtDecode } from 'jwt-decode';
const toSlug = (s) => function slugify(s) {
(s ?? '').toString().trim().toLowerCase() return (s || '')
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') .normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''); .toLowerCase().trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function extractTenantId(token) {
try {
const payload = jwtDecode(token);
const t = payload?.tenant;
if (Array.isArray(t)) {
// prefer a 24-hex string if present
const hex = t.find(x => typeof x === 'string' && /^[0-9a-f]{24}$/i.test(x));
return hex || (typeof t[0] === 'string' ? t[0] : '');
}
if (typeof t === 'string') return t;
} catch {}
return '';
}
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) { export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) {
const { user } = useAuth(); const { user } = useAuth();
@@ -19,204 +35,209 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
const [types, setTypes] = useState([]); const [types, setTypes] = useState([]);
const [allTags, setAllTags] = useState([]); const [allTags, setAllTags] = useState([]);
const [category, setCategory] = useState({ const [form, setForm] = useState({
_Id: '', _Id: '',
id: '', id: '',
name: '', tagName: '',
slug: '',
typeId: '', typeId: '',
parentTagId: [], // array<string> parentTagId: [],
slug: '',
displayOrder: 0, displayOrder: 0,
icon: '', icon: '',
status: 'Active', status: 'Active',
}); });
// cargar tipos y tags para selects
useEffect(() => { useEffect(() => {
// cargar Tag Types y Tags para selects
(async () => { (async () => {
try { try {
const [t, tags] = await Promise.all([ const [t, tags] = await Promise.all([api.getAllTypes(), api.getAll()]);
api.getAllTypes(),
api.getAll(),
]);
setTypes(Array.isArray(t) ? t : []); setTypes(Array.isArray(t) ? t : []);
setAllTags(Array.isArray(tags) ? tags : []); setAllTags(Array.isArray(tags) ? tags : []);
} catch (e) { } catch (e) {
console.error('Loading form dictionaries failed:', e); console.error('Failed to load tag types or tags', e);
} }
})(); })();
}, [api]); }, [api]);
// set inicial
useEffect(() => { useEffect(() => {
if (initialData) { if (initialData) {
const _Id = initialData._id || initialData._Id || ''; const _Id = initialData._id || initialData._Id || '';
const id = initialData.id || initialData.Id || _Id || ''; const id = initialData.id || initialData.Id || _Id || '';
const name = initialData.tagName ?? initialData.name ?? ''; setForm({
const slug = initialData.slug ?? toSlug(name); _Id,
const status = initialData.status ?? 'Active'; id,
const typeId = initialData.typeId ?? ''; tagName: initialData.tagName || initialData.name || '',
const parentTagId = Array.isArray(initialData.parentTagId) ? initialData.parentTagId : []; typeId: initialData.typeId || '',
const displayOrder = Number.isFinite(initialData.displayOrder) ? initialData.displayOrder : 0; parentTagId: Array.isArray(initialData.parentTagId) ? initialData.parentTagId : [],
const icon = initialData.icon ?? ''; slug: initialData.slug || slugify(initialData.tagName || initialData.name || ''),
setCategory({ _Id, id, name, slug, status, typeId, parentTagId, displayOrder, icon }); displayOrder: Number(initialData.displayOrder ?? 0),
icon: initialData.icon || '',
status: initialData.status || 'Active',
});
} else { } else {
setCategory({ setForm({
_Id: '', id: '', name: '', slug: '', status: 'Active', _Id: '',
typeId: '', parentTagId: [], displayOrder: 0, icon: '' id: '',
tagName: '',
typeId: '',
parentTagId: [],
slug: '',
displayOrder: 0,
icon: '',
status: 'Active',
}); });
} }
}, [initialData]); }, [initialData]);
const updateField = (name, value) => { const setVal = (name, value) => setForm(p => ({ ...p, [name]: value }));
setCategory((prev) => ({ ...prev, [name]: value }));
if (name === 'name' && !category._Id) { const handleChange = (e) => {
// autogenera slug en "create" const { name, value } = e.target;
const maybe = toSlug(value); setVal(name, value);
setCategory((prev) => ({ ...prev, slug: maybe })); if (name === 'tagName' && !form._Id) {
setVal('slug', slugify(value));
} }
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
if (category._Id) { const tenantId = extractTenantId(token);
// UpdateTagRequest
if (!form.tagName?.trim()) throw new Error('Tag name is required');
if (!form.typeId) throw new Error('Type is required');
if (!form.icon?.trim()) throw new Error('Icon is required');
if (!tenantId) throw new Error('TenantId not found in token');
const base = {
tagName: form.tagName.trim(),
typeId: form.typeId,
parentTagId: form.parentTagId,
slug: form.slug.trim() || slugify(form.tagName),
displayOrder: Number(form.displayOrder) || 0,
icon: form.icon.trim(),
status: form.status || 'Active',
tenantId, // requerido por backend (400 si falta)
};
if (form._Id) {
// UPDATE
const payload = { const payload = {
id: category.id || category._Id, id: form.id || form._Id, // backend acepta GUID; si no hay, mandamos _id
tenantId: user?.tenantId ?? undefined, ...base,
tagName: category.name,
typeId: category.typeId || null,
parentTagId: category.parentTagId?.length ? category.parentTagId : [],
slug: category.slug,
displayOrder: Number(category.displayOrder) || 0,
icon: category.icon || null,
status: category.status || 'Active',
}; };
await api.update(payload); await api.update(payload);
} else { } else {
// CreateTagRequest // CREATE
const payload = { await api.create(base);
tenantId: user?.tenantId ?? undefined,
tagName: category.name,
typeId: category.typeId || null,
parentTagId: category.parentTagId?.length ? category.parentTagId : [],
slug: category.slug || toSlug(category.name),
displayOrder: Number(category.displayOrder) || 0,
icon: category.icon || null,
};
await api.create(payload);
} }
onAdd?.(); onAdd?.();
} catch (e) { } catch (e) {
console.error('Submit category failed:', e); console.error('Submit category failed:', e);
alert(e.message || 'Submit failed');
} }
}; };
const typeOptions = types.map(t => ({
id: t.id || t._id,
label: t.typeName || '(no name)',
}));
const tagOptions = allTags
.filter(t => (t._id || t.id) !== (category._Id || category.id)) // evita ser su propio padre
.map(t => ({
id: t.id || t._id,
label: t.tagName || t.name || '(no name)',
}));
return ( return (
<Paper sx={{ p: 2 }}> <Paper sx={{ p: 2 }}>
<Typography variant="subtitle1" sx={{ mb: 2 }}> <Typography variant="subtitle1" sx={{ mb: 2 }}>
{category._Id ? 'Edit Category' : 'Add Category'} {form._Id ? 'Edit Category' : 'Add Category'}
</Typography> </Typography>
<TextField <TextField
name="name" name="tagName"
label="Name *" label="Name"
value={category.name} value={form.tagName}
onChange={(e) => updateField('name', e.target.value)} onChange={handleChange}
fullWidth fullWidth
sx={{ mb: 2 }} sx={{ mb: 2 }}
required
/> />
<TextField
name="typeId"
label="Type"
value={form.typeId}
onChange={handleChange}
select
fullWidth
sx={{ mb: 2 }}
required
>
{types.map(t => (
<MenuItem key={t.id || t._id} value={t.id || t._id}>
{t.typeName} ({t.level ?? '-'})
</MenuItem>
))}
</TextField>
<TextField
name="parentTagId"
label="Parent tags"
value={form.parentTagId}
onChange={(e) => {
const val = e.target.value;
setVal('parentTagId', typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : val);
}}
select
SelectProps={{ multiple: true, renderValue: (selected) => (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{selected.map(v => <Chip key={v} label={v} size="small" />)}
</Box>
)}}
fullWidth
sx={{ mb: 2 }}
>
{allTags.map(t => {
const value = t._id || t.id;
const label = t.tagName || t.name || value;
return <MenuItem key={value} value={value}>{label}</MenuItem>;
})}
</TextField>
<TextField <TextField
name="slug" name="slug"
label="Slug" label="Slug"
value={category.slug} value={form.slug}
onChange={(e) => updateField('slug', e.target.value)} onChange={handleChange}
fullWidth fullWidth
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel id="type-label">Type *</InputLabel>
<Select
labelId="type-label"
label="Type *"
value={category.typeId}
onChange={(e) => updateField('typeId', e.target.value)}
>
{typeOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.label}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel id="parent-label">Parent Categories</InputLabel>
<Select
multiple
labelId="parent-label"
label="Parent Categories"
value={category.parentTagId}
onChange={(e) => updateField('parentTagId', e.target.value)}
input={<OutlinedInput label="Parent Categories" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => {
const match = tagOptions.find(o => o.id === value);
return <Chip key={value} label={match?.label ?? value} />;
})}
</Box>
)}
>
{tagOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.label}</MenuItem>
))}
</Select>
</FormControl>
<TextField <TextField
name="displayOrder" name="displayOrder"
label="Display Order" label="Display order"
type="number" type="number"
value={category.displayOrder} value={form.displayOrder}
onChange={(e) => updateField('displayOrder', e.target.value)} onChange={handleChange}
fullWidth fullWidth
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
<TextField <TextField
name="icon" name="icon"
label="Icon (URL)" label="Icon URL"
value={category.icon} value={form.icon}
onChange={(e) => updateField('icon', e.target.value)} onChange={handleChange}
fullWidth fullWidth
sx={{ mb: 2 }}
required
/> />
{category._Id && ( <TextField
<FormControl fullWidth sx={{ mt: 2 }}> name="status"
<InputLabel id="status-label">Status</InputLabel> label="Status"
<Select value={form.status}
labelId="status-label" onChange={handleChange}
label="Status" select
value={category.status} fullWidth
onChange={(e) => updateField('status', e.target.value)} sx={{ mb: 2 }}
> >
<MenuItem value="Active">Active</MenuItem> <MenuItem value="Active">Active</MenuItem>
<MenuItem value="Inactive">Inactive</MenuItem> <MenuItem value="Inactive">Inactive</MenuItem>
</Select> </TextField>
</FormControl>
)}
<Box display="flex" justifyContent="flex-end" gap={1} mt={3}> <Box display="flex" justifyContent="flex-end" gap={1} mt={3}>
<Button onClick={onCancel} className="button-transparent">Cancel</Button> <Button onClick={onCancel} className="button-transparent">Cancel</Button>

View File

@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material'; import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography,
ToggleButton, ToggleButtonGroup } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
@@ -13,6 +14,7 @@ export default function Categories() {
const api = useMemo(() => new CategoriesApi(token), [token]); const api = useMemo(() => new CategoriesApi(token), [token]);
const [rows, setRows] = useState([]); const [rows, setRows] = useState([]);
const [statusFilter, setStatusFilter] = useState('Active'); // <- por defecto Active
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editingCategory, setEditingCategory] = useState(null); const [editingCategory, setEditingCategory] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
@@ -29,7 +31,13 @@ export default function Categories() {
const loadData = async () => { const loadData = async () => {
try { try {
const data = await api.getAll(); const data = await api.getAll();
setRows(Array.isArray(data) ? data : []); const safeRows = (Array.isArray(data) ? data : [])
.filter(Boolean)
.map((r, idx) => ({
...r,
__rid: r?._id || r?._Id || r?.id || r?.Id || `tmp-${idx}`,
}));
setRows(safeRows);
} catch (e) { } catch (e) {
console.error('Failed to load categories:', e); console.error('Failed to load categories:', e);
setRows([]); setRows([]);
@@ -47,32 +55,41 @@ export default function Categories() {
setEditingCategory({ setEditingCategory({
_Id: r._id || r._Id || '', _Id: r._id || r._Id || '',
id: r.id || r.Id || '', id: r.id || r.Id || '',
name: r.tagName ?? r.name ?? '', tagName: r.tagName || r.name || '',
slug: r.slug ?? '', typeId: r.typeId || '',
typeId: r.typeId ?? '',
parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [], parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [],
displayOrder: Number.isFinite(r.displayOrder) ? r.displayOrder : 0, slug: r.slug || '',
icon: r.icon ?? '', displayOrder: Number(r.displayOrder ?? 0),
icon: r.icon || '',
status: r.status ?? 'Active', status: r.status ?? 'Active',
}); });
setOpen(true); setOpen(true);
}; };
const handleDeleteClick = (row) => { const handleDeleteClick = (row) => {
if (!row) return;
setRowToDelete(row); setRowToDelete(row);
setConfirmOpen(true); setConfirmOpen(true);
}; };
const pickHexId = (r) =>
[r?._id, r?._Id, r?.id, r?.Id]
.filter(Boolean)
.find((x) => typeof x === 'string' && /^[0-9a-f]{24}$/i.test(x)) || null;
const confirmDelete = async () => { const confirmDelete = async () => {
try { try {
if (!rowToDelete) return; if (!rowToDelete) return;
await api.changeStatus({ const hexId = pickHexId(rowToDelete);
id: rowToDelete.id || rowToDelete.Id || rowToDelete._id || rowToDelete._Id, if (!hexId) {
status: 'Inactive', alert('No se encontró _id (24-hex) para ChangeStatus en esta fila.');
}); return;
}
await api.changeStatus({ id: hexId, status: 'Inactive' });
await loadData(); await loadData();
} catch (e) { } catch (e) {
console.error('Delete failed:', e); console.error('Delete failed:', e);
alert('Delete failed. Revisa la consola para más detalles.');
} finally { } finally {
setConfirmOpen(false); setConfirmOpen(false);
setRowToDelete(null); setRowToDelete(null);
@@ -85,41 +102,89 @@ export default function Categories() {
setEditingCategory(null); setEditingCategory(null);
}; };
// --- FILTRO DE ESTADO ---
const filteredRows = useMemo(() => {
if (statusFilter === 'All') return rows;
const want = String(statusFilter).toLowerCase();
return rows.filter((r) => String(r?.status ?? 'Active').toLowerCase() === want);
}, [rows, statusFilter]);
const columns = [ const columns = [
{ field: 'tagName', headerName: 'Name', flex: 1, minWidth: 200, valueGetter: (p) => p.row?.tagName ?? p.row?.name }, {
{ field: 'slug', headerName: 'Slug', width: 220 }, field: 'tagName',
{ field: 'displayOrder', headerName: 'Display', width: 120, valueGetter: (p) => p.row?.displayOrder ?? 0 }, headerName: 'Name',
{ field: 'status', headerName: 'Status', width: 140, valueGetter: (p) => p.row?.status ?? 'Active' }, flex: 1,
minWidth: 200,
valueGetter: (p) => p?.row?.tagName ?? p?.row?.name ?? '',
},
{
field: 'slug',
headerName: 'Slug',
width: 180,
valueGetter: (p) => p?.row?.slug ?? '',
},
{
field: 'displayOrder',
headerName: 'Display',
width: 120,
valueGetter: (p) => Number(p?.row?.displayOrder ?? 0),
},
{
field: 'status',
headerName: 'Status',
width: 140,
valueGetter: (p) => p?.row?.status ?? 'Active',
},
{ {
field: 'actions', field: 'actions',
headerName: '', headerName: '',
width: 120, width: 120,
sortable: false, sortable: false,
filterable: false, filterable: false,
renderCell: (params) => ( renderCell: (params) => {
<Box sx={{ display: 'flex', gap: 1 }}> const r = params?.row;
<IconButton size="small" onClick={() => handleEditClick(params)}><EditIcon /></IconButton> if (!r) return null;
<IconButton size="small" color="error" onClick={() => handleDeleteClick(params.row)}><DeleteIcon /></IconButton> return (
</Box> <Box sx={{ display: 'flex', gap: 1 }}>
), <IconButton size="small" onClick={() => handleEditClick(params)}><EditIcon /></IconButton>
<IconButton size="small" color="error" onClick={() => handleDeleteClick(r)}><DeleteIcon /></IconButton>
</Box>
);
},
}, },
]; ];
return ( return (
<Box> <Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, gap: 2, flexWrap: 'wrap' }}>
<Typography variant="h6">Categories</Typography> <Typography variant="h6">Categories</Typography>
<Button variant="contained" onClick={handleAddClick} className="button-gold">Add Category</Button>
<Box sx={{ 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" onClick={handleAddClick} className="button-gold">
Add Category
</Button>
</Box>
</Box> </Box>
<DataGrid <DataGrid
rows={rows} rows={filteredRows}
columns={columns} columns={columns}
pageSize={10} pageSize={10}
rowsPerPageOptions={[10]} rowsPerPageOptions={[10]}
autoHeight autoHeight
disableColumnMenu disableColumnMenu
getRowId={(r) => r._id || r._Id || r.id || r.Id} getRowId={(r) => r?.__rid}
/> />
<Dialog open={open} onClose={() => { setOpen(false); setEditingCategory(null); }} fullWidth> <Dialog open={open} onClose={() => { setOpen(false); setEditingCategory(null); }} fullWidth>

View File

@@ -1,9 +1,9 @@
import SectionContainer from '../../components/SectionContainer'; import SectionContainer from '../../components/SectionContainer';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState, useMemo } from 'react';
import { DataGrid } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid';
import { import {
Typography, Button, Dialog, DialogTitle, DialogContent, Typography, Button, Dialog, DialogTitle, DialogContent,
IconButton, Box IconButton, Box, ToggleButton, ToggleButtonGroup
} from '@mui/material'; } from '@mui/material';
import EditRoundedIcon from '@mui/icons-material/EditRounded'; import EditRoundedIcon from '@mui/icons-material/EditRounded';
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
@@ -65,6 +65,7 @@ export default function FurnitureVariantManagement() {
const apiRef = useRef(null); const apiRef = useRef(null);
const [rows, setRows] = useState([]); const [rows, setRows] = useState([]);
const [statusFilter, setStatusFilter] = useState('Active'); // <- por defecto Active
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editingData, setEditingData] = useState(null); const [editingData, setEditingData] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
@@ -129,7 +130,7 @@ export default function FurnitureVariantManagement() {
const handleConfirmDelete = async () => { const handleConfirmDelete = async () => {
try { try {
if (!apiRef.current || !(rowToDelete?._id || rowToDelete?._Id)) throw new Error('Missing API or id'); if (!apiRef.current || !(rowToDelete?._id || rowToDelete?._Id)) throw new Error('Missing API or id');
// If your inventory BFF uses soft delete via Update (status=Inactive), do this: // Soft-delete vía Update (status=Inactive)
const payload = { const payload = {
id: rowToDelete.id || rowToDelete.Id || '', id: rowToDelete.id || rowToDelete.Id || '',
_Id: rowToDelete._id || rowToDelete._Id, _Id: rowToDelete._id || rowToDelete._Id,
@@ -149,7 +150,6 @@ export default function FurnitureVariantManagement() {
}, },
status: 'Inactive', status: 'Inactive',
}; };
// Prefer update soft-delete; if you truly have DELETE, switch to apiRef.current.deleteVariant({ _Id: ... })
await apiRef.current.updateVariant(payload); await apiRef.current.updateVariant(payload);
await loadData(); await loadData();
} catch (e) { } catch (e) {
@@ -160,6 +160,13 @@ export default function FurnitureVariantManagement() {
} }
}; };
// --- FILTRO DE ESTADO ---
const filteredRows = useMemo(() => {
if (statusFilter === 'All') return rows;
const want = String(statusFilter).toLowerCase();
return rows.filter((r) => String(r?.status ?? 'Active').toLowerCase() === want);
}, [rows, statusFilter]);
const columns = [ const columns = [
{ {
field: 'actions', field: 'actions',
@@ -229,9 +236,23 @@ export default function FurnitureVariantManagement() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Box mt={2} sx={{ width: '100%', overflowX: 'auto' }}> {/* Toolbar de filtro */}
<Box mt={1} mb={1} display="flex" justifyContent="flex-end">
<ToggleButtonGroup
value={statusFilter}
exclusive
onChange={(_, v) => v && setStatusFilter(v)}
size="small"
>
<ToggleButton value="Active">Active</ToggleButton>
<ToggleButton value="All">All</ToggleButton>
<ToggleButton value="Inactive">Inactive</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={{ width: '100%', overflowX: 'auto' }}>
<DataGrid <DataGrid
rows={rows} rows={filteredRows}
columns={columns} columns={columns}
pageSize={5} pageSize={5}
rowsPerPageOptions={[5]} rowsPerPageOptions={[5]}