285 lines
9.0 KiB
JavaScript
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>
|
|
);
|
|
}
|