From 0a74c7a22a96782d2160d8b733b856b2cc7ec8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20White?= Date: Mon, 1 Sep 2025 17:19:06 -0600 Subject: [PATCH] feat: added categories --- public/logo.png | Bin 3379 -> 0 bytes src/App.jsx | 15 +- src/api/CategoriesApi.js | 54 ++++ src/components/MenuDrawerPrivate.jsx | 34 ++- .../categories/AddOrEditCategoryForm.jsx | 144 ++++++---- src/private/categories/Categories.jsx | 256 +++++++++--------- 6 files changed, 296 insertions(+), 207 deletions(-) delete mode 100644 public/logo.png create mode 100644 src/api/CategoriesApi.js diff --git a/public/logo.png b/public/logo.png deleted file mode 100644 index 95e03f53753d1e3735d1a08a0df7ea43c32c1449..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3379 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 JRH->2{|C9icN+iz diff --git a/src/App.jsx b/src/App.jsx index 9ca1440..28b05cc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import Footer from './components/Footer'; import Dashboard from './private/dashboard/Dashboard'; import UserManagement from './private/users/UserManagement'; import FurnitureVariantManagement from './private/fornitures/FurnitureVariantManagement'; +import Categories from './private/categories/Categories'; import LoginPage from './private/LoginPage'; import { Routes, Route, Navigate } from 'react-router-dom'; import { useAuth } from './context/AuthContext'; @@ -24,10 +25,8 @@ export default function App() { const mainLeft = isMobile ? 0 : (drawerExpanded ? DRAWER_EXPANDED : DRAWER_COLLAPSED); - // Evita flicker mientras se restaura sesión if (initializing) return null; - // === RUTAS PÚBLICAS (sin shell) === if (!user) { return ( @@ -37,14 +36,9 @@ export default function App() { ); } - // === RUTAS PRIVADAS (con shell) === return ( <> - + setCurrentView(value)} @@ -64,9 +58,7 @@ export default function App() { }} > - {/* Si ya está autenticado, /login redirige al dashboard */} } /> - )} + {currentView === '/ProductsManagement/CatalogManagement/Categories' && ( + + )} } /> diff --git a/src/api/CategoriesApi.js b/src/api/CategoriesApi.js new file mode 100644 index 0000000..a61128e --- /dev/null +++ b/src/api/CategoriesApi.js @@ -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(); + } +} diff --git a/src/components/MenuDrawerPrivate.jsx b/src/components/MenuDrawerPrivate.jsx index 8237123..ce6e1ee 100644 --- a/src/components/MenuDrawerPrivate.jsx +++ b/src/components/MenuDrawerPrivate.jsx @@ -40,11 +40,13 @@ const menuData = [ { title: 'Categories' } ] }, - { title: 'Products', - children: [ - { title: 'AR Assets Library Management' }, - { title: 'Media Management' }, - ] }, + { + title: 'Products', + children: [ + { title: 'AR Assets Library Management' }, + { title: 'Media Management' }, + ] + }, { title: 'Product Collections' }, ] } @@ -54,14 +56,15 @@ const menuData = [ title: 'Customers', icon: , children: [ - { title: 'CRM', + { + title: 'CRM', children: [ - { title: 'Customer List' }, - { title: 'Projects' }, + { title: 'Customer List' }, + { title: 'Projects' }, { title: 'Customer Collections' }, ] - }, - + }, + { title: 'Sales', children: [ @@ -86,12 +89,13 @@ const menuData = [ icon: , children: [ { title: 'Users Management' }, - { title: 'Access Control', + { + title: 'Access Control', children: [ { title: 'Roles' }, { title: 'Permissions' }, ] - }, + }, ] }, { @@ -157,6 +161,8 @@ export default function MenuDrawerPrivate({ onSelect?.('/Users/UserManagement'); } else if (node.title === 'Product Collections') { onSelect?.('/ProductsManagement/CatalogManagement/ProductCollections'); + } else if (node.title === 'Categories') { + onSelect?.('/ProductsManagement/CatalogManagement/Categories'); } else { onSelect?.(node.title); } @@ -199,7 +205,7 @@ export default function MenuDrawerPrivate({ {hasChildren && !collapsed && ( - {node.children.map((child, idx) => renderNode(child, `${key}-`))} + {node.children.map((child) => renderNode(child, `${key}-`))} )} @@ -303,4 +309,4 @@ export default function MenuDrawerPrivate({ ); -} \ No newline at end of file +} diff --git a/src/private/categories/AddOrEditCategoryForm.jsx b/src/private/categories/AddOrEditCategoryForm.jsx index e86a2d6..e6469f2 100644 --- a/src/private/categories/AddOrEditCategoryForm.jsx +++ b/src/private/categories/AddOrEditCategoryForm.jsx @@ -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 ( - - - - Category Details - - - - - - - - - - ); + 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 ( + + + {category._Id ? 'Edit Category' : 'Add Category'} + + + + + + + + + + + + ); } diff --git a/src/private/categories/Categories.jsx b/src/private/categories/Categories.jsx index 3b2fba3..41feccb 100644 --- a/src/private/categories/Categories.jsx +++ b/src/private/categories/Categories.jsx @@ -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) => ( - - handleEditClick(params)} - > - - - handleDeleteClick(params.row)} - > - - - - ) - } - ]; + const handleFormDone = async () => { + await loadData(); + setOpen(false); + setEditingCategory(null); + }; - return ( - - - Categories - + 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) => ( + + handleEditClick(params)}> + handleDeleteClick(params.row)}> + + ), + }, + ]; - { setOpen(false); setEditingCategory(null); }} fullWidth> - {editingCategory ? 'Edit Category' : 'Add Category'} - - { setOpen(false); setEditingCategory(null); }} /> - - + return ( + + + Categories + + - setConfirmOpen(false)}> - Confirm Delete - - - Are you sure you want to delete {rowToDelete?.name}? - - - - - - - + r._id || r._Id || r.id || r.Id} + /> - - ({ top: 8, bottom: 8 })} - /> + { setOpen(false); setEditingCategory(null); }} fullWidth> + {editingCategory ? 'Edit Category' : 'Add Category'} + + { setOpen(false); setEditingCategory(null); }} + /> + + - - - - - - ); + setConfirmOpen(false)}> + Delete Category + + + + + + + + + ); }