Compare commits
2 Commits
fead820091
...
aa62b06c23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa62b06c23 | ||
|
|
d699af9d75 |
@@ -1,4 +1,3 @@
|
|||||||
// src/private/categories/AddOrEditCategoryForm.jsx
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material';
|
import { Box, Button, Paper, TextField, Typography, MenuItem, Chip } from '@mui/material';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
@@ -18,16 +17,15 @@ function extractTenantId(token) {
|
|||||||
const payload = jwtDecode(token);
|
const payload = jwtDecode(token);
|
||||||
const t = payload?.tenant;
|
const t = payload?.tenant;
|
||||||
if (Array.isArray(t)) {
|
if (Array.isArray(t)) {
|
||||||
// prefer a 24-hex string if present
|
|
||||||
const hex = t.find(x => typeof x === 'string' && /^[0-9a-f]{24}$/i.test(x));
|
const hex = t.find(x => typeof x === 'string' && /^[0-9a-f]{24}$/i.test(x));
|
||||||
return hex || (typeof t[0] === 'string' ? t[0] : '');
|
return hex || (typeof t[0] === 'string' ? t[0] : '');
|
||||||
}
|
}
|
||||||
if (typeof t === 'string') return t;
|
if (typeof t === 'string') return t;
|
||||||
} catch {}
|
} catch { }
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) {
|
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel, materials: materialsProp = [], initialMaterialNames = [] }) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
||||||
const api = useMemo(() => new CategoriesApi(token), [token]);
|
const api = useMemo(() => new CategoriesApi(token), [token]);
|
||||||
@@ -35,6 +33,15 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
const [types, setTypes] = useState([]);
|
const [types, setTypes] = useState([]);
|
||||||
const [allTags, setAllTags] = useState([]);
|
const [allTags, setAllTags] = useState([]);
|
||||||
|
|
||||||
|
const tagLabelById = useMemo(() => {
|
||||||
|
const map = {};
|
||||||
|
for (const t of allTags) {
|
||||||
|
const key = t._id || t.id;
|
||||||
|
map[key] = t.tagName || t.name || key;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [allTags]);
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
_Id: '',
|
_Id: '',
|
||||||
id: '',
|
id: '',
|
||||||
@@ -56,15 +63,58 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const [t, tags] = await Promise.all([api.getAllTypes(), api.getAll()]);
|
// Always try to load all tags (for materials, lookups, etc.)
|
||||||
setTypes(Array.isArray(t) ? t : []);
|
const tags = typeof api.getAll === 'function' ? await api.getAll() : [];
|
||||||
|
|
||||||
|
// Try multiple method names for types; if none exist, derive from tags
|
||||||
|
let typesResp = [];
|
||||||
|
if (typeof api.getAllTypes === 'function') {
|
||||||
|
typesResp = await api.getAllTypes();
|
||||||
|
} else if (typeof api.getTypes === 'function') {
|
||||||
|
typesResp = await api.getTypes();
|
||||||
|
} else if (Array.isArray(tags)) {
|
||||||
|
// Derive a minimal "types" list from existing tag.typeId values
|
||||||
|
const uniqueTypeIds = [...new Set(tags.map(t => t?.typeId).filter(Boolean))];
|
||||||
|
typesResp = uniqueTypeIds.map(id => ({ id, typeName: id, level: null }));
|
||||||
|
console.warn('CategoriesApi has no getAllTypes/getTypes; derived types from tag.typeId.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setTypes(Array.isArray(typesResp) ? typesResp : []);
|
||||||
setAllTags(Array.isArray(tags) ? tags : []);
|
setAllTags(Array.isArray(tags) ? tags : []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load tag types or tags', e);
|
console.error('Failed to load tag types or tags', e);
|
||||||
|
setTypes([]);
|
||||||
|
setAllTags([]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
|
// When editing: if we received material names from the grid, map them to IDs once allTags are loaded.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Array.isArray(initialMaterialNames) || initialMaterialNames.length === 0) return;
|
||||||
|
// If parentTagId already has values (ids), do not override.
|
||||||
|
if (Array.isArray(form.parentTagId) && form.parentTagId.length > 0) return;
|
||||||
|
if (!Array.isArray(allTags) || allTags.length === 0) return;
|
||||||
|
|
||||||
|
// Build a case-insensitive name -> id map
|
||||||
|
const nameToId = new Map(
|
||||||
|
allTags.map(t => {
|
||||||
|
const id = t._id || t.id;
|
||||||
|
const label = (t.tagName || t.name || '').toLowerCase();
|
||||||
|
return [label, id];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const ids = initialMaterialNames
|
||||||
|
.map(n => (typeof n === 'string' ? n.toLowerCase() : ''))
|
||||||
|
.map(lower => nameToId.get(lower))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (ids.length > 0) {
|
||||||
|
setForm(prev => ({ ...prev, parentTagId: ids }));
|
||||||
|
}
|
||||||
|
}, [initialMaterialNames, allTags]);
|
||||||
|
|
||||||
// set inicial
|
// set inicial
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
@@ -106,6 +156,9 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
}
|
}
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
|
const isEdit = Boolean(form._Id || form.id);
|
||||||
|
const isAdd = !isEdit;
|
||||||
|
|
||||||
const setVal = (name, value) => setForm(p => ({ ...p, [name]: value }));
|
const setVal = (name, value) => setForm(p => ({ ...p, [name]: value }));
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
@@ -180,23 +233,16 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
{form._Id ? 'Edit Category' : 'Add Category'}
|
{form._Id ? 'Edit Category' : 'Add Category'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* IDs (read-only) */}
|
{isAdd && (
|
||||||
{form._Id || form.id ? (
|
<TextField
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mb: 2 }}>
|
name="tenantId"
|
||||||
<TextField label="_Id" value={form._Id} InputProps={{ readOnly: true }} fullWidth />
|
label="Tenant Id"
|
||||||
<TextField label="id" value={form.id} InputProps={{ readOnly: true }} fullWidth />
|
value={form.tenantId}
|
||||||
</Box>
|
fullWidth
|
||||||
) : null}
|
sx={{ mb: 2 }}
|
||||||
|
InputProps={{ readOnly: true }}
|
||||||
{/* 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"
|
||||||
@@ -227,25 +273,35 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
name="parentTagId"
|
name="parentTagId"
|
||||||
label="Parent tags"
|
label="Material"
|
||||||
value={form.parentTagId}
|
value={form.parentTagId}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
// For MUI Select multiple, e.target.value is an array of selected IDs
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setVal('parentTagId', typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : val);
|
setVal('parentTagId', Array.isArray(val) ? val : []);
|
||||||
}}
|
}}
|
||||||
select
|
select
|
||||||
SelectProps={{ multiple: true, renderValue: (selected) => (
|
SelectProps={{
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
multiple: true,
|
||||||
{selected.map(v => <Chip key={v} label={v} size="small" />)}
|
renderValue: (selected) => (
|
||||||
</Box>
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||||
)}}
|
{selected.map((id) => (
|
||||||
|
<Chip key={id} label={tagLabelById[id] || id} size="small" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
>
|
>
|
||||||
{allTags.map(t => {
|
{allTags.map((t) => {
|
||||||
const value = t._id || t.id;
|
const value = t._id || t.id;
|
||||||
const label = t.tagName || t.name || value;
|
const label = t.tagName || t.name || value;
|
||||||
return <MenuItem key={value} value={value}>{label}</MenuItem>;
|
return (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
@@ -301,9 +357,9 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Box display="flex" justifyContent="space-between" gap={1} mt={3}>
|
<Box display="flex" justifyContent="space-between" gap={1} mt={3}>
|
||||||
{ (form._Id || form.id) ? (
|
{(form._Id || form.id) ? (
|
||||||
<Button color="error" onClick={handleDelete}>Delete</Button>
|
<Button color="error" onClick={handleDelete}>Delete</Button>
|
||||||
) : <span /> }
|
) : <span />}
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
|
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
|
||||||
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography,
|
import {
|
||||||
ToggleButton, ToggleButtonGroup } from '@mui/material';
|
Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography,
|
||||||
|
ToggleButton, ToggleButtonGroup
|
||||||
|
} from '@mui/material';
|
||||||
import { DataGrid } from '@mui/x-data-grid';
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||||
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
||||||
@@ -14,6 +16,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 [allTags, setAllTags] = useState([]);
|
||||||
const [statusFilter, setStatusFilter] = useState('All'); // <- por defecto All
|
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);
|
||||||
@@ -35,6 +38,8 @@ export default function Categories() {
|
|||||||
const data = await api.getAll();
|
const data = await api.getAll();
|
||||||
const list = Array.isArray(data) ? data : [];
|
const list = Array.isArray(data) ? data : [];
|
||||||
|
|
||||||
|
setAllTags(list);
|
||||||
|
|
||||||
// Build a map of parentId -> array of child tagNames
|
// Build a map of parentId -> array of child tagNames
|
||||||
const parentToChildren = {};
|
const parentToChildren = {};
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
@@ -76,6 +81,9 @@ export default function Categories() {
|
|||||||
displayOrder: Number(r.displayOrder ?? 0),
|
displayOrder: Number(r.displayOrder ?? 0),
|
||||||
icon: r.icon || '',
|
icon: r.icon || '',
|
||||||
status: r.status ?? 'Active',
|
status: r.status ?? 'Active',
|
||||||
|
materialNames: typeof r.material === 'string'
|
||||||
|
? r.material.split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
: Array.isArray(r.material) ? r.material : [],
|
||||||
});
|
});
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
@@ -164,10 +172,8 @@ export default function Categories() {
|
|||||||
},
|
},
|
||||||
{ field: 'tagName', headerName: 'Name', flex: 1.2, minWidth: 180 },
|
{ field: 'tagName', headerName: 'Name', flex: 1.2, minWidth: 180 },
|
||||||
{ field: 'slug', headerName: 'Slug', flex: 1.0, minWidth: 160 },
|
{ field: 'slug', headerName: 'Slug', flex: 1.0, minWidth: 160 },
|
||||||
{ field: 'icon', headerName: 'Icon', flex: 0.7, minWidth: 120 },
|
{ field: 'icon', headerName: 'Icon', flex: 0.7, minWidth: 250 },
|
||||||
// New computed column
|
|
||||||
{ field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 },
|
{ field: 'material', headerName: 'Material', flex: 1.2, minWidth: 200 },
|
||||||
// Hidden audit columns
|
|
||||||
{
|
{
|
||||||
field: 'createdAt',
|
field: 'createdAt',
|
||||||
headerName: 'Created Date',
|
headerName: 'Created Date',
|
||||||
@@ -229,7 +235,17 @@ export default function Categories() {
|
|||||||
<DataGrid
|
<DataGrid
|
||||||
rows={filteredRows}
|
rows={filteredRows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
initialState={{ pagination: { paginationModel: { pageSize: pageSize } } }}
|
initialState={{
|
||||||
|
pagination: { paginationModel: { pageSize } },
|
||||||
|
columns: {
|
||||||
|
columnVisibilityModel: {
|
||||||
|
createdAt: false,
|
||||||
|
createdBy: false,
|
||||||
|
updatedAt: false,
|
||||||
|
updatedBy: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
pageSizeOptions={[pageSize]}
|
pageSizeOptions={[pageSize]}
|
||||||
disableColumnMenu
|
disableColumnMenu
|
||||||
getRowId={(r) => r?._id || r?.id}
|
getRowId={(r) => r?._id || r?.id}
|
||||||
@@ -256,6 +272,8 @@ export default function Categories() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<AddOrEditCategoryForm
|
<AddOrEditCategoryForm
|
||||||
initialData={editingCategory}
|
initialData={editingCategory}
|
||||||
|
allTags={allTags}
|
||||||
|
initialMaterialNames={editingCategory?.materialNames || []}
|
||||||
onAdd={handleFormDone}
|
onAdd={handleFormDone}
|
||||||
onCancel={() => { setOpen(false); setEditingCategory(null); }}
|
onCancel={() => { setOpen(false); setEditingCategory(null); }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user