features: services and filters implemented
This commit is contained in:
		| @@ -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) { | ||||||
| @@ -63,14 +63,15 @@ export default class CategoriesApi { | |||||||
|     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(); | ||||||
|   } |   } | ||||||
|  |    | ||||||
|   // 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(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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]} | ||||||
| @@ -253,4 +274,4 @@ export default function FurnitureVariantManagement() { | |||||||
|       </Box> |       </Box> | ||||||
|     </SectionContainer> |     </SectionContainer> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user