From e55d9a8cf42d523cf40bcc2366bcb78165e5a621 Mon Sep 17 00:00:00 2001 From: Rodolfo Ruiz Date: Mon, 1 Sep 2025 13:45:22 -0600 Subject: [PATCH] chore: add new forniture page --- src/App.jsx | 2 + src/api/furnitureVariantApi.js | 55 ++++ src/components/MenuDrawerPrivate.jsx | 2 + .../AddOrEditFurnitureVariantForm.jsx | 181 +++++++++++++ .../fornitures/FurnitureVariantManagement.jsx | 254 ++++++++++++++++++ 5 files changed, 494 insertions(+) create mode 100644 src/api/furnitureVariantApi.js create mode 100644 src/private/fornitures/AddOrEditFurnitureVariantForm.jsx create mode 100644 src/private/fornitures/FurnitureVariantManagement.jsx diff --git a/src/App.jsx b/src/App.jsx index 15cbe31..a1d32db 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import MenuDrawerPrivate, { OPEN_WIDTH, MINI_WIDTH } from './components/MenuDraw import Footer from './components/Footer'; import Dashboard from './private/dashboard/Dashboard'; import UserManagement from './private/users/UserManagement'; +import FurnitureVariantManagement from './private/fornitures/FurnitureVariantManagement'; import LoginPage from './private/LoginPage'; import { Routes, Route, Navigate } from 'react-router-dom'; import { useAuth } from './context/AuthContext'; @@ -66,6 +67,7 @@ export default function App() { {zone === 'public' && currentView === 'Dashboard' && } {zone === 'public' && currentView === '/Users/UserManagement' && } + {zone === 'public' && currentView === '/ProductsManagement/CatalogManagement/ProductCollections' && } } /> diff --git a/src/api/furnitureVariantApi.js b/src/api/furnitureVariantApi.js new file mode 100644 index 0000000..d815c9c --- /dev/null +++ b/src/api/furnitureVariantApi.js @@ -0,0 +1,55 @@ +export default class FurnitureVariantApi { + constructor(token) { + this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/FurnitureVariant'; + this.token = token; + } + + headers(json = true) { + return { + accept: 'application/json', + ...(json ? { 'Content-Type': 'application/json' } : {}), + ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), + }; + } + + async getAllVariants() { + 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(); + } + + // Assuming similar endpoints; adjust names if backend differs. + async createVariant(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 updateVariant(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 deleteVariant(payload) { + // If your API is soft-delete via Update status, reuse updateVariant. + 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(); + } +} \ No newline at end of file diff --git a/src/components/MenuDrawerPrivate.jsx b/src/components/MenuDrawerPrivate.jsx index 286c7ca..8237123 100644 --- a/src/components/MenuDrawerPrivate.jsx +++ b/src/components/MenuDrawerPrivate.jsx @@ -155,6 +155,8 @@ export default function MenuDrawerPrivate({ } else { if (node.title === 'Users Management') { onSelect?.('/Users/UserManagement'); + } else if (node.title === 'Product Collections') { + onSelect?.('/ProductsManagement/CatalogManagement/ProductCollections'); } else { onSelect?.(node.title); } diff --git a/src/private/fornitures/AddOrEditFurnitureVariantForm.jsx b/src/private/fornitures/AddOrEditFurnitureVariantForm.jsx new file mode 100644 index 0000000..eca2ca9 --- /dev/null +++ b/src/private/fornitures/AddOrEditFurnitureVariantForm.jsx @@ -0,0 +1,181 @@ +// src/private/furniture/AddOrEditFurnitureVariantForm.jsx +import { useEffect, useMemo, useState } from 'react'; +import { Box, Button, TextField, MenuItem, Grid } from '@mui/material'; +import FurnitureVariantApi from '../../api/furnitureVariantApi'; +import { useAuth } from '../../context/AuthContext'; + +export default function AddOrEditFurnitureVariantForm({ initialData, onAdd, onCancel }) { + const { user } = useAuth(); + const token = user?.thalosToken || localStorage.getItem('thalosToken'); + const api = useMemo(() => (new FurnitureVariantApi(token)), [token]); + + const [form, setForm] = useState({ + _Id: '', + modelId: '', + name: '', + color: '', + line: '', + stock: 0, + price: 0, + currency: 'USD', + categoryId: '', + providerId: '', + attributes: { material: '', legs: '', origin: '' }, + status: 'Active', + }); + + useEffect(() => { + if (initialData) { + setForm({ + _Id: initialData._id || initialData._Id || '', + modelId: initialData.modelId ?? '', + name: initialData.name ?? '', + color: initialData.color ?? '', + line: initialData.line ?? '', + stock: initialData.stock ?? 0, + price: initialData.price ?? 0, + currency: initialData.currency ?? 'USD', + categoryId: initialData.categoryId ?? '', + providerId: initialData.providerId ?? '', + attributes: { + material: initialData?.attributes?.material ?? '', + legs: initialData?.attributes?.legs ?? '', + origin: initialData?.attributes?.origin ?? '', + }, + status: initialData.status ?? 'Active', + }); + } else { + setForm({ + _Id: '', + modelId: '', + name: '', + color: '', + line: '', + stock: 0, + price: 0, + currency: 'USD', + categoryId: '', + providerId: '', + attributes: { material: '', legs: '', origin: '' }, + status: 'Active', + }); + } + }, [initialData]); + + const setVal = (name, value) => setForm((p) => ({ ...p, [name]: value })); + const setAttr = (name, value) => setForm((p) => ({ ...p, attributes: { ...p.attributes, [name]: value } })); + + const handleSubmit = async () => { + try { + if (form._Id) { + // UPDATE + const payload = { + _Id: form._Id, + modelId: form.modelId, + name: form.name, + color: form.color, + line: form.line, + stock: Number(form.stock) || 0, + price: Number(form.price) || 0, + currency: form.currency, + categoryId: form.categoryId, + providerId: form.providerId, + attributes: { + material: form.attributes.material, + legs: form.attributes.legs, + origin: form.attributes.origin, + }, + status: form.status, + }; + await api.updateVariant(payload); + } else { + // CREATE + const payload = { + modelId: form.modelId, + name: form.name, + color: form.color, + line: form.line, + stock: Number(form.stock) || 0, + price: Number(form.price) || 0, + currency: form.currency, + categoryId: form.categoryId, + providerId: form.providerId, + attributes: { + material: form.attributes.material, + legs: form.attributes.legs, + origin: form.attributes.origin, + }, + status: form.status, + }; + await api.createVariant(payload); + } + onAdd?.(); + } catch (err) { + console.error('Submit variant failed:', err); + } + }; + + return ( + + + + setVal('modelId', e.target.value)} /> + + + setVal('name', e.target.value)} /> + + + + setVal('color', e.target.value)} /> + + + setVal('line', e.target.value)} /> + + + setVal('stock', e.target.value)} /> + + + setVal('price', e.target.value)} /> + + + + setVal('currency', e.target.value)} /> + + + setVal('categoryId', e.target.value)} /> + + + setVal('providerId', e.target.value)} /> + + + + setAttr('material', e.target.value)} /> + + + setAttr('legs', e.target.value)} /> + + + setAttr('origin', e.target.value)} /> + + + + setVal('status', e.target.value)} + > + Active + Inactive + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/private/fornitures/FurnitureVariantManagement.jsx b/src/private/fornitures/FurnitureVariantManagement.jsx new file mode 100644 index 0000000..725d8ac --- /dev/null +++ b/src/private/fornitures/FurnitureVariantManagement.jsx @@ -0,0 +1,254 @@ +import SectionContainer from '../../components/SectionContainer'; +import { useEffect, useRef, useState } from 'react'; +import { DataGrid } from '@mui/x-data-grid'; +import { + Typography, Button, Dialog, DialogTitle, DialogContent, + IconButton, Box +} from '@mui/material'; +import EditRoundedIcon from '@mui/icons-material/EditRounded'; +import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; +import AddOrEditFurnitureVariantForm from './AddOrEditFurnitureVariantForm'; +import FurnitureVariantApi from '../../api/furnitureVariantApi'; +import { useAuth } from '../../context/AuthContext'; +import useApiToast from '../../hooks/useApiToast'; + +const columnsBase = [ + { field: 'modelId', headerName: 'Model Id', width: 260 }, + { field: 'name', headerName: 'Name', width: 220 }, + { field: 'color', headerName: 'Color', width: 160 }, + { field: 'line', headerName: 'Line', width: 160 }, + { field: 'stock', headerName: 'Stock', width: 100, type: 'number' }, + { field: 'price', headerName: 'Price', width: 120, type: 'number', + valueFormatter: (p) => p?.value != null ? Number(p.value).toFixed(2) : '—' + }, + { field: 'currency', headerName: 'Currency', width: 120 }, + { field: 'categoryId', headerName: 'Category Id', width: 280 }, + { field: 'providerId', headerName: 'Provider Id', width: 280 }, + { + field: 'attributes.material', + headerName: 'Material', + width: 160, + valueGetter: (p) => p?.row?.attributes?.material ?? '—' + }, + { + field: 'attributes.legs', + headerName: 'Legs', + width: 160, + valueGetter: (p) => p?.row?.attributes?.legs ?? '—' + }, + { + field: 'attributes.origin', + headerName: 'Origin', + width: 160, + valueGetter: (p) => p?.row?.attributes?.origin ?? '—' + }, + { field: 'status', headerName: 'Status', width: 120 }, + { + field: 'createdAt', + headerName: 'Created At', + width: 180, + valueFormatter: (p) => p?.value ? new Date(p.value).toLocaleString() : '—' + }, + { field: 'createdBy', headerName: 'Created By', width: 160, valueGetter: (p) => p?.row?.createdBy ?? '—' }, + { + field: 'updatedAt', + headerName: 'Updated At', + width: 180, + valueFormatter: (p) => p?.value ? new Date(p.value).toLocaleString() : '—' + }, + { field: 'updatedBy', headerName: 'Updated By', width: 160, valueGetter: (p) => p?.row?.updatedBy ?? '—' }, +]; + +export default function FurnitureVariantManagement() { + const { user } = useAuth(); + const token = user?.thalosToken || localStorage.getItem('thalosToken'); + const apiRef = useRef(null); + + const [rows, setRows] = useState([]); + const [open, setOpen] = useState(false); + const [editingData, setEditingData] = useState(null); + const [confirmOpen, setConfirmOpen] = useState(false); + const [rowToDelete, setRowToDelete] = useState(null); + const { handleError } = useApiToast(); + const hasLoaded = useRef(false); + + useEffect(() => { + apiRef.current = new FurnitureVariantApi(token); + }, [token]); + + useEffect(() => { + if (!hasLoaded.current) { + loadData(); + hasLoaded.current = true; + } + }, []); + + const loadData = async () => { + try { + const data = await apiRef.current.getAllVariants(); + setRows(Array.isArray(data) ? data : []); + } catch (err) { + console.error('Error loading variants:', err); + handleError(err, 'Failed to load furniture variants'); + setRows([]); + } + }; + + const handleEditClick = (params) => { + if (!params?.row) return; + const r = params.row; + const normalized = { + _id: r._id || r._Id || '', + id: r.id || r.Id || '', + modelId: r.modelId ?? '', + name: r.name ?? '', + color: r.color ?? '', + line: r.line ?? '', + stock: r.stock ?? 0, + price: r.price ?? 0, + currency: r.currency ?? 'USD', + categoryId: r.categoryId ?? '', + providerId: r.providerId ?? '', + attributes: { + material: r?.attributes?.material ?? '', + legs: r?.attributes?.legs ?? '', + origin: r?.attributes?.origin ?? '', + }, + status: r.status ?? 'Active', + }; + setEditingData(normalized); + setOpen(true); + }; + + const handleDeleteClick = (row) => { + setRowToDelete(row); + setConfirmOpen(true); + }; + + const handleConfirmDelete = async () => { + try { + if (!apiRef.current || !rowToDelete?._id) throw new Error('Missing API or id'); + // If your inventory BFF uses soft delete via Update (status=Inactive), do this: + const payload = { + _Id: rowToDelete._id || rowToDelete._Id, + modelId: rowToDelete.modelId, + name: rowToDelete.name, + color: rowToDelete.color, + line: rowToDelete.line, + stock: rowToDelete.stock, + price: rowToDelete.price, + currency: rowToDelete.currency, + categoryId: rowToDelete.categoryId, + providerId: rowToDelete.providerId, + attributes: { + material: rowToDelete?.attributes?.material ?? '', + legs: rowToDelete?.attributes?.legs ?? '', + origin: rowToDelete?.attributes?.origin ?? '', + }, + status: 'Inactive', + }; + // Prefer update soft-delete; if you truly have DELETE, switch to apiRef.current.deleteVariant({ _Id: ... }) + await apiRef.current.updateVariant(payload); + await loadData(); + } catch (e) { + console.error('Delete failed:', e); + } finally { + setConfirmOpen(false); + setRowToDelete(null); + } + }; + + const columns = [ + { + field: 'actions', + headerName: '', + width: 130, + renderCell: (params) => ( + + handleEditClick(params)} + > + + + handleDeleteClick(params?.row)} + > + + + + ) + }, + ...columnsBase, + ]; + + return ( + + { setOpen(false); setEditingData(null); }} maxWidth="md" fullWidth> + {editingData ? 'Edit Furniture Variant' : 'Add Furniture Variant'} + + { setOpen(false); setEditingData(null); }} + onAdd={async () => { + await loadData(); + setOpen(false); + setEditingData(null); + }} + /> + + + + setConfirmOpen(false)}> + Confirm Delete + + + Are you sure you want to delete {rowToDelete?.name}? + + + + + + + + + + ({ top: 4, bottom: 4 })} + getRowId={(row) => row._id || row.id || row.modelId} + autoHeight + disableColumnMenu + getRowHeight={() => 'auto'} + sx={{ + '& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center' }, + '& .MuiDataGrid-columnHeader': { display: 'flex', alignItems: 'center' }, + }} + /> + + + + + + ); +} \ No newline at end of file