chore: edit existing item

This commit is contained in:
Rodolfo Ruiz
2025-09-01 20:53:04 -06:00
parent 24f82889e1
commit 304d5a6e59
2 changed files with 158 additions and 57 deletions

View File

@@ -38,6 +38,7 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
const [form, setForm] = useState({ const [form, setForm] = useState({
_Id: '', _Id: '',
id: '', id: '',
tenantId: '',
tagName: '', tagName: '',
typeId: '', typeId: '',
parentTagId: [], parentTagId: [],
@@ -45,6 +46,10 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
displayOrder: 0, displayOrder: 0,
icon: '', icon: '',
status: 'Active', status: 'Active',
createdAt: null,
createdBy: null,
updatedAt: null,
updatedBy: null,
}); });
// cargar tipos y tags para selects // cargar tipos y tags para selects
@@ -68,6 +73,7 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
setForm({ setForm({
_Id, _Id,
id, id,
tenantId: initialData.tenantId || extractTenantId(token) || '',
tagName: initialData.tagName || initialData.name || '', tagName: initialData.tagName || initialData.name || '',
typeId: initialData.typeId || '', typeId: initialData.typeId || '',
parentTagId: Array.isArray(initialData.parentTagId) ? initialData.parentTagId : [], parentTagId: Array.isArray(initialData.parentTagId) ? initialData.parentTagId : [],
@@ -75,11 +81,16 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
displayOrder: Number(initialData.displayOrder ?? 0), displayOrder: Number(initialData.displayOrder ?? 0),
icon: initialData.icon || '', icon: initialData.icon || '',
status: initialData.status || 'Active', status: initialData.status || 'Active',
createdAt: initialData.createdAt ?? null,
createdBy: initialData.createdBy ?? null,
updatedAt: initialData.updatedAt ?? null,
updatedBy: initialData.updatedBy ?? null,
}); });
} else { } else {
setForm({ setForm({
_Id: '', _Id: '',
id: '', id: '',
tenantId: extractTenantId(token) || '',
tagName: '', tagName: '',
typeId: '', typeId: '',
parentTagId: [], parentTagId: [],
@@ -87,6 +98,10 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
displayOrder: 0, displayOrder: 0,
icon: '', icon: '',
status: 'Active', status: 'Active',
createdAt: null,
createdBy: null,
updatedAt: null,
updatedBy: null,
}); });
} }
}, [initialData]); }, [initialData]);
@@ -140,12 +155,44 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
} }
}; };
const handleDelete = async () => {
try {
// Try to use Mongo _Id (24-hex); if not present, fall back to GUID `id`.
const hex = typeof form._Id === 'string' && /^[0-9a-f]{24}$/i.test(form._Id) ? form._Id : null;
const idToUse = hex || form.id;
if (!idToUse) throw new Error('Missing id to delete');
await api.changeStatus({ id: idToUse, status: 'Inactive' });
onAdd?.();
} catch (e) {
console.error('Delete category failed:', e);
alert(e.message || 'Delete failed');
}
};
return ( return (
<Paper sx={{ p: 2 }}> <Paper sx={{ p: 2 }}>
<Typography variant="subtitle1" sx={{ mb: 2 }}> <Typography variant="subtitle1" sx={{ mb: 2 }}>
{form._Id ? 'Edit Category' : 'Add Category'} {form._Id ? 'Edit Category' : 'Add Category'}
</Typography> </Typography>
{/* IDs (read-only) */}
{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
name="tenantId"
label="Tenant Id"
value={form.tenantId}
fullWidth
sx={{ mb: 2 }}
InputProps={{ readOnly: true }}
/>
<TextField <TextField
name="tagName" name="tagName"
label="Name" label="Name"
@@ -239,9 +286,23 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
<MenuItem value="Inactive">Inactive</MenuItem> <MenuItem value="Inactive">Inactive</MenuItem>
</TextField> </TextField>
<Box display="flex" justifyContent="flex-end" gap={1} mt={3}> {form._Id || form.id ? (
<Button onClick={onCancel} className="button-transparent">Cancel</Button> <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mt: 2 }}>
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button> <TextField label="Created At" value={form.createdAt ? new Date(form.createdAt).toLocaleString() : '—'} InputProps={{ readOnly: true }} fullWidth />
<TextField label="Created By" value={form.createdBy ?? '—'} InputProps={{ readOnly: true }} fullWidth />
<TextField label="Updated At" value={form.updatedAt ? new Date(form.updatedAt).toLocaleString() : '—'} InputProps={{ readOnly: true }} fullWidth />
<TextField label="Updated By" value={form.updatedBy ?? '—'} InputProps={{ readOnly: true }} fullWidth />
</Box>
) : null}
<Box display="flex" justifyContent="space-between" gap={1} mt={3}>
{ (form._Id || form.id) ? (
<Button color="error" onClick={handleDelete}>Delete</Button>
) : <span /> }
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
</Box>
</Box> </Box>
</Paper> </Paper>
); );

View File

