From 29aa8e30e7c745d7d32b91690ee84d2e6c141bce Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 7 Jan 2026 17:52:10 -0300 Subject: [PATCH] Feat ERP 2 --- frontend/admin-panel/package-lock.json | 49 +++ frontend/admin-panel/package.json | 1 + frontend/admin-panel/src/App.tsx | 10 + .../src/components/Companies/CompanyModal.tsx | 116 +++++++ .../src/components/Products/ProductModal.tsx | 274 ++++++++++++++++ .../src/layouts/ProtectedLayout.tsx | 136 +++++--- .../src/pages/Companies/CalendarManager.tsx | 135 ++++++++ .../src/pages/Companies/CompanyManager.tsx | 127 ++++++++ .../src/pages/Finance/CreditManager.tsx | 164 ++++++++++ .../src/pages/Products/ProductManager.tsx | 159 +++++++++ .../src/pages/Reports/SettlementReport.tsx | 78 +++++ .../src/services/calendarService.ts | 17 + .../src/services/companyService.ts | 22 ++ .../src/services/financeService.ts | 25 ++ .../src/services/productService.ts | 49 +++ frontend/admin-panel/src/types/Calendar.ts | 6 + frontend/admin-panel/src/types/Company.ts | 9 + frontend/admin-panel/src/types/Finance.ts | 17 + frontend/admin-panel/src/types/Product.ts | 28 ++ frontend/admin-panel/tsconfig.app.json | 2 +- frontend/counter-panel/src/App.tsx | 4 +- .../src/components/POS/AdEditorModal.tsx | 299 +++++++++++++++++ .../src/components/POS/ProductSearch.tsx | 100 ++++++ .../src/components/PaymentModal.tsx | 255 +++++++++------ .../src/layouts/CounterLayout.tsx | 4 +- .../src/pages/UniversalPosPage.tsx | 302 ++++++++++++++++++ .../src/services/companyService.ts | 12 + .../src/services/financeService.ts | 12 + .../src/services/orderService.ts | 32 ++ .../src/services/productService.ts | 49 +++ frontend/counter-panel/src/store/cartStore.ts | 84 +++++ frontend/counter-panel/src/types/Company.ts | 9 + frontend/counter-panel/src/types/Finance.ts | 11 + frontend/counter-panel/src/types/Order.ts | 28 ++ frontend/counter-panel/src/types/Product.ts | 28 ++ frontend/counter-panel/src/types/Seller.ts | 7 + frontend/counter-panel/tsconfig.app.json | 2 +- .../Controllers/CalendarController.cs | 40 +++ .../Controllers/CompaniesController.cs | 56 ++++ .../Controllers/ProductsController.cs | 19 ++ .../Controllers/ReportsController.cs | 17 +- src/SIGCM.Domain/Entities/OrderItem.cs | 35 +- src/SIGCM.Domain/Entities/ProductPrice.cs | 12 + .../Entities/PublicationBlackout.cs | 9 + .../Interfaces/ICalendarRepository.cs | 11 + .../Interfaces/ICompanyRepository.cs | 12 + .../Interfaces/IProductRepository.cs | 2 + .../Interfaces/IReportRepository.cs | 14 + .../DependencyInjection.cs | 3 + .../Repositories/CalendarRepository.cs | 43 +++ .../Repositories/CompanyRepository.cs | 53 +++ .../Repositories/ProductRepository.cs | 35 ++ .../Repositories/ReportRepository.cs | 37 +++ .../Services/OrderService.cs | 250 +++++++++------ 54 files changed, 3035 insertions(+), 275 deletions(-) create mode 100644 frontend/admin-panel/src/components/Companies/CompanyModal.tsx create mode 100644 frontend/admin-panel/src/components/Products/ProductModal.tsx create mode 100644 frontend/admin-panel/src/pages/Companies/CalendarManager.tsx create mode 100644 frontend/admin-panel/src/pages/Companies/CompanyManager.tsx create mode 100644 frontend/admin-panel/src/pages/Finance/CreditManager.tsx create mode 100644 frontend/admin-panel/src/pages/Products/ProductManager.tsx create mode 100644 frontend/admin-panel/src/pages/Reports/SettlementReport.tsx create mode 100644 frontend/admin-panel/src/services/calendarService.ts create mode 100644 frontend/admin-panel/src/services/companyService.ts create mode 100644 frontend/admin-panel/src/services/financeService.ts create mode 100644 frontend/admin-panel/src/services/productService.ts create mode 100644 frontend/admin-panel/src/types/Calendar.ts create mode 100644 frontend/admin-panel/src/types/Company.ts create mode 100644 frontend/admin-panel/src/types/Finance.ts create mode 100644 frontend/admin-panel/src/types/Product.ts create mode 100644 frontend/counter-panel/src/components/POS/AdEditorModal.tsx create mode 100644 frontend/counter-panel/src/components/POS/ProductSearch.tsx create mode 100644 frontend/counter-panel/src/pages/UniversalPosPage.tsx create mode 100644 frontend/counter-panel/src/services/companyService.ts create mode 100644 frontend/counter-panel/src/services/financeService.ts create mode 100644 frontend/counter-panel/src/services/orderService.ts create mode 100644 frontend/counter-panel/src/services/productService.ts create mode 100644 frontend/counter-panel/src/store/cartStore.ts create mode 100644 frontend/counter-panel/src/types/Company.ts create mode 100644 frontend/counter-panel/src/types/Finance.ts create mode 100644 frontend/counter-panel/src/types/Order.ts create mode 100644 frontend/counter-panel/src/types/Product.ts create mode 100644 frontend/counter-panel/src/types/Seller.ts create mode 100644 src/SIGCM.API/Controllers/CalendarController.cs create mode 100644 src/SIGCM.API/Controllers/CompaniesController.cs create mode 100644 src/SIGCM.Domain/Entities/ProductPrice.cs create mode 100644 src/SIGCM.Domain/Entities/PublicationBlackout.cs create mode 100644 src/SIGCM.Domain/Interfaces/ICalendarRepository.cs create mode 100644 src/SIGCM.Domain/Interfaces/ICompanyRepository.cs create mode 100644 src/SIGCM.Domain/Interfaces/IReportRepository.cs create mode 100644 src/SIGCM.Infrastructure/Repositories/CalendarRepository.cs create mode 100644 src/SIGCM.Infrastructure/Repositories/CompanyRepository.cs create mode 100644 src/SIGCM.Infrastructure/Repositories/ReportRepository.cs diff --git a/frontend/admin-panel/package-lock.json b/frontend/admin-panel/package-lock.json index 52dc62f..5dd9bf2 100644 --- a/frontend/admin-panel/package-lock.json +++ b/frontend/admin-panel/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/postcss": "^4.1.18", "axios": "^1.13.2", "clsx": "^2.1.1", + "framer-motion": "^12.24.10", "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -3078,6 +3079,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.24.10.tgz", + "integrity": "sha512-8yoyMkCn2RmV9UB9mfmMuzKyenQe909hRQRl0yGBhbZJjZZ9bSU87NIGAruqCXCuTNCA0qHw2LWLrcXLL9GF6A==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.24.10", + "motion-utils": "^12.24.10", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3788,6 +3816,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.24.10.tgz", + "integrity": "sha512-H3HStYaJ6wANoZVNT0ZmYZHGvrpvi9pKJRzsgNEHkdITR4Qd9FFu2e9sH4e2Phr4tKCmyyloex6SOSmv0Tlq+g==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.24.10" + } + }, + "node_modules/motion-utils": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz", + "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4337,6 +4380,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/admin-panel/package.json b/frontend/admin-panel/package.json index 444d8a2..53fd46c 100644 --- a/frontend/admin-panel/package.json +++ b/frontend/admin-panel/package.json @@ -13,6 +13,7 @@ "@tailwindcss/postcss": "^4.1.18", "axios": "^1.13.2", "clsx": "^2.1.1", + "framer-motion": "^12.24.10", "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/frontend/admin-panel/src/App.tsx b/frontend/admin-panel/src/App.tsx index f43c63f..6bf0c63 100644 --- a/frontend/admin-panel/src/App.tsx +++ b/frontend/admin-panel/src/App.tsx @@ -13,6 +13,11 @@ import ListingExplorer from './pages/Listings/ListingExplorer'; import AuditTimeline from './pages/Audit/AuditTimeline'; import ClientManager from './pages/Clients/ClientManager'; import CouponsPage from './pages/Coupons/CouponsPage'; +import ProductManager from './pages/Products/ProductManager'; +import CompanyManager from './pages/Companies/CompanyManager'; +import CreditManager from './pages/Finance/CreditManager'; +import CalendarManager from './pages/Companies/CalendarManager'; +import SettlementReport from './pages/Reports/SettlementReport'; function App() { return ( @@ -28,11 +33,16 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> } /> } /> + } /> + } /> + } /> + } /> diff --git a/frontend/admin-panel/src/components/Companies/CompanyModal.tsx b/frontend/admin-panel/src/components/Companies/CompanyModal.tsx new file mode 100644 index 0000000..016c5e1 --- /dev/null +++ b/frontend/admin-panel/src/components/Companies/CompanyModal.tsx @@ -0,0 +1,116 @@ +import { useState, useEffect } from 'react'; +import type { Company } from '../../types/Company'; +import { companyService } from '../../services/companyService'; +import { X, Save, Building2, FileText, MapPin, Link } from 'lucide-react'; +import { motion } from 'framer-motion'; + +interface Props { + company: Company | null; + onClose: (refresh?: boolean) => void; +} + +export default function CompanyModal({ company, onClose }: Props) { + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState>({ + name: '', + taxId: '', + legalAddress: '', + logoUrl: '', + externalSystemId: '', + isActive: true + }); + + useEffect(() => { + if (company) setFormData(company); + }, [company]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + if (company) { + await companyService.update(company.id, formData); + } else { + await companyService.create(formData); + } + onClose(true); + } catch (error) { + alert("Error al guardar la empresa"); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+

