277 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { useEffect, useMemo, useRef, useState } from 'react';
 | |
| import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography,
 | |
|          ToggleButton, ToggleButtonGroup } from '@mui/material';
 | |
| import { DataGrid } from '@mui/x-data-grid';
 | |
| import EditRoundedIcon from '@mui/icons-material/EditRounded';
 | |
| import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
 | |
| import AddOrEditCategoryForm from './AddOrEditCategoryForm';
 | |
| import CategoriesApi from '../../api/CategoriesApi';
 | |
| import { useAuth } from '../../context/AuthContext';
 | |
| 
 | |
| export default function Categories() {
 | |
|   const { user } = useAuth();
 | |
|   const token = user?.thalosToken || localStorage.getItem('thalosToken');
 | |
|   const api = useMemo(() => new CategoriesApi(token), [token]);
 | |
| 
 | |
|   const [rows, setRows] = useState([]);
 | |
|   const [statusFilter, setStatusFilter] = useState('All'); // <- por defecto All
 | |
|   const [open, setOpen] = useState(false);
 | |
|   const [editingCategory, setEditingCategory] = useState(null);
 | |
|   const [confirmOpen, setConfirmOpen] = useState(false);
 | |
|   const [rowToDelete, setRowToDelete] = useState(null);
 | |
|   const hasLoaded = useRef(false);
 | |
| 
 | |
|   const pageSize = 100; // Número de filas por página
 | |
| 
 | |
|   useEffect(() => {
 | |
|     if (!hasLoaded.current) {
 | |
|       loadData();
 | |
|       hasLoaded.current = true;
 | |
|     }
 | |
|   }, []);
 | |
| 
 | |
|   const loadData = async () => {
 | |
|     try {
 | |
|       const data = await api.getAll();
 | |
|       const list = Array.isArray(data) ? data : [];
 | |
| 
 | |
|       // Build a map of parentId -> array of child tagNames
 | |
|       const parentToChildren = {};
 | |
|       for (const item of list) {
 | |
|         const parents = Array.isArray(item?.parentTagId) ? item.parentTagId : [];
 | |
|         for (const pid of parents) {
 | |
|           if (!parentToChildren[pid]) parentToChildren[pid] = [];
 | |
|           if (item?.tagName) parentToChildren[pid].push(item.tagName);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // Enrich each row with `material` (children whose parentTagId includes this _id)
 | |
|       const enriched = list.map((r) => ({
 | |
|         ...r,
 | |
|         material: Array.isArray(parentToChildren[r?._id]) ? parentToChildren[r._id].join(', ') : '',
 | |
|       }));
 | |
| 
 | |
|       setRows(enriched);
 | |
|     } catch (e) {
 | |
|       console.error('Failed to load categories:', e);
 | |
|       setRows([]);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const handleAddClick = () => {
 | |
|     setEditingCategory(null);
 | |
|     setOpen(true);
 | |
|   };
 | |
| 
 | |
|   const handleEditClick = (params) => {
 | |
|     const r = params?.row;
 | |
|     if (!r) return;
 | |
|     setEditingCategory({
 | |
|       _Id: r._id || r._Id || '',
 | |
|       id: r.id || r.Id || '',
 | |
|       tagName: r.tagName || r.name || '',
 | |
|       typeId: r.typeId || '',
 | |
|       parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [],
 | |
|       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;
 | |
|       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);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const handleFormDone = async () => {
 | |
|     await loadData();
 | |
|     setOpen(false);
 | |
|     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: 'actions',
 | |
|       headerName: '',
 | |
|       width: 130,
 | |
|       sortable: false,
 | |
|       filterable: false,
 | |
|       disableExport: true,
 | |
|       renderCell: (params) => (
 | |
|         <Box display="flex" alignItems="center" justifyContent="flex-start" height="100%" gap={1}>
 | |
|           <IconButton
 | |
|             size="small"
 | |
|             sx={{
 | |
|               backgroundColor: '#DFCCBC',
 | |
|               color: '#26201A',
 | |
|               '&:hover': { backgroundColor: '#C2B2A4' },
 | |
|               borderRadius: 2,
 | |
|               p: 1,
 | |
|             }}
 | |
|             onClick={() => handleEditClick(params)}
 | |
|           >
 | |
|             <EditRoundedIcon fontSize="small" />
 | |
|           </IconButton>
 | |
|           <IconButton
 | |
|             size="small"
 | |
|             sx={{
 | |
|               backgroundColor: '#FBE9E7',
 | |
|               color: '#C62828',
 | |
|               '&:hover': { backgroundColor: '#EF9A9A' },
 | |
|               borderRadius: 2,
 | |
|               p: 1,
 | |
|             }}
 | |
|             onClick={() => handleDeleteClick(params?.row)}
 | |
|           >
 | |
|             <DeleteRoundedIcon fontSize="small" />
 | |
|           </IconButton>
 | |
|         </Box>
 | |
|       ),
 | |
|     },
 | |
|     { field: 'tagName', headerName: 'Name', flex: 1.2, minWidth: 180 },
 | |
|     { field: 'slug', headerName: 'Slug', flex: 1.0, minWidth: 160 },
 | |
|     { field: 'icon', headerName: 'Icon', flex: 0.7, minWidth: 120 },
 | |
|     // New computed column
 | |
|     { field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 },
 | |
|     // Hidden audit columns
 | |
|     {
 | |
|       field: 'createdAt',
 | |
|       headerName: 'Created Date',
 | |
|       flex: 1.0,
 | |
|       minWidth: 180,
 | |
|       hide: true,
 | |
|       valueFormatter: (p) => {
 | |
|         const v = p?.value;
 | |
|         return v ? new Date(v).toLocaleString() : '—';
 | |
|       },
 | |
|     },
 | |
|     { field: 'createdBy', headerName: 'Created By', flex: 0.9, minWidth: 160, hide: true },
 | |
|     {
 | |
|       field: 'updatedAt',
 | |
|       headerName: 'Updated Date',
 | |
|       flex: 1.0,
 | |
|       minWidth: 180,
 | |
|       hide: true,
 | |
|       valueFormatter: (p) => {
 | |
|         const v = p?.value;
 | |
|         return v ? new Date(v).toLocaleString() : '—';
 | |
|       },
 | |
|     },
 | |
|     { field: 'updatedBy', headerName: 'Updated By', flex: 0.9, minWidth: 160, hide: true },
 | |
|     { field: 'status', headerName: 'Status', flex: 0.7, minWidth: 120 },
 | |
|   ];
 | |
| 
 | |
|   return (
 | |
|     <Box
 | |
|       sx={{
 | |
|         height: 'calc(100vh - 64px - 64px)',
 | |
|         display: 'flex',
 | |
|         flexDirection: 'column',
 | |
|         gap: 2,
 | |
|       }}
 | |
|     >
 | |
|       <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
 | |
|         <Typography variant="h6">Categories</Typography>
 | |
| 
 | |
|         <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 sx={{ flex: 1, minHeight: 0 }}>
 | |
|         <DataGrid
 | |
|           rows={filteredRows}
 | |
|           columns={columns}
 | |
|           initialState={{ pagination: { paginationModel: { pageSize: pageSize } } }}
 | |
|           pageSizeOptions={[pageSize]}
 | |
|           disableColumnMenu
 | |
|           getRowId={(r) => r?._id || r?.id}
 | |
|           sx={{
 | |
|             height: '100%',
 | |
|             '& .MuiDataGrid-cell, & .MuiDataGrid-columnHeader': {
 | |
|               display: 'flex',
 | |
|               alignItems: 'center',
 | |
|             },
 | |
|             '& .MuiDataGrid-filler': {
 | |
|               display: 'none',
 | |
|             },
 | |
|           }}
 | |
|           slots={{
 | |
|             noRowsOverlay: () => (
 | |
|               <Box sx={{ p: 2 }}>No categories found. Try switching the status filter to "All".</Box>
 | |
|             ),
 | |
|           }}
 | |
|         />
 | |
|       </Box>
 | |
| 
 | |
|       <Dialog open={open} onClose={() => { setOpen(false); setEditingCategory(null); }} fullWidth>
 | |
|         <DialogTitle>{editingCategory ? 'Edit Category' : 'Add Category'}</DialogTitle>
 | |
|         <DialogContent>
 | |
|           <AddOrEditCategoryForm
 | |
|             initialData={editingCategory}
 | |
|             onAdd={handleFormDone}
 | |
|             onCancel={() => { setOpen(false); setEditingCategory(null); }}
 | |
|           />
 | |
|         </DialogContent>
 | |
|       </Dialog>
 | |
| 
 | |
|       <Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
 | |
|         <DialogTitle>Delete Category</DialogTitle>
 | |
|         <DialogContent>
 | |
|           <Box sx={{ display: 'flex', gap: 1, mt: 2, mb: 1 }}>
 | |
|             <Button onClick={() => setConfirmOpen(false)}>Cancel</Button>
 | |
|             <Button color="error" variant="contained" onClick={confirmDelete}>Delete</Button>
 | |
|           </Box>
 | |
|         </DialogContent>
 | |
|       </Dialog>
 | |
|     </Box>
 | |
|   );
 | |
| }
 | 
