feat: tags fixes
This commit is contained in:
		| @@ -1,7 +1,8 @@ | |||||||
| // src/api/CategoriesApi.js | // src/api/CategoriesApi.js | ||||||
| export default class CategoriesApi { | export default class CategoriesApi { | ||||||
|   constructor(token) { |   constructor(token) { | ||||||
|     this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/Tags'; |     this.tagUrl = 'https://inventory-bff.dream-views.com/api/v1/Tag';       // <— singular | ||||||
|  |     this.tagTypeUrl = 'https://inventory-bff.dream-views.com/api/v1/TagType'; | ||||||
|     this.token = token; |     this.token = token; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -13,8 +14,9 @@ export default class CategoriesApi { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // TAGS | ||||||
|   async getAll() { |   async getAll() { | ||||||
|     const res = await fetch(`${this.baseUrl}/GetAll`, { |     const res = await fetch(`${this.tagUrl}/GetAll`, { | ||||||
|       method: 'GET', |       method: 'GET', | ||||||
|       headers: this.headers(false), |       headers: this.headers(false), | ||||||
|     }); |     }); | ||||||
| @@ -22,8 +24,8 @@ export default class CategoriesApi { | |||||||
|     return res.json(); |     return res.json(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async create(payload) { |   async create(payload) { // CreateTagRequest | ||||||
|     const res = await fetch(`${this.baseUrl}/Create`, { |     const res = await fetch(`${this.tagUrl}/Create`, { | ||||||
|       method: 'POST', |       method: 'POST', | ||||||
|       headers: this.headers(), |       headers: this.headers(), | ||||||
|       body: JSON.stringify(payload), |       body: JSON.stringify(payload), | ||||||
| @@ -32,8 +34,8 @@ export default class CategoriesApi { | |||||||
|     return res.json(); |     return res.json(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async update(payload) { |   async update(payload) { // UpdateTagRequest | ||||||
|     const res = await fetch(`${this.baseUrl}/Update`, { |     const res = await fetch(`${this.tagUrl}/Update`, { | ||||||
|       method: 'PUT', |       method: 'PUT', | ||||||
|       headers: this.headers(), |       headers: this.headers(), | ||||||
|       body: JSON.stringify(payload), |       body: JSON.stringify(payload), | ||||||
| @@ -42,8 +44,18 @@ export default class CategoriesApi { | |||||||
|     return res.json(); |     return res.json(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async changeStatus(payload) { // { id, status } | ||||||
|  |     const res = await fetch(`${this.tagUrl}/ChangeStatus`, { | ||||||
|  |       method: 'PATCH', | ||||||
|  |       headers: this.headers(), | ||||||
|  |       body: JSON.stringify(payload), | ||||||
|  |     }); | ||||||
|  |     if (!res.ok) throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`); | ||||||
|  |     return res.json(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async delete(payload) { |   async delete(payload) { | ||||||
|     const res = await fetch(`${this.baseUrl}/Delete`, { |     const res = await fetch(`${this.tagUrl}/Delete`, { | ||||||
|       method: 'DELETE', |       method: 'DELETE', | ||||||
|       headers: this.headers(), |       headers: this.headers(), | ||||||
|       body: JSON.stringify(payload), |       body: JSON.stringify(payload), | ||||||
| @@ -51,4 +63,14 @@ 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) | ||||||
|  |   async getAllTypes() { | ||||||
|  |     const res = await fetch(`${this.tagTypeUrl}/GetAll`, { | ||||||
|  |       method: 'GET', | ||||||
|  |       headers: this.headers(false), | ||||||
|  |     }); | ||||||
|  |     if (!res.ok) throw new Error(`GetAllTypes error ${res.status}: ${await res.text()}`); | ||||||
|  |     return res.json(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,56 +1,107 @@ | |||||||
| import { useEffect, useMemo, useState } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||||
| import { Box, Button, Paper, TextField, Typography } from '@mui/material'; | import { | ||||||
|  |   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'; | ||||||
|  |  | ||||||
|  | const toSlug = (s) => | ||||||
|  |   (s ?? '').toString().trim().toLowerCase() | ||||||
|  |     .normalize('NFD').replace(/[\u0300-\u036f]/g, '') | ||||||
|  |     .replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''); | ||||||
|  |  | ||||||
| export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) { | export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) { | ||||||
|   const { user } = useAuth(); |   const { user } = useAuth(); | ||||||
|   const token = user?.thalosToken || localStorage.getItem('thalosToken'); |   const token = user?.thalosToken || localStorage.getItem('thalosToken'); | ||||||
|   const api = useMemo(() => new CategoriesApi(token), [token]); |   const api = useMemo(() => new CategoriesApi(token), [token]); | ||||||
|  |  | ||||||
|  |   const [types, setTypes] = useState([]); | ||||||
|  |   const [allTags, setAllTags] = useState([]); | ||||||
|  |  | ||||||
|   const [category, setCategory] = useState({ |   const [category, setCategory] = useState({ | ||||||
|     _Id: '', |     _Id: '', | ||||||
|     id: '', |     id: '', | ||||||
|     name: '', |     name: '', | ||||||
|     description: '', |     slug: '', | ||||||
|  |     typeId: '', | ||||||
|  |     parentTagId: [],   // array<string> | ||||||
|  |     displayOrder: 0, | ||||||
|  |     icon: '', | ||||||
|     status: 'Active', |     status: 'Active', | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     // cargar Tag Types y Tags para selects | ||||||
|  |     (async () => { | ||||||
|  |       try { | ||||||
|  |         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); | ||||||
|  |       } | ||||||
|  |     })(); | ||||||
|  |   }, [api]); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (initialData) { |     if (initialData) { | ||||||
|       setCategory({ |       const _Id = initialData._id || initialData._Id || ''; | ||||||
|         _Id: initialData._id || initialData._Id || '', |       const id = initialData.id || initialData.Id || _Id || ''; | ||||||
|         id: initialData.id || initialData.Id || initialData._id || initialData._Id || '', |       const name = initialData.tagName ?? initialData.name ?? ''; | ||||||
|         name: initialData.name ?? '', |       const slug = initialData.slug ?? toSlug(name); | ||||||
|         description: initialData.description ?? '', |       const status = initialData.status ?? 'Active'; | ||||||
|         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 }); | ||||||
|     } else { |     } else { | ||||||
|       setCategory({ _Id: '', id: '', name: '', description: '', status: 'Active' }); |       setCategory({ | ||||||
|  |         _Id: '', id: '', name: '', slug: '', status: 'Active', | ||||||
|  |         typeId: '', parentTagId: [], displayOrder: 0, icon: '' | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   }, [initialData]); |   }, [initialData]); | ||||||
|  |  | ||||||
|   const handleChange = (e) => { |   const updateField = (name, value) => { | ||||||
|     const { name, value } = e.target; |  | ||||||
|     setCategory((prev) => ({ ...prev, [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 handleSubmit = async () => { |   const handleSubmit = async () => { | ||||||
|     try { |     try { | ||||||
|       if (category._Id) { |       if (category._Id) { | ||||||
|  |         // UpdateTagRequest | ||||||
|         const payload = { |         const payload = { | ||||||
|           _Id: category._Id, |           id: category.id || category._Id, | ||||||
|           Id: category.id || category._Id, |           tenantId: user?.tenantId ?? undefined, | ||||||
|           name: category.name, |           tagName: category.name, | ||||||
|           description: category.description, |           typeId: category.typeId || null, | ||||||
|           status: category.status, |           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 | ||||||
|         const payload = { |         const payload = { | ||||||
|           name: category.name, |           tenantId: user?.tenantId ?? undefined, | ||||||
|           description: category.description, |           tagName: category.name, | ||||||
|           status: category.status, |           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); |         await api.create(payload); | ||||||
|       } |       } | ||||||
| @@ -60,6 +111,18 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   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 }}> | ||||||
| @@ -68,23 +131,93 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|  |  | ||||||
|       <TextField |       <TextField | ||||||
|         name="name" |         name="name" | ||||||
|         label="Name" |         label="Name *" | ||||||
|         value={category.name} |         value={category.name} | ||||||
|         onChange={handleChange} |         onChange={(e) => updateField('name', e.target.value)} | ||||||
|         fullWidth |         fullWidth | ||||||
|         sx={{ mb: 2 }} |         sx={{ mb: 2 }} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <TextField |       <TextField | ||||||
|         name="description" |         name="slug" | ||||||
|         label="Description" |         label="Slug" | ||||||
|         value={category.description} |         value={category.slug} | ||||||
|         onChange={handleChange} |         onChange={(e) => updateField('slug', e.target.value)} | ||||||
|         fullWidth |         fullWidth | ||||||
|         multiline |         sx={{ mb: 2 }} | ||||||
|         minRows={3} |  | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|  |       <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" | ||||||
|  |         type="number" | ||||||
|  |         value={category.displayOrder} | ||||||
|  |         onChange={(e) => updateField('displayOrder', e.target.value)} | ||||||
|  |         fullWidth | ||||||
|  |         sx={{ mb: 2 }} | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <TextField | ||||||
|  |         name="icon" | ||||||
|  |         label="Icon (URL)" | ||||||
|  |         value={category.icon} | ||||||
|  |         onChange={(e) => updateField('icon', e.target.value)} | ||||||
|  |         fullWidth | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       {category._Id && ( | ||||||
|  |         <FormControl fullWidth sx={{ mt: 2 }}> | ||||||
|  |           <InputLabel id="status-label">Status</InputLabel> | ||||||
|  |           <Select | ||||||
|  |             labelId="status-label" | ||||||
|  |             label="Status" | ||||||
|  |             value={category.status} | ||||||
|  |             onChange={(e) => updateField('status', e.target.value)} | ||||||
|  |           > | ||||||
|  |             <MenuItem value="Active">Active</MenuItem> | ||||||
|  |             <MenuItem value="Inactive">Inactive</MenuItem> | ||||||
|  |           </Select> | ||||||
|  |         </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> | ||||||
|         <Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button> |         <Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button> | ||||||
|   | |||||||
| @@ -47,8 +47,12 @@ 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.name ?? '', |       name: r.tagName ?? r.name ?? '', | ||||||
|       description: r.description ?? '', |       slug: r.slug ?? '', | ||||||
|  |       typeId: r.typeId ?? '', | ||||||
|  |       parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [], | ||||||
|  |       displayOrder: Number.isFinite(r.displayOrder) ? r.displayOrder : 0, | ||||||
|  |       icon: r.icon ?? '', | ||||||
|       status: r.status ?? 'Active', |       status: r.status ?? 'Active', | ||||||
|     }); |     }); | ||||||
|     setOpen(true); |     setOpen(true); | ||||||
| @@ -62,14 +66,10 @@ export default function Categories() { | |||||||
|   const confirmDelete = async () => { |   const confirmDelete = async () => { | ||||||
|     try { |     try { | ||||||
|       if (!rowToDelete) return; |       if (!rowToDelete) return; | ||||||
|       const payload = { |       await api.changeStatus({ | ||||||
|         _Id: rowToDelete._id || rowToDelete._Id, |         id: rowToDelete.id || rowToDelete.Id || rowToDelete._id || rowToDelete._Id, | ||||||
|         id: rowToDelete.id || rowToDelete.Id || '', |         status: 'Inactive', | ||||||
|         name: rowToDelete.name, |       }); | ||||||
|         description: rowToDelete.description, |  | ||||||
|         status: 'Inactive', // soft-delete |  | ||||||
|       }; |  | ||||||
|       await api.update(payload); |  | ||||||
|       await loadData(); |       await loadData(); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       console.error('Delete failed:', e); |       console.error('Delete failed:', e); | ||||||
| @@ -86,8 +86,9 @@ export default function Categories() { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const columns = [ |   const columns = [ | ||||||
|     { field: 'name', headerName: 'Name', flex: 1, minWidth: 200 }, |     { field: 'tagName', headerName: 'Name', flex: 1, minWidth: 200, valueGetter: (p) => p.row?.tagName ?? p.row?.name }, | ||||||
|     { field: 'description', headerName: 'Description', flex: 1, minWidth: 250 }, |     { 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: 'status', headerName: 'Status', width: 140, valueGetter: (p) => p.row?.status ?? 'Active' }, | ||||||
|     { |     { | ||||||
|       field: 'actions', |       field: 'actions', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user