Compare commits
	
		
			2 Commits
		
	
	
		
			fead820091
			...
			aa62b06c23
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | aa62b06c23 | ||
|   | d699af9d75 | 
| @@ -1,4 +1,3 @@ | |||||||
| // src/private/categories/AddOrEditCategoryForm.jsx |  | ||||||
| import { useEffect, useMemo, useState } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||||
| import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material'; | import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material'; | ||||||
| import { useAuth } from '../../context/AuthContext'; | import { useAuth } from '../../context/AuthContext'; | ||||||
| @@ -18,7 +17,6 @@ function extractTenantId(token) { | |||||||
|     const payload = jwtDecode(token); |     const payload = jwtDecode(token); | ||||||
|     const t = payload?.tenant; |     const t = payload?.tenant; | ||||||
|     if (Array.isArray(t)) { |     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)); |       const hex = t.find(x => typeof x === 'string' && /^[0-9a-f]{24}$/i.test(x)); | ||||||
|       return hex || (typeof t[0] === 'string' ? t[0] : ''); |       return hex || (typeof t[0] === 'string' ? t[0] : ''); | ||||||
|     } |     } | ||||||
| @@ -27,7 +25,7 @@ function extractTenantId(token) { | |||||||
|   return ''; |   return ''; | ||||||
| } | } | ||||||
|  |  | ||||||
| export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) { | export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel, materials: materialsProp = [], initialMaterialNames = [] }) { | ||||||
|   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]); | ||||||
| @@ -35,6 +33,15 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|   const [types, setTypes] = useState([]); |   const [types, setTypes] = useState([]); | ||||||
|   const [allTags, setAllTags] = useState([]); |   const [allTags, setAllTags] = useState([]); | ||||||
|  |  | ||||||
|  | const tagLabelById = useMemo(() => { | ||||||
|  |   const map = {}; | ||||||
|  |   for (const t of allTags) { | ||||||
|  |     const key = t._id || t.id; | ||||||
|  |     map[key] = t.tagName || t.name || key; | ||||||
|  |   } | ||||||
|  |   return map; | ||||||
|  | }, [allTags]); | ||||||
|  |  | ||||||
|   const [form, setForm] = useState({ |   const [form, setForm] = useState({ | ||||||
|     _Id: '', |     _Id: '', | ||||||
|     id: '', |     id: '', | ||||||
| @@ -56,15 +63,58 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     (async () => { |     (async () => { | ||||||
|       try { |       try { | ||||||
|         const [t, tags] = await Promise.all([api.getAllTypes(), api.getAll()]); |         // Always try to load all tags (for materials, lookups, etc.) | ||||||
|         setTypes(Array.isArray(t) ? t : []); |         const tags = typeof api.getAll === 'function' ? await api.getAll() : []; | ||||||
|  |  | ||||||
|  |         // Try multiple method names for types; if none exist, derive from tags | ||||||
|  |         let typesResp = []; | ||||||
|  |         if (typeof api.getAllTypes === 'function') { | ||||||
|  |           typesResp = await api.getAllTypes(); | ||||||
|  |         } else if (typeof api.getTypes === 'function') { | ||||||
|  |           typesResp = await api.getTypes(); | ||||||
|  |         } else if (Array.isArray(tags)) { | ||||||
|  |           // Derive a minimal "types" list from existing tag.typeId values | ||||||
|  |           const uniqueTypeIds = [...new Set(tags.map(t => t?.typeId).filter(Boolean))]; | ||||||
|  |           typesResp = uniqueTypeIds.map(id => ({ id, typeName: id, level: null })); | ||||||
|  |           console.warn('CategoriesApi has no getAllTypes/getTypes; derived types from tag.typeId.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         setTypes(Array.isArray(typesResp) ? typesResp : []); | ||||||
|         setAllTags(Array.isArray(tags) ? tags : []); |         setAllTags(Array.isArray(tags) ? tags : []); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         console.error('Failed to load tag types or tags', e); |         console.error('Failed to load tag types or tags', e); | ||||||
|  |         setTypes([]); | ||||||
|  |         setAllTags([]); | ||||||
|       } |       } | ||||||
|     })(); |     })(); | ||||||
|   }, [api]); |   }, [api]); | ||||||
|  |  | ||||||
|  |   // When editing: if we received material names from the grid, map them to IDs once allTags are loaded. | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!Array.isArray(initialMaterialNames) || initialMaterialNames.length === 0) return; | ||||||
|  |     // If parentTagId already has values (ids), do not override. | ||||||
|  |     if (Array.isArray(form.parentTagId) && form.parentTagId.length > 0) return; | ||||||
|  |     if (!Array.isArray(allTags) || allTags.length === 0) return; | ||||||
|  |  | ||||||
|  |     // Build a case-insensitive name -> id map | ||||||
|  |     const nameToId = new Map( | ||||||
|  |       allTags.map(t => { | ||||||
|  |         const id = t._id || t.id; | ||||||
|  |         const label = (t.tagName || t.name || '').toLowerCase(); | ||||||
|  |         return [label, id]; | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const ids = initialMaterialNames | ||||||
|  |       .map(n => (typeof n === 'string' ? n.toLowerCase() : '')) | ||||||
|  |       .map(lower => nameToId.get(lower)) | ||||||
|  |       .filter(Boolean); | ||||||
|  |  | ||||||
|  |     if (ids.length > 0) { | ||||||
|  |       setForm(prev => ({ ...prev, parentTagId: ids })); | ||||||
|  |     } | ||||||
|  |   }, [initialMaterialNames, allTags]); | ||||||
|  |  | ||||||
|   // set inicial |   // set inicial | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (initialData) { |     if (initialData) { | ||||||
| @@ -106,6 +156,9 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|     } |     } | ||||||
|   }, [initialData]); |   }, [initialData]); | ||||||
|  |  | ||||||
|  |   const isEdit = Boolean(form._Id || form.id); | ||||||
|  |   const isAdd = !isEdit; | ||||||
|  |  | ||||||
|   const setVal = (name, value) => setForm(p => ({ ...p, [name]: value })); |   const setVal = (name, value) => setForm(p => ({ ...p, [name]: value })); | ||||||
|  |  | ||||||
|   const handleChange = (e) => { |   const handleChange = (e) => { | ||||||
| @@ -180,15 +233,7 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|         {form._Id ? 'Edit Category' : 'Add Category'} |         {form._Id ? 'Edit Category' : 'Add Category'} | ||||||
|       </Typography> |       </Typography> | ||||||
|  |  | ||||||
|       {/* IDs (read-only) */} |       {isAdd && ( | ||||||
|       {form._Id || form.id ? ( |  | ||||||
|         <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mb: 2 }}> |  | ||||||
|           <TextField label="_Id" value={form._Id} InputProps={{ readOnly: true }} fullWidth /> |  | ||||||
|           <TextField label="id" value={form.id} InputProps={{ readOnly: true }} fullWidth /> |  | ||||||
|         </Box> |  | ||||||
|       ) : null} |  | ||||||
|  |  | ||||||
|       {/* Tenant (read-only; comes from token or existing record) */} |  | ||||||
|         <TextField |         <TextField | ||||||
|           name="tenantId" |           name="tenantId" | ||||||
|           label="Tenant Id" |           label="Tenant Id" | ||||||
| @@ -197,6 +242,7 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|           sx={{ mb: 2 }} |           sx={{ mb: 2 }} | ||||||
|           InputProps={{ readOnly: true }} |           InputProps={{ readOnly: true }} | ||||||
|         /> |         /> | ||||||
|  |       )} | ||||||
|  |  | ||||||
|       <TextField |       <TextField | ||||||
|         name="tagName" |         name="tagName" | ||||||
| @@ -227,25 +273,35 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) | |||||||
|  |  | ||||||
|       <TextField |       <TextField | ||||||
|         name="parentTagId" |         name="parentTagId" | ||||||
|         label="Parent tags" |         label="Material" | ||||||
|         value={form.parentTagId} |         value={form.parentTagId} | ||||||
|         onChange={(e) => { |         onChange={(e) => { | ||||||
|  |           // For MUI Select multiple, e.target.value is an array of selected IDs | ||||||
|           const val = e.target.value; |           const val = e.target.value; | ||||||
|           setVal('parentTagId', typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : val); |           setVal('parentTagId', Array.isArray(val) ? val : []); | ||||||
|         }} |         }} | ||||||
|         select |         select | ||||||
|         SelectProps={{ multiple: true, renderValue: (selected) => ( |         SelectProps={{ | ||||||
|  |           multiple: true, | ||||||
|  |           renderValue: (selected) => ( | ||||||
|             <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}> |             <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}> | ||||||
|             {selected.map(v => <Chip key={v} label={v} size="small" />)} |               {selected.map((id) => ( | ||||||
|  |                 <Chip key={id} label={tagLabelById[id] || id} size="small" /> | ||||||
|  |               ))} | ||||||
|             </Box> |             </Box> | ||||||
|         )}} |           ), | ||||||
|  |         }} | ||||||
|         fullWidth |         fullWidth | ||||||
|         sx={{ mb: 2 }} |         sx={{ mb: 2 }} | ||||||
|       > |       > | ||||||
|         {allTags.map(t => { |         {allTags.map((t) => { | ||||||
|           const value = t._id || t.id; |           const value = t._id || t.id; | ||||||
|           const label = t.tagName || t.name || value; |           const label = t.tagName || t.name || value; | ||||||
|         return <MenuItem key={value} value={value}>{label}</MenuItem>; |           return ( | ||||||
|  |             <MenuItem key={value} value={value}> | ||||||
|  |               {label} | ||||||
|  |             </MenuItem> | ||||||
|  |           ); | ||||||
|         })} |         })} | ||||||
|       </TextField> |       </TextField> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import { useEffect, useMemo, useRef, useState } from 'react'; | import { useEffect, useMemo, useRef, useState } from 'react'; | ||||||
| import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography, | import { | ||||||
|          ToggleButton, ToggleButtonGroup } from '@mui/material'; |   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 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'; | ||||||
| @@ -14,6 +16,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 [allTags, setAllTags] = useState([]); | ||||||
|   const [statusFilter, setStatusFilter] = useState('All'); // <- por defecto All |   const [statusFilter, setStatusFilter] = useState('All'); // <- por defecto All | ||||||
|   const [open, setOpen] = useState(false); |   const [open, setOpen] = useState(false); | ||||||
|   const [editingCategory, setEditingCategory] = useState(null); |   const [editingCategory, setEditingCategory] = useState(null); | ||||||
| @@ -35,6 +38,8 @@ export default function Categories() { | |||||||
|       const data = await api.getAll(); |       const data = await api.getAll(); | ||||||
|       const list = Array.isArray(data) ? data : []; |       const list = Array.isArray(data) ? data : []; | ||||||
|  |  | ||||||
|  |       setAllTags(list); | ||||||
|  |  | ||||||
|       // Build a map of parentId -> array of child tagNames |       // Build a map of parentId -> array of child tagNames | ||||||
|       const parentToChildren = {}; |       const parentToChildren = {}; | ||||||
|       for (const item of list) { |       for (const item of list) { | ||||||
| @@ -76,6 +81,9 @@ export default function Categories() { | |||||||
|       displayOrder: Number(r.displayOrder ?? 0), |       displayOrder: Number(r.displayOrder ?? 0), | ||||||
|       icon: r.icon || '', |       icon: r.icon || '', | ||||||
|       status: r.status ?? 'Active', |       status: r.status ?? 'Active', | ||||||
|  |       materialNames: typeof r.material === 'string' | ||||||
|  |         ? r.material.split(',').map(s => s.trim()).filter(Boolean) | ||||||
|  |         : Array.isArray(r.material) ? r.material : [], | ||||||
|     }); |     }); | ||||||
|     setOpen(true); |     setOpen(true); | ||||||
|   }; |   }; | ||||||
| @@ -164,10 +172,8 @@ export default function Categories() { | |||||||
|     }, |     }, | ||||||
|     { field: 'tagName', headerName: 'Name', flex: 1.2, minWidth: 180 }, |     { field: 'tagName', headerName: 'Name', flex: 1.2, minWidth: 180 }, | ||||||
|     { field: 'slug', headerName: 'Slug', flex: 1.0, minWidth: 160 }, |     { field: 'slug', headerName: 'Slug', flex: 1.0, minWidth: 160 }, | ||||||
|     { field: 'icon', headerName: 'Icon', flex: 0.7, minWidth: 120 }, |     { field: 'icon', headerName: 'Icon', flex: 0.7, minWidth: 250 }, | ||||||
|     // New computed column |  | ||||||
|     { field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 }, |     { field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 }, | ||||||
|     // Hidden audit columns |  | ||||||
|     { |     { | ||||||
|       field: 'createdAt', |       field: 'createdAt', | ||||||
|       headerName: 'Created Date', |       headerName: 'Created Date', | ||||||
| @@ -229,7 +235,17 @@ export default function Categories() { | |||||||
|         <DataGrid |         <DataGrid | ||||||
|           rows={filteredRows} |           rows={filteredRows} | ||||||
|           columns={columns} |           columns={columns} | ||||||
|           initialState={{ pagination: { paginationModel: { pageSize: pageSize } } }} |           initialState={{ | ||||||
|  |             pagination: { paginationModel: { pageSize } }, | ||||||
|  |             columns: { | ||||||
|  |               columnVisibilityModel: { | ||||||
|  |                 createdAt: false, | ||||||
|  |                 createdBy: false, | ||||||
|  |                 updatedAt: false, | ||||||
|  |                 updatedBy: false, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }} | ||||||
|           pageSizeOptions={[pageSize]} |           pageSizeOptions={[pageSize]} | ||||||
|           disableColumnMenu |           disableColumnMenu | ||||||
|           getRowId={(r) => r?._id || r?.id} |           getRowId={(r) => r?._id || r?.id} | ||||||
| @@ -256,6 +272,8 @@ export default function Categories() { | |||||||
|         <DialogContent> |         <DialogContent> | ||||||
|           <AddOrEditCategoryForm |           <AddOrEditCategoryForm | ||||||
|             initialData={editingCategory} |             initialData={editingCategory} | ||||||
|  |             allTags={allTags} | ||||||
|  |             initialMaterialNames={editingCategory?.materialNames || []} | ||||||
|             onAdd={handleFormDone} |             onAdd={handleFormDone} | ||||||
|             onCancel={() => { setOpen(false); setEditingCategory(null); }} |             onCancel={() => { setOpen(false); setEditingCategory(null); }} | ||||||
|           /> |           /> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user