Files
fendi-react-app/src/private/categories/Categories.jsx
2025-09-04 20:51:46 -06:00

285 lines
9.0 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: 250 },
{ field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 },
{
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 } },
columns: {
columnVisibilityModel: {
createdAt: false,
createdBy: false,
updatedAt: false,
updatedBy: false,
},
},
}}
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>
);
}