chore: edit existing item
This commit is contained in:
@@ -38,6 +38,7 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
||||
const [form, setForm] = useState({
|
||||
_Id: '',
|
||||
id: '',
|
||||
tenantId: '',
|
||||
tagName: '',
|
||||
typeId: '',
|
||||
parentTagId: [],
|
||||
@@ -45,6 +46,10 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
||||
displayOrder: 0,
|
||||
icon: '',
|
||||
status: 'Active',
|
||||
createdAt: null,
|
||||
createdBy: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
});
|
||||
|
||||
// cargar tipos y tags para selects
|
||||
@@ -68,6 +73,7 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
||||
setForm({
|
||||
_Id,
|
||||
id,
|
||||
tenantId: initialData.tenantId || extractTenantId(token) || '',
|
||||
tagName: initialData.tagName || initialData.name || '',
|
||||
typeId: initialData.typeId || '',
|
||||
parentTagId: Array.isArray(initialData.parentTagId) ? initialData.parentTagId : [],
|
||||
@@ -75,11 +81,16 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
||||
displayOrder: Number(initialData.displayOrder ?? 0),
|
||||
icon: initialData.icon || '',
|
||||
status: initialData.status || 'Active',
|
||||
createdAt: initialData.createdAt ?? null,
|
||||
createdBy: initialData.createdBy ?? null,
|
||||
updatedAt: initialData.updatedAt ?? null,
|
||||
updatedBy: initialData.updatedBy ?? null,
|
||||
});
|
||||
} else {
|
||||
setForm({
|
||||
_Id: '',
|
||||
id: '',
|
||||
tenantId: extractTenantId(token) || '',
|
||||
tagName: '',
|
||||
typeId: '',
|
||||
parentTagId: [],
|
||||
@@ -87,6 +98,10 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
||||
displayOrder: 0,
|
||||
icon: '',
|
||||
status: 'Active',
|
||||
createdAt: null,
|
||||
createdBy: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
});
|
||||
}
|
||||
}, [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 (
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||
{form._Id ? 'Edit Category' : 'Add Category'}
|
||||
</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
|
||||
name="tagName"
|
||||
label="Name"
|
||||
@@ -239,10 +286,24 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
||||
<MenuItem value="Inactive">Inactive</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<Box display="flex" justifyContent="flex-end" gap={1} mt={3}>
|
||||
{form._Id || form.id ? (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mt: 2 }}>
|
||||
<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>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ 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 EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
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';
|
||||
@@ -14,7 +14,7 @@ export default function Categories() {
|
||||
const api = useMemo(() => new CategoriesApi(token), [token]);
|
||||
|
||||
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 [editingCategory, setEditingCategory] = useState(null);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
@@ -31,13 +31,8 @@ export default function Categories() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const data = await api.getAll();
|
||||
const safeRows = (Array.isArray(data) ? data : [])
|
||||
.filter(Boolean)
|
||||
.map((r, idx) => ({
|
||||
...r,
|
||||
__rid: r?._id || r?._Id || r?.id || r?.Id || `tmp-${idx}`,
|
||||
}));
|
||||
setRows(safeRows);
|
||||
const list = Array.isArray(data) ? data : [];
|
||||
setRows(list);
|
||||
} catch (e) {
|
||||
console.error('Failed to load categories:', e);
|
||||
setRows([]);
|
||||
@@ -110,48 +105,79 @@ export default function Categories() {
|
||||
}, [rows, statusFilter]);
|
||||
|
||||
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',
|
||||
headerName: '',
|
||||
width: 120,
|
||||
width: 130,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
renderCell: (params) => {
|
||||
const r = params?.row;
|
||||
if (!r) return null;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<IconButton size="small" onClick={() => handleEditClick(params)}><EditIcon /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => handleDeleteClick(r)}><DeleteIcon /></IconButton>
|
||||
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: '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 (
|
||||
@@ -177,15 +203,29 @@ export default function Categories() {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: '100%', overflowX: 'auto' }}>
|
||||
<DataGrid
|
||||
rows={filteredRows}
|
||||
columns={columns}
|
||||
pageSize={10}
|
||||
rowsPerPageOptions={[10]}
|
||||
initialState={{ pagination: { paginationModel: { pageSize: 10 } } }}
|
||||
pageSizeOptions={[10]}
|
||||
autoHeight
|
||||
disableColumnMenu
|
||||
getRowId={(r) => r?.__rid}
|
||||
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>
|
||||
<DialogTitle>{editingCategory ? 'Edit Category' : 'Add Category'}</DialogTitle>
|
||||
|
||||
Reference in New Issue
Block a user