feat: tags fixes
This commit is contained in:
		| @@ -1,7 +1,8 @@ | ||||
| // src/api/CategoriesApi.js | ||||
| export default class CategoriesApi { | ||||
|   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; | ||||
|   } | ||||
|  | ||||
| @@ -13,8 +14,9 @@ export default class CategoriesApi { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // TAGS | ||||
|   async getAll() { | ||||
|     const res = await fetch(`${this.baseUrl}/GetAll`, { | ||||
|     const res = await fetch(`${this.tagUrl}/GetAll`, { | ||||
|       method: 'GET', | ||||
|       headers: this.headers(false), | ||||
|     }); | ||||
| @@ -22,8 +24,8 @@ export default class CategoriesApi { | ||||
|     return res.json(); | ||||
|   } | ||||
|  | ||||
|   async create(payload) { | ||||
|     const res = await fetch(`${this.baseUrl}/Create`, { | ||||
|   async create(payload) { // CreateTagRequest | ||||
|     const res = await fetch(`${this.tagUrl}/Create`, { | ||||
|       method: 'POST', | ||||
|       headers: this.headers(), | ||||
|       body: JSON.stringify(payload), | ||||
| @@ -32,8 +34,8 @@ export default class CategoriesApi { | ||||
|     return res.json(); | ||||
|   } | ||||
|  | ||||
|   async update(payload) { | ||||
|     const res = await fetch(`${this.baseUrl}/Update`, { | ||||
|   async update(payload) { // UpdateTagRequest | ||||
|     const res = await fetch(`${this.tagUrl}/Update`, { | ||||
|       method: 'PUT', | ||||
|       headers: this.headers(), | ||||
|       body: JSON.stringify(payload), | ||||
| @@ -42,8 +44,18 @@ export default class CategoriesApi { | ||||
|     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) { | ||||
|     const res = await fetch(`${this.baseUrl}/Delete`, { | ||||
|     const res = await fetch(`${this.tagUrl}/Delete`, { | ||||
|       method: 'DELETE', | ||||
|       headers: this.headers(), | ||||
|       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()}`); | ||||
|     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 { 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 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 }) { | ||||
|   const { user } = useAuth(); | ||||
|   const token = user?.thalosToken || localStorage.getItem('thalosToken'); | ||||
|   const api = useMemo(() => new CategoriesApi(token), [token]); | ||||
|  | ||||
|   const [types, setTypes] = useState([]); | ||||
|   const [allTags, setAllTags] = useState([]); | ||||
|  | ||||
|   const [category, setCategory] = useState({ | ||||
|     _Id: '', | ||||
|     id: '', | ||||
|     name: '', | ||||
|     description: '', | ||||
|     slug: '', | ||||
|     typeId: '', | ||||
|     parentTagId: [],   // array<string> | ||||
|     displayOrder: 0, | ||||
|     icon: '', | ||||
|     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(() => { | ||||
|     if (initialData) { | ||||
|       setCategory({ | ||||
|         _Id: initialData._id || initialData._Id || '', | ||||
|         id: initialData.id || initialData.Id || initialData._id || initialData._Id || '', | ||||
|         name: initialData.name ?? '', | ||||
|         description: initialData.description ?? '', | ||||
|         status: initialData.status ?? 'Active', | ||||
|       }); | ||||
|       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 }); | ||||
|     } else { | ||||
|       setCategory({ _Id: '', id: '', name: '', description: '', status: 'Active' }); | ||||
|       setCategory({ | ||||
|         _Id: '', id: '', name: '', slug: '', status: 'Active', | ||||
|         typeId: '', parentTagId: [], displayOrder: 0, icon: '' | ||||
|       }); | ||||
|     } | ||||
|   }, [initialData]); | ||||
|  | ||||
|   const handleChange = (e) => { | ||||
|     const { name, value } = e.target; | ||||
|   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 handleSubmit = async () => { | ||||
|     try { | ||||
|       if (category._Id) { | ||||
|         // UpdateTagRequest | ||||
|         const payload = { | ||||
|           _Id: category._Id, | ||||
|           Id: category.id || category._Id, | ||||
|           name: category.name, | ||||
|           description: category.description, | ||||
|           status: category.status, | ||||
|           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', | ||||
|         }; | ||||
|         await api.update(payload); | ||||
|       } else { | ||||
|         // CreateTagRequest | ||||
|         const payload = { | ||||
|           name: category.name, | ||||
|           description: category.description, | ||||
|           status: category.status, | ||||
|           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); | ||||
|       } | ||||
| @@ -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 ( | ||||
|     <Paper sx={{ p: 2 }}> | ||||
|       <Typography variant="subtitle1" sx={{ mb: 2 }}> | ||||
| @@ -68,23 +131,93 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | ||||
|  | ||||
|       <TextField | ||||
|         name="name" | ||||
|         label="Name" | ||||
|         label="Name *" | ||||
|         value={category.name} | ||||
|         onChange={handleChange} | ||||
|         onChange={(e) => updateField('name', e.target.value)} | ||||
|         fullWidth | ||||
|         sx={{ mb: 2 }} | ||||
|       /> | ||||
|  | ||||
|       <TextField | ||||
|         name="description" | ||||
|         label="Description" | ||||
|         value={category.description} | ||||
|         onChange={handleChange} | ||||
|         name="slug" | ||||
|         label="Slug" | ||||
|         value={category.slug} | ||||
|         onChange={(e) => updateField('slug', e.target.value)} | ||||
|         fullWidth | ||||
|         multiline | ||||
|         minRows={3} | ||||
|         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" | ||||
|         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}> | ||||
|         <Button onClick={onCancel} className="button-transparent">Cancel</Button> | ||||
|         <Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button> | ||||
|   | ||||
| @@ -47,8 +47,12 @@ export default function Categories() { | ||||
|     setEditingCategory({ | ||||
|       _Id: r._id || r._Id || '', | ||||
|       id: r.id || r.Id || '', | ||||
|       name: r.name ?? '', | ||||
|       description: r.description ?? '', | ||||
|       name: r.tagName ?? r.name ?? '', | ||||
|       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', | ||||
|     }); | ||||
|     setOpen(true); | ||||
| @@ -62,14 +66,10 @@ export default function Categories() { | ||||
|   const confirmDelete = async () => { | ||||
|     try { | ||||
|       if (!rowToDelete) return; | ||||
|       const payload = { | ||||
|         _Id: rowToDelete._id || rowToDelete._Id, | ||||
|         id: rowToDelete.id || rowToDelete.Id || '', | ||||
|         name: rowToDelete.name, | ||||
|         description: rowToDelete.description, | ||||
|         status: 'Inactive', // soft-delete | ||||
|       }; | ||||
|       await api.update(payload); | ||||
|       await api.changeStatus({ | ||||
|         id: rowToDelete.id || rowToDelete.Id || rowToDelete._id || rowToDelete._Id, | ||||
|         status: 'Inactive', | ||||
|       }); | ||||
|       await loadData(); | ||||
|     } catch (e) { | ||||
|       console.error('Delete failed:', e); | ||||
| @@ -86,8 +86,9 @@ export default function Categories() { | ||||
|   }; | ||||
|  | ||||
|   const columns = [ | ||||
|     { field: 'name', headerName: 'Name', flex: 1, minWidth: 200 }, | ||||
|     { field: 'description', headerName: 'Description', flex: 1, minWidth: 250 }, | ||||
|     { 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: 'actions', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user