feat: added categories

This commit is contained in:
2025-09-01 17:19:06 -06:00
parent af323aade7
commit 0a74c7a22a
6 changed files with 296 additions and 207 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -7,6 +7,7 @@ import Footer from './components/Footer';
import Dashboard from './private/dashboard/Dashboard'; import Dashboard from './private/dashboard/Dashboard';
import UserManagement from './private/users/UserManagement'; import UserManagement from './private/users/UserManagement';
import FurnitureVariantManagement from './private/fornitures/FurnitureVariantManagement'; import FurnitureVariantManagement from './private/fornitures/FurnitureVariantManagement';
import Categories from './private/categories/Categories';
import LoginPage from './private/LoginPage'; import LoginPage from './private/LoginPage';
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import { useAuth } from './context/AuthContext'; import { useAuth } from './context/AuthContext';
@@ -24,10 +25,8 @@ export default function App() {
const mainLeft = isMobile ? 0 : (drawerExpanded ? DRAWER_EXPANDED : DRAWER_COLLAPSED); const mainLeft = isMobile ? 0 : (drawerExpanded ? DRAWER_EXPANDED : DRAWER_COLLAPSED);
// Evita flicker mientras se restaura sesión
if (initializing) return null; if (initializing) return null;
// === RUTAS PÚBLICAS (sin shell) ===
if (!user) { if (!user) {
return ( return (
<Routes> <Routes>
@@ -37,14 +36,9 @@ export default function App() {
); );
} }
// === RUTAS PRIVADAS (con shell) ===
return ( return (
<> <>
<AppHeader <AppHeader zone="private" currentPage={currentView} leftOffset={mainLeft} />
zone="private"
currentPage={currentView}
leftOffset={mainLeft}
/>
<MenuDrawerPrivate <MenuDrawerPrivate
onSelect={(value) => setCurrentView(value)} onSelect={(value) => setCurrentView(value)}
@@ -64,9 +58,7 @@ export default function App() {
}} }}
> >
<Routes> <Routes>
{/* Si ya está autenticado, /login redirige al dashboard */}
<Route path="/login" element={<Navigate to="/" replace />} /> <Route path="/login" element={<Navigate to="/" replace />} />
<Route <Route
path="/" path="/"
element={ element={
@@ -76,6 +68,9 @@ export default function App() {
{currentView === '/ProductsManagement/CatalogManagement/ProductCollections' && ( {currentView === '/ProductsManagement/CatalogManagement/ProductCollections' && (
<FurnitureVariantManagement /> <FurnitureVariantManagement />
)} )}
{currentView === '/ProductsManagement/CatalogManagement/Categories' && (
<Categories />
)}
</> </>
} }
/> />

54
src/api/CategoriesApi.js Normal file
View File

@@ -0,0 +1,54 @@
// src/api/CategoriesApi.js
export default class CategoriesApi {
constructor(token) {
this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/Tags';
this.token = token;
}
headers(json = true) {
return {
accept: 'application/json',
...(json ? { 'Content-Type': 'application/json' } : {}),
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
};
}
async getAll() {
const res = await fetch(`${this.baseUrl}/GetAll`, {
method: 'GET',
headers: this.headers(false),
});
if (!res.ok) throw new Error(`GetAll error ${res.status}: ${await res.text()}`);
return res.json();
}
async create(payload) {
const res = await fetch(`${this.baseUrl}/Create`, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Create error ${res.status}: ${await res.text()}`);
return res.json();
}
async update(payload) {
const res = await fetch(`${this.baseUrl}/Update`, {
method: 'PUT',
headers: this.headers(),
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Update error ${res.status}: ${await res.text()}`);
return res.json();
}
async delete(payload) {
const res = await fetch(`${this.baseUrl}/Delete`, {
method: 'DELETE',
headers: this.headers(),
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`);
return res.json();
}
}

View File

@@ -40,11 +40,13 @@ const menuData = [
{ title: 'Categories' } { title: 'Categories' }
] ]
}, },
{ title: 'Products', {
title: 'Products',
children: [ children: [
{ title: 'AR Assets Library Management' }, { title: 'AR Assets Library Management' },
{ title: 'Media Management' }, { title: 'Media Management' },
] }, ]
},
{ title: 'Product Collections' }, { title: 'Product Collections' },
] ]
} }
@@ -54,7 +56,8 @@ const menuData = [
title: 'Customers', title: 'Customers',
icon: <PeopleAltIcon />, icon: <PeopleAltIcon />,
children: [ children: [
{ title: 'CRM', {
title: 'CRM',
children: [ children: [
{ title: 'Customer List' }, { title: 'Customer List' },
{ title: 'Projects' }, { title: 'Projects' },
@@ -86,7 +89,8 @@ const menuData = [
icon: <AdminPanelSettingsIcon />, icon: <AdminPanelSettingsIcon />,
children: [ children: [
{ title: 'Users Management' }, { title: 'Users Management' },
{ title: 'Access Control', {
title: 'Access Control',
children: [ children: [
{ title: 'Roles' }, { title: 'Roles' },
{ title: 'Permissions' }, { title: 'Permissions' },
@@ -157,6 +161,8 @@ export default function MenuDrawerPrivate({
onSelect?.('/Users/UserManagement'); onSelect?.('/Users/UserManagement');
} else if (node.title === 'Product Collections') { } else if (node.title === 'Product Collections') {
onSelect?.('/ProductsManagement/CatalogManagement/ProductCollections'); onSelect?.('/ProductsManagement/CatalogManagement/ProductCollections');
} else if (node.title === 'Categories') {
onSelect?.('/ProductsManagement/CatalogManagement/Categories');
} else { } else {
onSelect?.(node.title); onSelect?.(node.title);
} }
@@ -199,7 +205,7 @@ export default function MenuDrawerPrivate({
{hasChildren && !collapsed && ( {hasChildren && !collapsed && (
<Collapse in={!!openMap[key]} timeout="auto" unmountOnExit> <Collapse in={!!openMap[key]} timeout="auto" unmountOnExit>
<List component="div" disablePadding sx={{ pl: 7 }}> <List component="div" disablePadding sx={{ pl: 7 }}>
{node.children.map((child, idx) => renderNode(child, `${key}-`))} {node.children.map((child) => renderNode(child, `${key}-`))}
</List> </List>
</Collapse> </Collapse>
)} )}

View File

@@ -1,17 +1,32 @@
import { useState, useEffect } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Box, Button, TextField, Typography, Paper } from '@mui/material'; import { Box, Button, Paper, TextField, Typography } from '@mui/material';
import { useAuth } from '../../context/AuthContext';
import CategoriesApi from '../../api/CategoriesApi';
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) { export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) {
const { user } = useAuth();
const token = user?.thalosToken || localStorage.getItem('thalosToken');
const api = useMemo(() => new CategoriesApi(token), [token]);
const [category, setCategory] = useState({ const [category, setCategory] = useState({
_Id: '',
id: '',
name: '', name: '',
description: '' description: '',
status: 'Active',
}); });
useEffect(() => { useEffect(() => {
if (initialData) { if (initialData) {
setCategory(initialData); setCategory({
_Id: initialData._id || initialData._Id || '',
id: initialData.id || initialData.Id || initialData._id || initialData._Id || '',
name: initialData.name ?? '',
description: initialData.description ?? '',
status: initialData.status ?? 'Active',
});
} else { } else {
setCategory({ name: '', description: '' }); setCategory({ _Id: '', id: '', name: '', description: '', status: 'Active' });
} }
}, [initialData]); }, [initialData]);
@@ -20,45 +35,60 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
setCategory((prev) => ({ ...prev, [name]: value })); setCategory((prev) => ({ ...prev, [name]: value }));
}; };
const handleSubmit = () => { const handleSubmit = async () => {
if (onAdd) { try {
onAdd(category); if (category._Id) {
const payload = {
_Id: category._Id,
Id: category.id || category._Id,
name: category.name,
description: category.description,
status: category.status,
};
await api.update(payload);
} else {
const payload = {
name: category.name,
description: category.description,
status: category.status,
};
await api.create(payload);
}
onAdd?.();
} catch (e) {
console.error('Submit category failed:', e);
} }
}; };
return ( return (
<Box sx={{ px: 2, py: 3 }}> <Paper sx={{ p: 2 }}>
<Paper elevation={0} sx={{ p: 3, bgcolor: '#f9f9f9', borderRadius: 2 }}> <Typography variant="subtitle1" sx={{ mb: 2 }}>
<Typography variant="h6" gutterBottom> {category._Id ? 'Edit Category' : 'Add Category'}
Category Details
</Typography> </Typography>
<TextField <TextField
fullWidth
label="Name"
name="name" name="name"
label="Name"
value={category.name} value={category.name}
onChange={handleChange} onChange={handleChange}
margin="normal"
/>
<TextField
fullWidth fullWidth
label="Description" sx={{ mb: 2 }}
/>
<TextField
name="description" name="description"
label="Description"
value={category.description} value={category.description}
onChange={handleChange} onChange={handleChange}
margin="normal" fullWidth
multiline multiline
rows={4} minRows={3}
/> />
<Box display="flex" justifyContent="flex-end" gap={1} mt={3}> <Box display="flex" justifyContent="flex-end" gap={1} mt={3}>
<Button onClick={onCancel} className="button-transparent"> <Button onClick={onCancel} className="button-transparent">Cancel</Button>
Cancel <Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
</Button>
<Button variant="contained" onClick={handleSubmit} className="button-gold">
Save
</Button>
</Box> </Box>
</Paper> </Paper>
</Box>
); );
} }

View File

@@ -1,44 +1,56 @@
import SectionContainer from '../../components/SectionContainer.jsx'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useState } from 'react'; import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid';
import { Typography, Button, Dialog, DialogTitle, DialogContent, IconButton, Box } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit';
import AddOrEditCategoryForm from './AddOrEditCategoryForm.jsx'; import DeleteIcon from '@mui/icons-material/Delete';
import AddOrEditCategoryForm from './AddOrEditCategoryForm';
import CategoriesApi from '../../api/CategoriesApi';
import { useAuth } from '../../context/AuthContext';
import EditRoundedIcon from '@mui/icons-material/EditRounded'; export default function Categories() {
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; const { user } = useAuth();
import '../../App.css'; const token = user?.thalosToken || localStorage.getItem('thalosToken');
const api = useMemo(() => new CategoriesApi(token), [token]);
const columnsBase = [
{ field: 'name', headerName: 'Name', flex: 1 },
{ field: 'description', headerName: 'Description', flex: 2 }
];
export default function Categories({ children, maxWidth = 'lg', sx = {} }) {
const [rows, setRows] = useState([
{ id: 1, name: 'Fabrics', description: 'Textile materials including silk, cotton, and synthetics.' },
{ id: 2, name: 'Leather Goods', description: 'Leather-based components for luxury goods.' },
{ id: 3, name: 'Metal Accessories', description: 'Buttons, zippers, and hardware in metal.' },
{ id: 4, name: 'Embellishments', description: 'Decorative materials such as beads and sequins.' }
]);
const [rows, setRows] = useState([]);
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);
const [rowToDelete, setRowToDelete] = useState(null); const [rowToDelete, setRowToDelete] = useState(null);
const hasLoaded = useRef(false);
const handleAddOrEditCategory = (category) => { useEffect(() => {
if (editingCategory) { if (!hasLoaded.current) {
setRows(rows.map((row) => (row.id === editingCategory.id ? { ...editingCategory, ...category } : row))); loadData();
} else { hasLoaded.current = true;
const id = rows.length + 1;
setRows([...rows, { id, ...category }]);
} }
setOpen(false); }, []);
const loadData = async () => {
try {
const data = await api.getAll();
setRows(Array.isArray(data) ? data : []);
} catch (e) {
console.error('Failed to load categories:', e);
setRows([]);
}
};
const handleAddClick = () => {
setEditingCategory(null); setEditingCategory(null);
setOpen(true);
}; };
const handleEditClick = (params) => { const handleEditClick = (params) => {
setEditingCategory(params.row); const r = params?.row;
if (!r) return;
setEditingCategory({
_Id: r._id || r._Id || '',
id: r.id || r.Id || '',
name: r.name ?? '',
description: r.description ?? '',
status: r.status ?? 'Active',
});
setOpen(true); setOpen(true);
}; };
@@ -47,96 +59,88 @@ export default function Categories({ children, maxWidth = 'lg', sx = {} }) {
setConfirmOpen(true); setConfirmOpen(true);
}; };
const confirmDelete = () => { const confirmDelete = async () => {
setRows(rows.filter((row) => row.id !== rowToDelete.id)); try {
setRowToDelete(null); if (!rowToDelete) return;
const payload = {
_Id: rowToDelete._id || rowToDelete._Id,
id: rowToDelete.id || rowToDelete.Id || '',
name: rowToDelete.name,
description: rowToDelete.description,
status: 'Inactive', // soft-delete
};
await api.update(payload);
await loadData();
} catch (e) {
console.error('Delete failed:', e);
} finally {
setConfirmOpen(false); setConfirmOpen(false);
setRowToDelete(null);
}
};
const handleFormDone = async () => {
await loadData();
setOpen(false);
setEditingCategory(null);
}; };
const columns = [ const columns = [
...columnsBase, { field: 'name', headerName: 'Name', flex: 1, minWidth: 200 },
{ field: 'description', headerName: 'Description', flex: 1, minWidth: 250 },
{ field: 'status', headerName: 'Status', width: 140, valueGetter: (p) => p.row?.status ?? 'Active' },
{ {
field: 'actions', field: 'actions',
headerName: '', headerName: '',
width: 130, width: 120,
sortable: false,
filterable: false,
renderCell: (params) => ( renderCell: (params) => (
<Box display="flex" alignItems="center" justifyContent="flex-end" height="100%" gap={2}> <Box sx={{ display: 'flex', gap: 1 }}>
<IconButton <IconButton size="small" onClick={() => handleEditClick(params)}><EditIcon /></IconButton>
size="small" <IconButton size="small" color="error" onClick={() => handleDeleteClick(params.row)}><DeleteIcon /></IconButton>
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> </Box>
) ),
} },
]; ];
return ( return (
<SectionContainer sx={{ width: '100%' }}> <Box>
<Typography variant="h4" gutterBottom color='#26201AFF'> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
Categories <Typography variant="h6">Categories</Typography>
</Typography> <Button variant="contained" onClick={handleAddClick} className="button-gold">Add Category</Button>
</Box>
<DataGrid
rows={rows}
columns={columns}
pageSize={10}
rowsPerPageOptions={[10]}
autoHeight
disableColumnMenu
getRowId={(r) => r._id || r._Id || r.id || r.Id}
/>
<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>
<DialogContent> <DialogContent>
<AddOrEditCategoryForm onAdd={handleAddOrEditCategory} initialData={editingCategory} onCancel={() => { setOpen(false); setEditingCategory(null); }} /> <AddOrEditCategoryForm
initialData={editingCategory}
onAdd={handleFormDone}
onCancel={() => { setOpen(false); setEditingCategory(null); }}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}> <Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
<DialogTitle>Confirm Delete</DialogTitle> <DialogTitle>Delete Category</DialogTitle>
<DialogContent> <DialogContent>
<Typography> <Box sx={{ display: 'flex', gap: 1, mt: 2, mb: 1 }}>
Are you sure you want to delete <strong>{rowToDelete?.name}</strong>? <Button onClick={() => setConfirmOpen(false)}>Cancel</Button>
</Typography> <Button color="error" variant="contained" onClick={confirmDelete}>Delete</Button>
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
<Button onClick={() => setConfirmOpen(false)} className='button-transparent'>Cancel</Button>
<Button variant="contained" onClick={confirmDelete} className="button-gold">Delete</Button>
</Box> </Box>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Box mt={2}>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
getRowSpacing={() => ({ top: 8, bottom: 8 })}
/>
<Box display="flex" justifyContent="flex-end" mt={2}>
<Button variant="contained" onClick={() => setOpen(true)} className="button-gold">
Add Category
</Button>
</Box> </Box>
</Box>
</SectionContainer>
); );
} }