@@ -2,8 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography, import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography,
ToggleButton, ToggleButtonGroup } from '@mui/material'; ToggleButton, ToggleButtonGroup } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid';
import EditIcon from '@mui/icons-material/Edit'; import EditRoundedIcon from '@mui/icons-material/EditRounded';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
import AddOrEditCategoryForm from './AddOrEditCategoryForm'; import AddOrEditCategoryForm from './AddOrEditCategoryForm';
import CategoriesApi from '../../api/CategoriesApi'; import CategoriesApi from '../../api/CategoriesApi';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
@@ -14,7 +14,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 [statusFilter, setStatusFilter] = useState('Active'); // <- por defecto Active 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);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
@@ -31,13 +31,8 @@ export default function Categories() {
const loadData = async () => { const loadData = async () => {
try { try {
const data = await api.getAll(); const data = await api.getAll();
const safeRows = (Array.isArray(data) ? data : []) const list = Array.isArray(data) ? data : [];
.filter(Boolean) setRows(list);
.map((r, idx) => ({
...r,
__rid: r?._id || r?._Id || r?.id || r?.Id || `tmp-${idx}`,
}));
setRows(safeRows);
} catch (e) { } catch (e) {
console.error('Failed to load categories:', e); console.error('Failed to load categories:', e);
setRows([]); setRows([]);
@@ -110,48 +105,79 @@ export default function Categories() {
}, [rows, statusFilter]); }, [rows, statusFilter]);
const columns = [ const columns = [
{
field: 'tagName',
headerName: 'Name',
flex: 1,
minWidth: 200,
valueGetter: (p) => p?.row?.tagName ?? p?.row?.name ?? '',
},
{
field: 'slug',
headerName: 'Slug',
width: 180,
valueGetter: (p) => p?.row?.slug ?? '',
},
{
field: 'displayOrder',
headerName: 'Display',
width: 120,
valueGetter: (p) => Number(p?.row?.displayOrder ?? 0),
},
{
field: 'status',
headerName: 'Status',
width: 140,
valueGetter: (p) => p?.row?.status ?? 'Active',
},
{ {
field: 'actions', field: 'actions',
headerName: '', headerName: '',
width: 120, width: 130,
sortable: false, sortable: false,
filterable: false, filterable: false,
renderCell: (params) => { disableExport: true,
const r = params?.row; renderCell: (params) => (
if (!r) return null; <Box display="flex" alignItems="center" justifyContent="flex-start" height="100%" gap={1}>
return ( <IconButton
<Box sx={{ display: 'flex', gap: 1 }}> size="small"
<IconButton size="small" onClick={() => handleEditClick(params)}><EditIcon /></IconButton> sx={{
<IconButton size="small" color="error" onClick={() => handleDeleteClick(r)}><DeleteIcon /></IconButton> backgroundColor: '#DFCCBC',
</Box> 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: 'tenantId', headerName: 'tenantId', width: 180 },
{ field: 'tagName', headerName: 'tagName', width: 200 },
{ field: 'typeId', headerName: 'typeId', width: 260 },
{
field: 'parentTagId',
headerName: 'parentTagId',
width: 220,
valueGetter: (params) => Array.isArray(params.value) ? params.value.join(', ') : (params.value ?? '—'),
},
{ field: 'slug', headerName: 'slug', width: 180 },
{ field: 'displayOrder', headerName: 'displayOrder', width: 140, type: 'number' },
{ field: 'icon', headerName: 'icon', width: 160 },
{ field: '_id', headerName: '_id', width: 260 },
{ field: 'id', headerName: 'id', width: 280 },
{
field: 'createdAt',
headerName: 'createdAt',
width: 200,
valueFormatter: (p) => {
const v = p?.value;
return v ? new Date(v).toLocaleString() : '—';
}, },
}, },
{ field: 'createdBy', headerName: 'createdBy', width: 180 },
{
field: 'updatedAt',
headerName: 'updatedAt',
width: 200,
valueFormatter: (p) => {
const v = p?.value;
return v ? new Date(v).toLocaleString() : '—';
},
},
{ field: 'updatedBy', headerName: 'updatedBy', width: 180 },
{ field: 'status', headerName: 'status', width: 140 },
]; ];
return ( return (
@@ -177,15 +203,29 @@ export default function Categories() {
</Box> </Box>
</Box> </Box>
<DataGrid <Box sx={{ width: '100%', overflowX: 'auto' }}>
rows={filteredRows} <DataGrid
columns={columns} rows={filteredRows}
pageSize={10} columns={columns}
rowsPerPageOptions={[10]} initialState={{ pagination: { paginationModel: { pageSize: 10 } } }}
autoHeight pageSizeOptions={[10]}
disableColumnMenu autoHeight
getRowId={(r) => r?.__rid} disableColumnMenu
/> getRowId={(r) => r?._id || r?.id}
sx={{
minWidth: 1400,
'& .MuiDataGrid-cell, & .MuiDataGrid-columnHeader': {
display: 'flex',
alignItems: 'center',
},
}}
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> <Dialog open={open} onClose={() => { setOpen(false); setEditingCategory(null); }} fullWidth>
<DialogTitle>{editingCategory ? 'Edit Category' : 'Add Category'}</DialogTitle> <DialogTitle>{editingCategory ? 'Edit Category' : 'Add Category'}</DialogTitle>