285 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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 AddOrEditProductCollectionForm from './AddOrEditProductCollectionForm';
 | |
| 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 Product Collection' : 'Add Product Collection'}</DialogTitle>
 | |
|         <DialogContent>
 | |
|           <AddOrEditProductCollectionForm
 | |
|             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>
 | |
|   );
 | |
| }
 | 
