feat: added categories
This commit is contained in:
		| @@ -1,64 +1,94 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { Box, Button, TextField, Typography, Paper } from '@mui/material'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | ||||
| 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 }) { | ||||
|     const [category, setCategory] = useState({ | ||||
|         name: '', | ||||
|         description: '' | ||||
|     }); | ||||
|   const { user } = useAuth(); | ||||
|   const token = user?.thalosToken || localStorage.getItem('thalosToken'); | ||||
|   const api = useMemo(() => new CategoriesApi(token), [token]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (initialData) { | ||||
|             setCategory(initialData); | ||||
|         } else { | ||||
|             setCategory({ name: '', description: '' }); | ||||
|         } | ||||
|     }, [initialData]); | ||||
|   const [category, setCategory] = useState({ | ||||
|     _Id: '', | ||||
|     id: '', | ||||
|     name: '', | ||||
|     description: '', | ||||
|     status: 'Active', | ||||
|   }); | ||||
|  | ||||
|     const handleChange = (e) => { | ||||
|         const { name, value } = e.target; | ||||
|         setCategory((prev) => ({ ...prev, [name]: value })); | ||||
|     }; | ||||
|   useEffect(() => { | ||||
|     if (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 { | ||||
|       setCategory({ _Id: '', id: '', name: '', description: '', status: 'Active' }); | ||||
|     } | ||||
|   }, [initialData]); | ||||
|  | ||||
|     const handleSubmit = () => { | ||||
|         if (onAdd) { | ||||
|             onAdd(category); | ||||
|         } | ||||
|     }; | ||||
|   const handleChange = (e) => { | ||||
|     const { name, value } = e.target; | ||||
|     setCategory((prev) => ({ ...prev, [name]: value })); | ||||
|   }; | ||||
|  | ||||
|     return ( | ||||
|         <Box sx={{ px: 2, py: 3 }}> | ||||
|             <Paper elevation={0} sx={{ p: 3, bgcolor: '#f9f9f9', borderRadius: 2 }}> | ||||
|                 <Typography variant="h6" gutterBottom> | ||||
|                     Category Details | ||||
|                 </Typography> | ||||
|                 <TextField | ||||
|                     fullWidth | ||||
|                     label="Name" | ||||
|                     name="name" | ||||
|                     value={category.name} | ||||
|                     onChange={handleChange} | ||||
|                     margin="normal" | ||||
|                 /> | ||||
|                 <TextField | ||||
|                     fullWidth | ||||
|                     label="Description" | ||||
|                     name="description" | ||||
|                     value={category.description} | ||||
|                     onChange={handleChange} | ||||
|                     margin="normal" | ||||
|                     multiline | ||||
|                     rows={4} | ||||
|                 /> | ||||
|                 <Box display="flex" justifyContent="flex-end" gap={1} mt={3}> | ||||
|                     <Button onClick={onCancel} className="button-transparent"> | ||||
|                         Cancel | ||||
|                     </Button> | ||||
|                     <Button variant="contained" onClick={handleSubmit} className="button-gold"> | ||||
|                         Save | ||||
|                     </Button> | ||||
|                 </Box> | ||||
|             </Paper> | ||||
|         </Box> | ||||
|     ); | ||||
|   const handleSubmit = async () => { | ||||
|     try { | ||||
|       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 ( | ||||
|     <Paper sx={{ p: 2 }}> | ||||
|       <Typography variant="subtitle1" sx={{ mb: 2 }}> | ||||
|         {category._Id ? 'Edit Category' : 'Add Category'} | ||||
|       </Typography> | ||||
|  | ||||
|       <TextField | ||||
|         name="name" | ||||
|         label="Name" | ||||
|         value={category.name} | ||||
|         onChange={handleChange} | ||||
|         fullWidth | ||||
|         sx={{ mb: 2 }} | ||||
|       /> | ||||
|  | ||||
|       <TextField | ||||
|         name="description" | ||||
|         label="Description" | ||||
|         value={category.description} | ||||
|         onChange={handleChange} | ||||
|         fullWidth | ||||
|         multiline | ||||
|         minRows={3} | ||||
|       /> | ||||
|  | ||||
|       <Box display="flex" justifyContent="flex-end" gap={1} mt={3}> | ||||
|         <Button onClick={onCancel} className="button-transparent">Cancel</Button> | ||||
|         <Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button> | ||||
|       </Box> | ||||
|     </Paper> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -1,142 +1,146 @@ | ||||
| import SectionContainer from '../../components/SectionContainer.jsx'; | ||||
| import { useState } from 'react'; | ||||
| import { useEffect, useMemo, useRef, useState } from 'react'; | ||||
| import { Box, Button, Dialog, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material'; | ||||
| import { DataGrid } from '@mui/x-data-grid'; | ||||
| import { Typography, Button, Dialog, DialogTitle, DialogContent, IconButton, Box } from '@mui/material'; | ||||
| import AddOrEditCategoryForm from './AddOrEditCategoryForm.jsx'; | ||||
| import EditIcon from '@mui/icons-material/Edit'; | ||||
| 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'; | ||||
| import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; | ||||
| import '../../App.css'; | ||||
| export default function Categories() { | ||||
|   const { user } = useAuth(); | ||||
|   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 } | ||||
| ]; | ||||
|   const [rows, setRows] = useState([]); | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const [editingCategory, setEditingCategory] = useState(null); | ||||
|   const [confirmOpen, setConfirmOpen] = useState(false); | ||||
|   const [rowToDelete, setRowToDelete] = useState(null); | ||||
|   const hasLoaded = useRef(false); | ||||
|  | ||||
| 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.' } | ||||
|     ]); | ||||
|   useEffect(() => { | ||||
|     if (!hasLoaded.current) { | ||||
|       loadData(); | ||||
|       hasLoaded.current = true; | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|     const [open, setOpen] = useState(false); | ||||
|     const [editingCategory, setEditingCategory] = useState(null); | ||||
|     const [confirmOpen, setConfirmOpen] = useState(false); | ||||
|     const [rowToDelete, setRowToDelete] = useState(null); | ||||
|   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 handleAddOrEditCategory = (category) => { | ||||
|         if (editingCategory) { | ||||
|             setRows(rows.map((row) => (row.id === editingCategory.id ? { ...editingCategory, ...category } : row))); | ||||
|         } else { | ||||
|             const id = rows.length + 1; | ||||
|             setRows([...rows, { id, ...category }]); | ||||
|         } | ||||
|         setOpen(false); | ||||
|         setEditingCategory(null); | ||||
|     }; | ||||
|   const handleAddClick = () => { | ||||
|     setEditingCategory(null); | ||||
|     setOpen(true); | ||||
|   }; | ||||
|  | ||||
|     const handleEditClick = (params) => { | ||||
|         setEditingCategory(params.row); | ||||
|         setOpen(true); | ||||
|     }; | ||||
|   const handleEditClick = (params) => { | ||||
|     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); | ||||
|   }; | ||||
|  | ||||
|     const handleDeleteClick = (row) => { | ||||
|         setRowToDelete(row); | ||||
|         setConfirmOpen(true); | ||||
|     }; | ||||
|   const handleDeleteClick = (row) => { | ||||
|     setRowToDelete(row); | ||||
|     setConfirmOpen(true); | ||||
|   }; | ||||
|  | ||||
|     const confirmDelete = () => { | ||||
|         setRows(rows.filter((row) => row.id !== rowToDelete.id)); | ||||
|         setRowToDelete(null); | ||||
|         setConfirmOpen(false); | ||||
|     }; | ||||
|   const confirmDelete = async () => { | ||||
|     try { | ||||
|       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); | ||||
|       setRowToDelete(null); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|     const columns = [ | ||||
|         ...columnsBase, | ||||
|         { | ||||
|             field: 'actions', | ||||
|             headerName: '', | ||||
|             width: 130, | ||||
|             renderCell: (params) => ( | ||||
|                 <Box display="flex" alignItems="center" justifyContent="flex-end" height="100%" gap={2}> | ||||
|                     <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> | ||||
|             ) | ||||
|         } | ||||
|     ]; | ||||
|   const handleFormDone = async () => { | ||||
|     await loadData(); | ||||
|     setOpen(false); | ||||
|     setEditingCategory(null); | ||||
|   }; | ||||
|  | ||||
|     return ( | ||||
|         <SectionContainer sx={{ width: '100%' }}> | ||||
|             <Typography variant="h4" gutterBottom color='#26201AFF'> | ||||
|                 Categories | ||||
|             </Typography> | ||||
|   const columns = [ | ||||
|     { 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', | ||||
|       headerName: '', | ||||
|       width: 120, | ||||
|       sortable: false, | ||||
|       filterable: false, | ||||
|       renderCell: (params) => ( | ||||
|         <Box sx={{ display: 'flex', gap: 1 }}> | ||||
|           <IconButton size="small" onClick={() => handleEditClick(params)}><EditIcon /></IconButton> | ||||
|           <IconButton size="small" color="error" onClick={() => handleDeleteClick(params.row)}><DeleteIcon /></IconButton> | ||||
|         </Box> | ||||
|       ), | ||||
|     }, | ||||
|   ]; | ||||
|  | ||||
|             <Dialog open={open} onClose={() => { setOpen(false); setEditingCategory(null); }} fullWidth> | ||||
|                 <DialogTitle>{editingCategory ? 'Edit Category' : 'Add Category'}</DialogTitle> | ||||
|                 <DialogContent> | ||||
|                     <AddOrEditCategoryForm onAdd={handleAddOrEditCategory} initialData={editingCategory} onCancel={() => { setOpen(false); setEditingCategory(null); }} /> | ||||
|                 </DialogContent> | ||||
|             </Dialog> | ||||
|   return ( | ||||
|     <Box> | ||||
|       <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | ||||
|         <Typography variant="h6">Categories</Typography> | ||||
|         <Button variant="contained" onClick={handleAddClick} className="button-gold">Add Category</Button> | ||||
|       </Box> | ||||
|  | ||||
|             <Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}> | ||||
|                 <DialogTitle>Confirm Delete</DialogTitle> | ||||
|                 <DialogContent> | ||||
|                     <Typography> | ||||
|                         Are you sure you want to delete <strong>{rowToDelete?.name}</strong>? | ||||
|                     </Typography> | ||||
|                     <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> | ||||
|                 </DialogContent> | ||||
|             </Dialog> | ||||
|       <DataGrid | ||||
|         rows={rows} | ||||
|         columns={columns} | ||||
|         pageSize={10} | ||||
|         rowsPerPageOptions={[10]} | ||||
|         autoHeight | ||||
|         disableColumnMenu | ||||
|         getRowId={(r) => r._id || r._Id || r.id || r.Id} | ||||
|       /> | ||||
|  | ||||
|             <Box mt={2}> | ||||
|                 <DataGrid | ||||
|                     rows={rows} | ||||
|                     columns={columns} | ||||
|                     pageSize={5} | ||||
|                     rowsPerPageOptions={[5]} | ||||
|                     getRowSpacing={() => ({ top: 8, bottom: 8 })} | ||||
|                 /> | ||||
|       <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> | ||||
|  | ||||
|                 <Box display="flex" justifyContent="flex-end" mt={2}> | ||||
|                     <Button variant="contained" onClick={() => setOpen(true)} className="button-gold"> | ||||
|                         Add Category | ||||
|                     </Button> | ||||
|                 </Box> | ||||
|             </Box> | ||||
|         </SectionContainer> | ||||
|     ); | ||||
|       <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> | ||||
|   ); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user