+ + {company ? 'Editar Empresa' : 'Nueva Empresa'} +

+ +
+ +
+
+ + setFormData({ ...formData, name: e.target.value })} /> +
+ +
+
+ + setFormData({ ...formData, taxId: e.target.value })} /> +
+
+ + setFormData({ ...formData, externalSystemId: e.target.value })} /> +
+
+ +
+ + setFormData({ ...formData, legalAddress: e.target.value })} /> +
+ +
+ setFormData({ ...formData, isActive: e.target.checked })} + /> + +
+ +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/admin-panel/src/components/Products/ProductModal.tsx b/frontend/admin-panel/src/components/Products/ProductModal.tsx new file mode 100644 index 0000000..22a82af --- /dev/null +++ b/frontend/admin-panel/src/components/Products/ProductModal.tsx @@ -0,0 +1,274 @@ +import { useState, useEffect } from 'react'; +import type { Product, ProductBundleComponent } from '../../types/Product'; +import type { Company } from '../../types/Company'; +import { productService } from '../../services/productService'; +import { X, Save, Layers, Plus, Trash2, AlertCircle, Package } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Props { + product: Product | null; + companies: Company[]; + allProducts: Product[]; + onClose: (refresh?: boolean) => void; +} + +const PRODUCT_TYPES = [ + { id: 1, code: 'CLASSIFIED_AD', name: 'Aviso Clasificado' }, + { id: 2, code: 'GRAPHIC_AD', name: 'Publicidad Gráfica' }, + { id: 3, code: 'RADIO_AD', name: 'Publicidad Radial' }, + { id: 4, code: 'PHYSICAL', name: 'Producto Físico' }, + { id: 5, code: 'SERVICE', name: 'Servicio' }, + { id: 6, code: 'BUNDLE', name: 'Paquete Promocional (Combo)' }, +]; + +export default function ProductModal({ product, companies, allProducts, onClose }: Props) { + const [formData, setFormData] = useState>({ + name: '', + description: '', + companyId: 0, + productTypeId: 4, // Default Physical + basePrice: 0, + taxRate: 21, + sku: '', + isActive: true + }); + + const [isBundle, setIsBundle] = useState(false); + const [bundleComponents, setBundleComponents] = useState([]); + const [newComponentId, setNewComponentId] = useState(0); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (product) { + setFormData(product); + const type = PRODUCT_TYPES.find(t => t.id === product.productTypeId); + if (type?.code === 'BUNDLE') { + setIsBundle(true); + loadBundleComponents(product.id); + } + } + }, [product]); + + useEffect(() => { + const type = PRODUCT_TYPES.find(t => t.id === formData.productTypeId); + setIsBundle(type?.code === 'BUNDLE'); + }, [formData.productTypeId]); + + const loadBundleComponents = async (productId: number) => { + try { + const data = await productService.getBundleComponents(productId); + setBundleComponents(data); + } catch (error) { + console.error("Error cargando componentes", error); + } + }; + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + let productId = product?.id; + + if (product) { + await productService.update(product.id, formData); + } else { + const res = await productService.create(formData); + productId = res.id; + } + + // Si estamos creando un bundle nuevo y seleccionaron un componente inicial + if (isBundle && productId && newComponentId > 0 && !product) { + await productService.addComponentToBundle(productId, { + childProductId: newComponentId, + quantity: 1 + }); + } + + onClose(true); + } catch (error) { + alert("Error al guardar producto"); + } finally { + setLoading(false); + } + }; + + const addComponentDirectly = async () => { + if (!product || !newComponentId) return; + try { + await productService.addComponentToBundle(product.id, { + childProductId: newComponentId, + quantity: 1 + }); + setNewComponentId(0); + loadBundleComponents(product.id); // Recargar lista + } catch (e) { + alert("Error agregando componente"); + } + }; + + const removeComponent = async (childId: number) => { + if (!product) return; + if (!confirm("¿Quitar este producto del combo?")) return; + try { + await productService.removeComponentFromBundle(product.id, childId); + loadBundleComponents(product.id); + } catch (e) { + alert("Error eliminando componente"); + } + }; + + return ( +
+ +
+

+ {product ? `Editar: ${product.name}` : 'Nuevo Producto'} +

+ +
+ +
+
+ + {/* TIPO DE PRODUCTO */} +
+ +
+ {PRODUCT_TYPES.map(type => ( +
setFormData({ ...formData, productTypeId: type.id })} + className={`cursor-pointer p-3 rounded-xl border-2 text-xs font-bold transition-all text-center ${formData.productTypeId === type.id + ? 'border-blue-600 bg-blue-50 text-blue-700' + : 'border-slate-100 bg-white text-slate-500 hover:border-slate-300' + }`} + > + {type.name} +
+ ))} +
+
+ +
+
+ + setFormData({ ...formData, name: e.target.value })} /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, basePrice: parseFloat(e.target.value) })} /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, sku: e.target.value })} /> +
+
+ + {/* SECCIÓN ESPECIAL PARA BUNDLES */} + + {isBundle && ( + +
+ Contenido del Combo +
+ +
+ + El precio base se prorrateará entre estos componentes al facturar. +
+ + {/* LISTA DE COMPONENTES ACTUALES */} + {bundleComponents.length > 0 && ( +
+ {bundleComponents.map(comp => ( +
+
+ +
+

{comp.childProduct?.name}

+

Cantidad: {comp.quantity}

+
+
+ +
+ ))} +
+ )} + + {product ? ( +
+ + +
+ ) : ( +

Guarde el producto primero para agregar componentes.

+ )} +
+ )} +
+ +
+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/admin-panel/src/layouts/ProtectedLayout.tsx b/frontend/admin-panel/src/layouts/ProtectedLayout.tsx index 4d6a013..4466d47 100644 --- a/frontend/admin-panel/src/layouts/ProtectedLayout.tsx +++ b/frontend/admin-panel/src/layouts/ProtectedLayout.tsx @@ -1,8 +1,13 @@ import { Navigate, Outlet, useLocation, Link } from 'react-router-dom'; import { useAuthStore } from '../store/authStore'; import { - LogOut, LayoutDashboard, FolderTree, Users, - FileText, DollarSign, Eye, History, User as ClientIcon, Search, Tag + LogOut, LayoutDashboard, FolderTree, Users, Box, + FileText, DollarSign, Eye, History, User as ClientIcon, Search, Tag, + Building2, + Calendar, + ArrowRightLeft, + Settings, + ShieldAlert } from 'lucide-react'; import { useState, useEffect } from 'react'; import api from '../services/api'; @@ -39,63 +44,106 @@ export default function ProtectedLayout() { } }, [isAuthenticated, role]); - // Definición de permisos por ruta - const menuItems = [ - { label: 'Dashboard', href: '/', icon: , roles: ['Admin', 'Cajero'] }, - { label: 'Moderación', href: '/moderation', icon: , roles: ['Admin', 'Moderador'], badge: unreadCount }, - { label: 'Explorador', href: '/listings', icon: , roles: ['Admin', 'Cajero', 'Moderador'] }, - { label: 'Clientes', href: '/clients', icon: , roles: ['Admin', 'Cajero'] }, - { label: 'Categorías', href: '/categories', icon: , roles: ['Admin'] }, - { label: 'Usuarios', href: '/users', icon: , roles: ['Admin'] }, - { label: 'Tarifas', href: '/pricing', icon: , roles: ['Admin'] }, - { label: 'Promociones', href: '/promotions', icon: , roles: ['Admin'] }, - { label: 'Cupones', href: '/coupons', icon: , roles: ['Admin'] }, - { label: 'Diagramación', href: '/diagram', icon: , roles: ['Admin', 'Diagramador'] }, - { label: 'Auditoría', href: '/audit', icon: , roles: ['Admin'] }, + // Estructura de menú agrupada + const menuGroups = [ + { + title: "Gestión Diaria", + items: [ + { label: 'Dashboard', href: '/', icon: , roles: ['Admin', 'Cajero'] }, + { label: 'Moderación', href: '/moderation', icon: , roles: ['Admin', 'Moderador'], badge: unreadCount }, + { label: 'Explorador', href: '/listings', icon: , roles: ['Admin', 'Cajero', 'Moderador'] }, + { label: 'Clientes', href: '/clients', icon: , roles: ['Admin', 'Cajero'] }, + ] + }, + { + title: "Comercial & Catálogo", + items: [ + { label: 'Productos', href: '/products', icon: , roles: ['Admin'] }, + { label: 'Tarifas', href: '/pricing', icon: , roles: ['Admin'] }, + { label: 'Promociones', href: '/promotions', icon: , roles: ['Admin'] }, + { label: 'Cupones', href: '/coupons', icon: , roles: ['Admin'] }, + { label: 'Categorías', href: '/categories', icon: , roles: ['Admin'] }, + ] + }, + { + title: "Finanzas & Empresas", + items: [ + { label: 'Empresas', href: '/companies', icon: , roles: ['Admin'] }, + { label: 'Riesgo Crediticio', href: '/finance/credit', icon: , roles: ['Admin', 'Gerente'] }, + { label: 'Liquidación', href: '/reports/settlement', icon: , roles: ['Admin', 'Contador'] }, + ] + }, + { + title: "Operaciones & Sistema", + items: [ + { label: 'Diagramación', href: '/diagram', icon: , roles: ['Admin', 'Diagramador'] }, + { label: 'Calendario', href: '/companies/calendar', icon: , roles: ['Admin'] }, + { label: 'Usuarios', href: '/users', icon: , roles: ['Admin'] }, + { label: 'Auditoría', href: '/audit', icon: , roles: ['Admin'] }, + ] + } ]; return (
-