chore: change name of folder and page
This commit is contained in:
		
							
								
								
									
										284
									
								
								src/private/catalogs/ProductCollections.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								src/private/catalogs/ProductCollections.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,284 @@ | ||||
| import SectionContainer from '../../components/SectionContainer'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | ||||
| import { DataGrid } from '@mui/x-data-grid'; | ||||
| import { | ||||
|   Typography, Button, Dialog, DialogTitle, DialogContent, | ||||
|   IconButton, Box, FormControlLabel, Switch, Tooltip | ||||
| } from '@mui/material'; | ||||
| import EditRoundedIcon from '@mui/icons-material/EditRounded'; | ||||
| import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; | ||||
| import AddRoundedIcon from '@mui/icons-material/AddRounded'; | ||||
| import AddOrEditFurnitureVariantForm from './AddOrEditFurnitureVariantForm'; | ||||
| import FurnitureVariantApi from '../../api/furnitureVariantApi'; | ||||
| import CategoriesApi from '../../api/CategoriesApi'; | ||||
| import TagTypeApi from '../../api/TagTypeApi'; | ||||
| import { useAuth } from '../../context/AuthContext'; | ||||
| import useApiToast from '../../hooks/useApiToast'; | ||||
|  | ||||
| const parsePrice = (p) => { | ||||
|   if (p == null) return 0; | ||||
|   if (typeof p === 'number') return p; | ||||
|   if (typeof p === 'string') return Number(p) || 0; | ||||
|   if (typeof p === 'object' && p.$numberDecimal) return Number(p.$numberDecimal) || 0; | ||||
|   return 0; | ||||
| }; | ||||
|  | ||||
| const TYPE_NAMES = { | ||||
|   category: 'Furniture category', | ||||
|   provider: 'Provider', | ||||
|   color: 'Color', | ||||
|   line: 'Line', | ||||
|   currency: 'Currency', | ||||
|   material: 'Material', | ||||
|   legs: 'Legs', | ||||
|   origin: 'Origin', | ||||
| }; | ||||
|  | ||||
| export default function ProductCollections() { | ||||
|   const { user } = useAuth(); | ||||
|   const token = user?.thalosToken || localStorage.getItem('thalosToken'); | ||||
|  | ||||
|   const api = useMemo(() => new FurnitureVariantApi(token), [token]); | ||||
|   const tagTypeApi = useMemo(() => new TagTypeApi(token), [token]); | ||||
|   const categoriesApi = useMemo(() => new CategoriesApi(token), [token]); | ||||
|  | ||||
|   const toast = useApiToast(); | ||||
|  | ||||
|   const [rows, setRows] = useState([]); | ||||
|   const [rawRows, setRawRows] = useState([]); | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const [editRow, setEditRow] = useState(null); | ||||
|   const [showInactive, setShowInactive] = useState(false); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|  | ||||
|   // Tags | ||||
|   const [loadingTags, setLoadingTags] = useState(true); | ||||
|   const [typeMap, setTypeMap] = useState({}); | ||||
|   const [byType, setByType] = useState({ | ||||
|     [TYPE_NAMES.category]: [], | ||||
|     [TYPE_NAMES.provider]: [], | ||||
|     [TYPE_NAMES.color]: [], | ||||
|     [TYPE_NAMES.line]: [], | ||||
|     [TYPE_NAMES.currency]: [], | ||||
|     [TYPE_NAMES.material]: [], | ||||
|     [TYPE_NAMES.legs]: [], | ||||
|     [TYPE_NAMES.origin]: [], | ||||
|   }); | ||||
|  | ||||
|   const buildLabelResolver = (typeName) => { | ||||
|     const list = byType[typeName] || []; | ||||
|     return (value) => { | ||||
|       if (!value && value !== 0) return '—'; | ||||
|       if (list.some(t => t.tagName === value)) return value; // ya es tagName | ||||
|       const found = list.find(t => | ||||
|         t.id === value || | ||||
|         t._id === value || | ||||
|         t._id?.$oid === value || | ||||
|         t.slug === value | ||||
|       ); | ||||
|       return found?.tagName || String(value); | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   const labelCategory = useMemo(() => buildLabelResolver(TYPE_NAMES.category), [byType]); | ||||
|   const labelProvider = useMemo(() => buildLabelResolver(TYPE_NAMES.provider), [byType]); | ||||
|   const labelColor = useMemo(() => buildLabelResolver(TYPE_NAMES.color), [byType]); | ||||
|   const labelLine = useMemo(() => buildLabelResolver(TYPE_NAMES.line), [byType]); | ||||
|   const labelCurrency = useMemo(() => buildLabelResolver(TYPE_NAMES.currency), [byType]); | ||||
|   const labelMaterial = useMemo(() => buildLabelResolver(TYPE_NAMES.material), [byType]); | ||||
|   const labelLegs = useMemo(() => buildLabelResolver(TYPE_NAMES.legs), [byType]); | ||||
|   const labelOrigin = useMemo(() => buildLabelResolver(TYPE_NAMES.origin), [byType]); | ||||
|  | ||||
|   // Cargar TagTypes + Tags | ||||
|   useEffect(() => { | ||||
|     let mounted = true; | ||||
|     (async () => { | ||||
|       try { | ||||
|         const [types, tags] = await Promise.all([tagTypeApi.getAll(), categoriesApi.getAll()]); | ||||
|         const tmap = {}; | ||||
|         types?.forEach(t => { | ||||
|           if (!t?.typeName || !t?._id) return; | ||||
|           tmap[t.typeName] = t._id; | ||||
|         }); | ||||
|         const group = (tname) => { | ||||
|           const tid = tmap[tname]; | ||||
|           if (!tid) return []; | ||||
|           return (tags || []).filter(tag => tag?.typeId === tid); | ||||
|         }; | ||||
|         if (mounted) { | ||||
|           setTypeMap(tmap); | ||||
|           setByType({ | ||||
|             [TYPE_NAMES.category]: group(TYPE_NAMES.category), | ||||
|             [TYPE_NAMES.provider]: group(TYPE_NAMES.provider), | ||||
|             [TYPE_NAMES.color]: group(TYPE_NAMES.color), | ||||
|             [TYPE_NAMES.line]: group(TYPE_NAMES.line), | ||||
|             [TYPE_NAMES.currency]: group(TYPE_NAMES.currency), | ||||
|             [TYPE_NAMES.material]: group(TYPE_NAMES.material), | ||||
|             [TYPE_NAMES.legs]: group(TYPE_NAMES.legs), | ||||
|             [TYPE_NAMES.origin]: group(TYPE_NAMES.origin), | ||||
|           }); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         console.error('Failed loading TagTypes/Tags', e); | ||||
|       } finally { | ||||
|         if (mounted) setLoadingTags(false); | ||||
|       } | ||||
|     })(); | ||||
|     return () => { mounted = false; }; | ||||
|   }, [tagTypeApi, categoriesApi]); | ||||
|  | ||||
|   // Cargar variants | ||||
|   const load = async () => { | ||||
|     try { | ||||
|       setLoading(true); | ||||
|       const data = await api.getAllVariants(); | ||||
|       const normalized = (data || []).map((r, idx) => ({ | ||||
|         id: r.id || r._id || `row-${idx}`, | ||||
|         _Id: r._id || r._Id || '', | ||||
|         modelId: r.modelId ?? '', | ||||
|         name: r.name ?? '', | ||||
|         categoryId: r.categoryId ?? '', | ||||
|         providerId: r.providerId ?? '', | ||||
|         color: r.color ?? '', | ||||
|         line: r.line ?? '', | ||||
|         stock: Number(r.stock ?? 0), | ||||
|         price: parsePrice(r.price), | ||||
|         currency: r.currency ?? 'USD', | ||||
|         attributes: { | ||||
|           material: r?.attributes?.material ?? '', | ||||
|           legs: r?.attributes?.legs ?? '', | ||||
|           origin: r?.attributes?.origin ?? '', | ||||
|         }, | ||||
|         status: r.status ?? 'Active', | ||||
|         createdAt: r.createdAt ?? null, | ||||
|         createdBy: r.createdBy ?? null, | ||||
|       })); | ||||
|       setRawRows(normalized); | ||||
|       setRows(normalized.filter(r => showInactive ? true : r.status !== 'Inactive')); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error(err?.message || 'Error loading variants'); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { load(); /* eslint-disable-next-line */ }, []); | ||||
|   useEffect(() => { | ||||
|     setRows(rawRows.filter(r => showInactive ? true : r.status !== 'Inactive')); | ||||
|   }, [showInactive, rawRows]); | ||||
|  | ||||
|   const columns = [ | ||||
|     { field: 'modelId', headerName: 'Model Id', width: 220 }, | ||||
|     { field: 'name', headerName: 'Name', width: 200 }, | ||||
|     { field: 'categoryId', headerName: 'Category', width: 170, valueGetter: (p) => labelCategory(p?.row?.categoryId) }, | ||||
|     { field: 'providerId', headerName: 'Provider', width: 170, valueGetter: (p) => labelProvider(p?.row?.providerId) }, | ||||
|     { field: 'color', headerName: 'Color', width: 130, valueGetter: (p) => labelColor(p?.row?.color) }, | ||||
|     { field: 'line', headerName: 'Line', width: 130, valueGetter: (p) => labelLine(p?.row?.line) }, | ||||
|     { | ||||
|       field: 'price', | ||||
|       headerName: 'Price', | ||||
|       width: 130, | ||||
|       type: 'number', | ||||
|       valueGetter: (p) => parsePrice(p?.row?.price), | ||||
|       renderCell: (p) => { | ||||
|         const currency = labelCurrency(p?.row?.currency || 'USD'); | ||||
|         const val = parsePrice(p?.row?.price); | ||||
|         try { | ||||
|           return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency || 'USD' }).format(val); | ||||
|         } catch { | ||||
|           return `${currency} ${val.toFixed(2)}`; | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { field: 'currency', headerName: 'Currency', width: 120, valueGetter: (p) => labelCurrency(p?.row?.currency) }, | ||||
|     { field: 'stock', headerName: 'Stock', width: 100, type: 'number', valueGetter: (p) => Number(p?.row?.stock ?? 0) }, | ||||
|     { field: 'attributes.material', headerName: 'Material', width: 150, valueGetter: (p) => labelMaterial(p?.row?.attributes?.material) }, | ||||
|     { field: 'attributes.legs', headerName: 'Legs', width: 140, valueGetter: (p) => labelLegs(p?.row?.attributes?.legs) }, | ||||
|     { field: 'attributes.origin', headerName: 'Origin', width: 150, valueGetter: (p) => labelOrigin(p?.row?.attributes?.origin) }, | ||||
|     { field: 'status', headerName: 'Status', width: 120 }, | ||||
|     { | ||||
|       field: 'actions', | ||||
|       headerName: '', | ||||
|       sortable: false, | ||||
|       width: 110, | ||||
|       renderCell: (p) => ( | ||||
|         <Box display="flex" gap={1}> | ||||
|           <Tooltip title="Edit"> | ||||
|             <IconButton size="small" onClick={() => { setEditRow(p.row); setOpen(true); }}> | ||||
|               <EditRoundedIcon fontSize="small" /> | ||||
|             </IconButton> | ||||
|           </Tooltip> | ||||
|           <Tooltip title={p.row.status === 'Active' ? 'Deactivate' : 'Activate'}> | ||||
|             <IconButton | ||||
|               size="small" | ||||
|               onClick={async () => { | ||||
|                 try { | ||||
|                   const updated = { ...p.row, status: p.row.status === 'Active' ? 'Inactive' : 'Active' }; | ||||
|                   await api.updateVariant(updated); | ||||
|                   setRawRows(prev => prev.map(r => r.id === p.row.id ? updated : r)); | ||||
|                 } catch (err) { | ||||
|                   console.error(err); | ||||
|                   toast.error(err?.message || 'Error updating status'); | ||||
|                 } | ||||
|               }} | ||||
|             > | ||||
|               <DeleteRoundedIcon fontSize="small" /> | ||||
|             </IconButton> | ||||
|           </Tooltip> | ||||
|         </Box> | ||||
|       ) | ||||
|     }, | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <SectionContainer> | ||||
|       <Box display="flex" alignItems="center" justifyContent="space-between" mb={2}> | ||||
|         <Typography variant="h6">Furniture Variants</Typography> | ||||
|         <Box display="flex" alignItems="center" gap={2}> | ||||
|           <FormControlLabel control={<Switch checked={showInactive} onChange={(_, v) => setShowInactive(v)} />} label="Show Inactive" /> | ||||
|           <Button variant="contained" className="button-gold" startIcon={<AddRoundedIcon />} onClick={() => { setEditRow(null); setOpen(true); }}> | ||||
|             Add Variant | ||||
|           </Button> | ||||
|         </Box> | ||||
|       </Box> | ||||
|  | ||||
|       <Box sx={{ width: '100%', height: 560 }}> | ||||
|         <DataGrid | ||||
|           rows={rows} | ||||
|           columns={columns} | ||||
|           disableRowSelectionOnClick | ||||
|           loading={loading || loadingTags} | ||||
|           pageSizeOptions={[10, 25, 50]} | ||||
|           initialState={{ | ||||
|             pagination: { paginationModel: { pageSize: 10 } }, | ||||
|             columns: { columnVisibilityModel: { id: false, _Id: false } }, | ||||
|           }} | ||||
|           getRowHeight={() => 'auto'} | ||||
|           sx={{ | ||||
|             '& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center' }, | ||||
|             '& .MuiDataGrid-columnHeader': { display: 'flex', alignItems: 'center' }, | ||||
|           }} | ||||
|         /> | ||||
|       </Box> | ||||
|  | ||||
|       <Dialog open={open} onClose={() => setOpen(false)} maxWidth="md" fullWidth> | ||||
|         <DialogTitle>{editRow ? 'Edit Variant' : 'Add Variant'}</DialogTitle> | ||||
|         <DialogContent> | ||||
|           <AddOrEditFurnitureVariantForm | ||||
|             initialData={editRow} | ||||
|             onAdd={(saved) => { | ||||
|               setOpen(false); | ||||
|               if (editRow) { | ||||
|                 setRawRows(prev => prev.map(r => (r.id === editRow.id ? { ...saved } : r))); | ||||
|               } else { | ||||
|                 setRawRows(prev => [{ ...saved, id: saved.id || saved._id || `row-${Date.now()}` }, ...prev]); | ||||
|               } | ||||
|             }} | ||||
|             onCancel={() => setOpen(false)} | ||||
|           /> | ||||
|         </DialogContent> | ||||
|       </Dialog> | ||||
|     </SectionContainer> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Rodolfo Ruiz
					Rodolfo Ruiz