features: services and filters implemented
This commit is contained in:
		| @@ -1,8 +1,7 @@ | ||||
| // src/api/CategoriesApi.js | ||||
| export default class CategoriesApi { | ||||
|   constructor(token) { | ||||
|     this.tagUrl = 'https://inventory-bff.dream-views.com/api/v1/Tag';       // <— singular | ||||
|     this.tagTypeUrl = 'https://inventory-bff.dream-views.com/api/v1/TagType'; | ||||
|     this.root = 'https://inventory-bff.dream-views.com/api/v1'; | ||||
|     this.token = token; | ||||
|   } | ||||
|  | ||||
| @@ -14,9 +13,9 @@ export default class CategoriesApi { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // TAGS | ||||
|   // ---- Tag ---- | ||||
|   async getAll() { | ||||
|     const res = await fetch(`${this.tagUrl}/GetAll`, { | ||||
|     const res = await fetch(`${this.root}/Tag/GetAll`, { | ||||
|       method: 'GET', | ||||
|       headers: this.headers(false), | ||||
|     }); | ||||
| @@ -24,8 +23,8 @@ export default class CategoriesApi { | ||||
|     return res.json(); | ||||
|   } | ||||
|  | ||||
|   async create(payload) { // CreateTagRequest | ||||
|     const res = await fetch(`${this.tagUrl}/Create`, { | ||||
|   async create(payload) { | ||||
|     const res = await fetch(`${this.root}/Tag/Create`, { | ||||
|       method: 'POST', | ||||
|       headers: this.headers(), | ||||
|       body: JSON.stringify(payload), | ||||
| @@ -34,8 +33,8 @@ export default class CategoriesApi { | ||||
|     return res.json(); | ||||
|   } | ||||
|  | ||||
|   async update(payload) { // UpdateTagRequest | ||||
|     const res = await fetch(`${this.tagUrl}/Update`, { | ||||
|   async update(payload) { | ||||
|     const res = await fetch(`${this.root}/Tag/Update`, { | ||||
|       method: 'PUT', | ||||
|       headers: this.headers(), | ||||
|       body: JSON.stringify(payload), | ||||
| @@ -44,14 +43,15 @@ export default class CategoriesApi { | ||||
|     return res.json(); | ||||
|   } | ||||
|  | ||||
|   async changeStatus(payload) { // { id, status } | ||||
|     const res = await fetch(`${this.tagUrl}/ChangeStatus`, { | ||||
|   // PATCH ChangeStatus: body { id, status } | ||||
|   async changeStatus({ id, status }) { | ||||
|     const res = await fetch(`${this.root}/Tag/ChangeStatus`, { | ||||
|       method: 'PATCH', | ||||
|       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()}`); | ||||
|     return res.json(); | ||||
|     return res.json?.() ?? null; | ||||
|   } | ||||
|  | ||||
|   async delete(payload) { | ||||
| @@ -64,13 +64,14 @@ export default class CategoriesApi { | ||||
|     return res.json(); | ||||
|   } | ||||
|    | ||||
|   // TAG TYPES (para <select> de typeId) | ||||
|   // ---- TagType ---- | ||||
|   async getAllTypes() { | ||||
|     const res = await fetch(`${this.tagTypeUrl}/GetAll`, { | ||||
|     const res = await fetch(`${this.root}/TagType/GetAll`, { | ||||
|       method: 'GET', | ||||
|       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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,15 +1,31 @@ | ||||
| // src/private/categories/AddOrEditCategoryForm.jsx | ||||
| import { useEffect, useMemo, useState } from 'react'; | ||||
| import { | ||||
|   Box, Button, Paper, TextField, Typography, | ||||
|   FormControl, InputLabel, Select, MenuItem, Chip, OutlinedInput | ||||
| } from '@mui/material'; | ||||
| import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material'; | ||||
| import { useAuth } from '../../context/AuthContext'; | ||||
| import CategoriesApi from '../../api/CategoriesApi'; | ||||
| import { jwtDecode } from 'jwt-decode'; | ||||
|  | ||||
| const toSlug = (s) => | ||||
|   (s ?? '').toString().trim().toLowerCase() | ||||
|     .normalize('NFD').replace(/[\u0300-\u036f]/g, '') | ||||
|     .replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''); | ||||
| function slugify(s) { | ||||
|   return (s || '') | ||||
|     .normalize('NFKD').replace(/[\u0300-\u036f]/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 }) { | ||||
|   const { user } = useAuth(); | ||||
| @@ -19,204 +35,209 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | ||||
|   const [types, setTypes] = useState([]); | ||||
|   const [allTags, setAllTags] = useState([]); | ||||
|  | ||||
|   const [category, setCategory] = useState({ | ||||
|   const [form, setForm] = useState({ | ||||
|     _Id: '', | ||||
|     id: '', | ||||
|     name: '', | ||||
|     slug: '', | ||||
|     tagName: '', | ||||
|     typeId: '', | ||||
|     parentTagId: [],   // array<string> | ||||
|     parentTagId: [], | ||||
|     slug: '', | ||||
|     displayOrder: 0, | ||||
|     icon: '', | ||||
|     status: 'Active', | ||||
|   }); | ||||
|  | ||||
|   // cargar tipos y tags para selects | ||||
|   useEffect(() => { | ||||
|     // cargar Tag Types y Tags para selects | ||||
|     (async () => { | ||||
|       try { | ||||
|         const [t, tags] = await Promise.all([ | ||||
|           api.getAllTypes(), | ||||
|           api.getAll(), | ||||
|         ]); | ||||
|         const [t, tags] = await Promise.all([api.getAllTypes(), api.getAll()]); | ||||
|         setTypes(Array.isArray(t) ? t : []); | ||||
|         setAllTags(Array.isArray(tags) ? tags : []); | ||||
|       } catch (e) { | ||||
|         console.error('Loading form dictionaries failed:', e); | ||||
|         console.error('Failed to load tag types or tags', e); | ||||
|       } | ||||
|     })(); | ||||
|   }, [api]); | ||||
|  | ||||
|   // set inicial | ||||
|   useEffect(() => { | ||||
|     if (initialData) { | ||||
|       const _Id = initialData._id || initialData._Id || ''; | ||||
|       const id = initialData.id || initialData.Id || _Id || ''; | ||||
|       const name = initialData.tagName ?? initialData.name ?? ''; | ||||
|       const slug = initialData.slug ?? toSlug(name); | ||||
|       const status = initialData.status ?? 'Active'; | ||||
|       const typeId = initialData.typeId ?? ''; | ||||
|       const parentTagId = Array.isArray(initialData.parentTagId) ? initialData.parentTagId : []; | ||||
|       const displayOrder = Number.isFinite(initialData.displayOrder) ? initialData.displayOrder : 0; | ||||
|       const icon = initialData.icon ?? ''; | ||||
|       setCategory({ _Id, id, name, slug, status, typeId, parentTagId, displayOrder, icon }); | ||||
|       setForm({ | ||||
|         _Id, | ||||
|         id, | ||||
|         tagName: initialData.tagName || initialData.name || '', | ||||
|         typeId: initialData.typeId || '', | ||||
|         parentTagId: Array.isArray(initialData.parentTagId) ? initialData.parentTagId : [], | ||||
|         slug: initialData.slug || slugify(initialData.tagName || initialData.name || ''), | ||||
|         displayOrder: Number(initialData.displayOrder ?? 0), | ||||
|         icon: initialData.icon || '', | ||||
|         status: initialData.status || 'Active', | ||||
|       }); | ||||
|     } else { | ||||
|       setCategory({ | ||||
|         _Id: '', id: '', name: '', slug: '', status: 'Active', | ||||
|         typeId: '', parentTagId: [], displayOrder: 0, icon: '' | ||||
|       setForm({ | ||||
|         _Id: '', | ||||
|         id: '', | ||||
|         tagName: '', | ||||
|         typeId: '', | ||||
|         parentTagId: [], | ||||
|         slug: '', | ||||
|         displayOrder: 0, | ||||
|         icon: '', | ||||
|         status: 'Active', | ||||
|       }); | ||||
|     } | ||||
|   }, [initialData]); | ||||
|  | ||||
|   const updateField = (name, value) => { | ||||
|     setCategory((prev) => ({ ...prev, [name]: value })); | ||||
|     if (name === 'name' && !category._Id) { | ||||
|       // autogenera slug en "create" | ||||
|       const maybe = toSlug(value); | ||||
|       setCategory((prev) => ({ ...prev, slug: maybe })); | ||||
|   const setVal = (name, value) => setForm(p => ({ ...p, [name]: value })); | ||||
|  | ||||
|   const handleChange = (e) => { | ||||
|     const { name, value } = e.target; | ||||
|     setVal(name, value); | ||||
|     if (name === 'tagName' && !form._Id) { | ||||
|       setVal('slug', slugify(value)); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleSubmit = async () => { | ||||
|     try { | ||||
|       if (category._Id) { | ||||
|         // UpdateTagRequest | ||||
|       const tenantId = extractTenantId(token); | ||||
|  | ||||
|       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 = { | ||||
|           id: category.id || category._Id, | ||||
|           tenantId: user?.tenantId ?? undefined, | ||||
|           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', | ||||
|           id: form.id || form._Id, // backend acepta GUID; si no hay, mandamos _id | ||||
|           ...base, | ||||
|         }; | ||||
|         await api.update(payload); | ||||
|       } else { | ||||
|         // CreateTagRequest | ||||
|         const payload = { | ||||
|           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); | ||||
|         // CREATE | ||||
|         await api.create(base); | ||||
|       } | ||||
|  | ||||
|       onAdd?.(); | ||||
|     } catch (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 ( | ||||
|     <Paper sx={{ p: 2 }}> | ||||
|       <Typography variant="subtitle1" sx={{ mb: 2 }}> | ||||
|         {category._Id ? 'Edit Category' : 'Add Category'} | ||||
|         {form._Id ? 'Edit Category' : 'Add Category'} | ||||
|       </Typography> | ||||
|  | ||||
|       <TextField | ||||
|         name="name" | ||||
|         label="Name *" | ||||
|         value={category.name} | ||||
|         onChange={(e) => updateField('name', e.target.value)} | ||||
|         name="tagName" | ||||
|         label="Name" | ||||
|         value={form.tagName} | ||||
|         onChange={handleChange} | ||||
|         fullWidth | ||||
|         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 | ||||
|         name="slug" | ||||
|         label="Slug" | ||||
|         value={category.slug} | ||||
|         onChange={(e) => updateField('slug', e.target.value)} | ||||
|         value={form.slug} | ||||
|         onChange={handleChange} | ||||
|         fullWidth | ||||
|         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 | ||||
|         name="displayOrder" | ||||
|         label="Display Order" | ||||
|         label="Display order" | ||||
|         type="number" | ||||
|         value={category.displayOrder} | ||||
|         onChange={(e) => updateField('displayOrder', e.target.value)} | ||||
|         value={form.displayOrder} | ||||
|         onChange={handleChange} | ||||
|         fullWidth | ||||
|         sx={{ mb: 2 }} | ||||
|       /> | ||||
|  | ||||
|       <TextField | ||||
|         name="icon" | ||||
|         label="Icon (URL)" | ||||
|         value={category.icon} | ||||
|         onChange={(e) => updateField('icon', e.target.value)} | ||||
|         label="Icon URL" | ||||
|         value={form.icon} | ||||
|         onChange={handleChange} | ||||
|         fullWidth | ||||
|         sx={{ mb: 2 }} | ||||
|         required | ||||
|       /> | ||||
|  | ||||
|       {category._Id && ( | ||||
|         <FormControl fullWidth sx={{ mt: 2 }}> | ||||
|           <InputLabel id="status-label">Status</InputLabel> | ||||
|           <Select | ||||
|             labelId="status-label" | ||||
|       <TextField | ||||
|         name="status" | ||||
|         label="Status" | ||||
|             value={category.status} | ||||
|             onChange={(e) => updateField('status', e.target.value)} | ||||
|         value={form.status} | ||||
|         onChange={handleChange} | ||||
|         select | ||||
|         fullWidth | ||||
|         sx={{ mb: 2 }} | ||||
|       > | ||||
|         <MenuItem value="Active">Active</MenuItem> | ||||
|         <MenuItem value="Inactive">Inactive</MenuItem> | ||||
|           </Select> | ||||
|         </FormControl> | ||||
|       )} | ||||
|       </TextField> | ||||
|  | ||||
|       <Box display="flex" justifyContent="flex-end" gap={1} mt={3}> | ||||
|         <Button onClick={onCancel} className="button-transparent">Cancel</Button> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| 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 EditIcon from '@mui/icons-material/Edit'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | ||||
| @@ -13,6 +14,7 @@ export default function Categories() { | ||||
|   const api = useMemo(() => new CategoriesApi(token), [token]); | ||||
|  | ||||
|   const [rows, setRows] = useState([]); | ||||
|   const [statusFilter, setStatusFilter] = useState('Active'); // <- por defecto Active | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const [editingCategory, setEditingCategory] = useState(null); | ||||
|   const [confirmOpen, setConfirmOpen] = useState(false); | ||||
| @@ -29,7 +31,13 @@ export default function Categories() { | ||||
|   const loadData = async () => { | ||||
|     try { | ||||
|       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) { | ||||
|       console.error('Failed to load categories:', e); | ||||
|       setRows([]); | ||||
| @@ -47,32 +55,41 @@ export default function Categories() { | ||||
|     setEditingCategory({ | ||||
|       _Id: r._id || r._Id || '', | ||||
|       id: r.id || r.Id || '', | ||||
|       name: r.tagName ?? r.name ?? '', | ||||
|       slug: r.slug ?? '', | ||||
|       typeId: r.typeId ?? '', | ||||
|       tagName: r.tagName || r.name || '', | ||||
|       typeId: r.typeId || '', | ||||
|       parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [], | ||||
|       displayOrder: Number.isFinite(r.displayOrder) ? r.displayOrder : 0, | ||||
|       icon: r.icon ?? '', | ||||
|       slug: r.slug || '', | ||||
|       displayOrder: Number(r.displayOrder ?? 0), | ||||
|       icon: r.icon || '', | ||||
|       status: r.status ?? 'Active', | ||||
|     }); | ||||
|     setOpen(true); | ||||
|   }; | ||||
|  | ||||
|   const handleDeleteClick = (row) => { | ||||
|     if (!row) return; | ||||
|     setRowToDelete(row); | ||||
|     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 () => { | ||||
|     try { | ||||
|       if (!rowToDelete) return; | ||||
|       await api.changeStatus({ | ||||
|         id: rowToDelete.id || rowToDelete.Id || rowToDelete._id || rowToDelete._Id, | ||||
|         status: 'Inactive', | ||||
|       }); | ||||
|       const hexId = pickHexId(rowToDelete); | ||||
|       if (!hexId) { | ||||
|         alert('No se encontró _id (24-hex) para ChangeStatus en esta fila.'); | ||||
|         return; | ||||
|       } | ||||
|       await api.changeStatus({ id: hexId, status: 'Inactive' }); | ||||
|       await loadData(); | ||||
|     } catch (e) { | ||||
|       console.error('Delete failed:', e); | ||||
|       alert('Delete failed. Revisa la consola para más detalles.'); | ||||
|     } finally { | ||||
|       setConfirmOpen(false); | ||||
|       setRowToDelete(null); | ||||
| @@ -85,41 +102,89 @@ export default function Categories() { | ||||
|     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 = [ | ||||
|     { field: 'tagName', headerName: 'Name', flex: 1, minWidth: 200, valueGetter: (p) => p.row?.tagName ?? p.row?.name }, | ||||
|     { field: 'slug', headerName: 'Slug', width: 220 }, | ||||
|     { field: 'displayOrder', headerName: 'Display', width: 120, valueGetter: (p) => p.row?.displayOrder ?? 0 }, | ||||
|     { field: 'status', headerName: 'Status', width: 140, valueGetter: (p) => p.row?.status ?? 'Active' }, | ||||
|     { | ||||
|       field: 'tagName', | ||||
|       headerName: 'Name', | ||||
|       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', | ||||
|       headerName: '', | ||||
|       width: 120, | ||||
|       sortable: false, | ||||
|       filterable: false, | ||||
|       renderCell: (params) => ( | ||||
|       renderCell: (params) => { | ||||
|         const r = params?.row; | ||||
|         if (!r) return null; | ||||
|         return ( | ||||
|           <Box sx={{ display: 'flex', gap: 1 }}> | ||||
|             <IconButton size="small" onClick={() => handleEditClick(params)}><EditIcon /></IconButton> | ||||
|           <IconButton size="small" color="error" onClick={() => handleDeleteClick(params.row)}><DeleteIcon /></IconButton> | ||||
|             <IconButton size="small" color="error" onClick={() => handleDeleteClick(r)}><DeleteIcon /></IconButton> | ||||
|           </Box> | ||||
|       ), | ||||
|         ); | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <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> | ||||
|         <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> | ||||
|  | ||||
|       <DataGrid | ||||
|         rows={rows} | ||||
|         rows={filteredRows} | ||||
|         columns={columns} | ||||
|         pageSize={10} | ||||
|         rowsPerPageOptions={[10]} | ||||
|         autoHeight | ||||
|         disableColumnMenu | ||||
|         getRowId={(r) => r._id || r._Id || r.id || r.Id} | ||||
|         getRowId={(r) => r?.__rid} | ||||
|       /> | ||||
|  | ||||
|       <Dialog open={open} onClose={() => { setOpen(false); setEditingCategory(null); }} fullWidth> | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| 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 { | ||||
|   Typography, Button, Dialog, DialogTitle, DialogContent, | ||||
|   IconButton, Box | ||||
|   IconButton, Box, ToggleButton, ToggleButtonGroup | ||||
| } from '@mui/material'; | ||||
| import EditRoundedIcon from '@mui/icons-material/EditRounded'; | ||||
| import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; | ||||
| @@ -65,6 +65,7 @@ export default function FurnitureVariantManagement() { | ||||
|   const apiRef = useRef(null); | ||||
|  | ||||
|   const [rows, setRows] = useState([]); | ||||
|   const [statusFilter, setStatusFilter] = useState('Active'); // <- por defecto Active | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const [editingData, setEditingData] = useState(null); | ||||
|   const [confirmOpen, setConfirmOpen] = useState(false); | ||||
| @@ -129,7 +130,7 @@ export default function FurnitureVariantManagement() { | ||||
|   const handleConfirmDelete = async () => { | ||||
|     try { | ||||
|       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 = { | ||||
|         id: rowToDelete.id || rowToDelete.Id || '', | ||||
|         _Id: rowToDelete._id || rowToDelete._Id, | ||||
| @@ -149,7 +150,6 @@ export default function FurnitureVariantManagement() { | ||||
|         }, | ||||
|         status: 'Inactive', | ||||
|       }; | ||||
|       // Prefer update soft-delete; if you truly have DELETE, switch to apiRef.current.deleteVariant({ _Id: ... }) | ||||
|       await apiRef.current.updateVariant(payload); | ||||
|       await loadData(); | ||||
|     } 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 = [ | ||||
|     { | ||||
|       field: 'actions', | ||||
| @@ -229,9 +236,23 @@ export default function FurnitureVariantManagement() { | ||||
|         </DialogContent> | ||||
|       </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 | ||||
|           rows={rows} | ||||
|           rows={filteredRows} | ||||
|           columns={columns} | ||||
|           pageSize={5} | ||||
|           rowsPerPageOptions={[5]} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user