From b4fa74ad9b45e8769bbea0a2ba8335de0469e058 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 25 Feb 2026 18:11:52 -0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Configuraci=C3=B3n=20y=20Administraci?= =?UTF-8?q?=C3=B3n=20de=20Tipos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/admin-panel/index.html | 24 +-- frontend/admin-panel/src/App.tsx | 2 + .../src/components/Products/ProductModal.tsx | 67 ++++--- .../components/Products/ProductTypeModal.tsx | 124 ++++++++++++ .../src/layouts/ProtectedLayout.tsx | 1 + .../src/pages/Products/ProductTypeManager.tsx | 139 +++++++++++++ .../src/services/productTypeService.ts | 23 +++ frontend/admin-panel/src/types/Product.ts | 15 ++ frontend/counter-panel/index.html | 24 +-- .../src/components/POS/AdEditorModal.tsx | 21 +- .../POS/BundleConfiguratorModal.tsx | 186 ++++++++++++++++++ .../src/pages/UniversalPosPage.tsx | 72 +++++-- frontend/counter-panel/src/store/cartStore.ts | 16 +- frontend/counter-panel/src/types/Product.ts | 7 +- .../Controllers/ProductTypesController.cs | 64 ++++++ src/SIGCM.Application/DTOs/OrderDtos.cs | 5 +- src/SIGCM.Domain/Entities/Product.cs | 3 + src/SIGCM.Domain/Entities/ProductType.cs | 17 ++ .../Interfaces/IProductTypeRepository.cs | 12 ++ .../Data/DbInitializer.cs | 107 ++++++++++ .../DependencyInjection.cs | 3 +- .../Repositories/ProductRepository.cs | 12 +- .../Repositories/ProductTypeRepository.cs | 60 ++++++ .../Services/OrderService.cs | 5 +- .../Services/PricingService.cs | 39 ++-- 25 files changed, 941 insertions(+), 107 deletions(-) create mode 100644 frontend/admin-panel/src/components/Products/ProductTypeModal.tsx create mode 100644 frontend/admin-panel/src/pages/Products/ProductTypeManager.tsx create mode 100644 frontend/admin-panel/src/services/productTypeService.ts create mode 100644 frontend/counter-panel/src/components/POS/BundleConfiguratorModal.tsx create mode 100644 src/SIGCM.API/Controllers/ProductTypesController.cs create mode 100644 src/SIGCM.Domain/Entities/ProductType.cs create mode 100644 src/SIGCM.Domain/Interfaces/IProductTypeRepository.cs create mode 100644 src/SIGCM.Infrastructure/Repositories/ProductTypeRepository.cs diff --git a/frontend/admin-panel/index.html b/frontend/admin-panel/index.html index 3781f2b..571bda9 100644 --- a/frontend/admin-panel/index.html +++ b/frontend/admin-panel/index.html @@ -1,13 +1,15 @@ - - - - - admin-panel - - -
- - - + + + + + admin-panel + + + +
+ + + + \ No newline at end of file diff --git a/frontend/admin-panel/src/App.tsx b/frontend/admin-panel/src/App.tsx index 6bf0c63..95b66c4 100644 --- a/frontend/admin-panel/src/App.tsx +++ b/frontend/admin-panel/src/App.tsx @@ -14,6 +14,7 @@ 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 ProductTypeManager from './pages/Products/ProductTypeManager'; import CompanyManager from './pages/Companies/CompanyManager'; import CreditManager from './pages/Finance/CreditManager'; import CalendarManager from './pages/Companies/CalendarManager'; @@ -34,6 +35,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/admin-panel/src/components/Products/ProductModal.tsx b/frontend/admin-panel/src/components/Products/ProductModal.tsx index e8f4df7..915499f 100644 --- a/frontend/admin-panel/src/components/Products/ProductModal.tsx +++ b/frontend/admin-panel/src/components/Products/ProductModal.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from 'react'; -import type { Product, ProductBundleComponent } from '../../types/Product'; +import type { Product, ProductBundleComponent, ProductType } from '../../types/Product'; import type { Company } from '../../types/Company'; import type { Category } from '../../types/Category'; import { productService } from '../../services/productService'; +import { productTypeService } from '../../services/productTypeService'; import { X, Save, Layers, Plus, Trash2, AlertCircle, Package } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; @@ -14,22 +15,13 @@ interface Props { 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, categories, allProducts, onClose }: Props) { const [formData, setFormData] = useState>({ name: '', description: '', companyId: 0, categoryId: undefined, - productTypeId: 4, // Default Physical + productTypeId: 0, basePrice: 0, priceDurationDays: 1, taxRate: 21, @@ -37,35 +29,48 @@ export default function ProductModal({ product, companies, categories, allProduc isActive: true }); + const [productTypes, setProductTypes] = useState([]); const [isBundle, setIsBundle] = useState(false); const [bundleComponents, setBundleComponents] = useState([]); const [newComponentId, setNewComponentId] = useState(0); const [loading, setLoading] = useState(false); - const [hasDuration, setHasDuration] = useState(false); + const [hasDurationUI, setHasDurationUI] = useState(false); + + useEffect(() => { + productTypeService.getAll().then(setProductTypes).catch(console.error); + }, []); useEffect(() => { if (product) { setFormData(product); - setHasDuration(product.priceDurationDays > 1); - const type = PRODUCT_TYPES.find(t => t.id === product.productTypeId); - if (type?.code === 'BUNDLE') { - setIsBundle(true); - loadBundleComponents(product.id); + setHasDurationUI(product.priceDurationDays > 1); + if (productTypes.length > 0) { + const type = productTypes.find(t => t.id === product.productTypeId); + if (type?.isBundle) { + setIsBundle(true); + loadBundleComponents(product.id); + } } + } else if (productTypes.length > 0 && formData.productTypeId === 0) { + // Default to first type if creating new + setFormData(f => ({ ...f, productTypeId: productTypes[0].id })); } - }, [product]); + }, [product, productTypes]); useEffect(() => { - const type = PRODUCT_TYPES.find(t => t.id === formData.productTypeId); - setIsBundle(type?.code === 'BUNDLE'); + const type = productTypes.find(t => t.id === formData.productTypeId); + if (!type) return; - // Si es producto físico o combo, forzamos duración 1 (sin periodo) - if (formData.productTypeId === 4 || formData.productTypeId === 6) { + setIsBundle(type.isBundle); + + // Si el tipo de producto NO soporta duración, forzamos duración 1 + if (!type.hasDuration) { if (formData.priceDurationDays !== 1) { setFormData(f => ({ ...f, priceDurationDays: 1 })); + setHasDurationUI(false); } } - }, [formData.productTypeId]); + }, [formData.productTypeId, productTypes]); const loadBundleComponents = async (productId: number) => { try { @@ -150,7 +155,7 @@ export default function ProductModal({ product, companies, categories, allProduc
- {PRODUCT_TYPES.map(type => ( + {productTypes.map(type => (
setFormData({ ...formData, productTypeId: type.id })} @@ -198,27 +203,27 @@ export default function ProductModal({ product, companies, categories, allProduc value={formData.basePrice} onChange={e => setFormData({ ...formData, basePrice: parseFloat(e.target.value) })} />
- {formData.productTypeId !== 4 && formData.productTypeId !== 6 && ( + {productTypes.find(t => t.id === formData.productTypeId)?.hasDuration && (
{ - const newValue = !hasDuration; - setHasDuration(newValue); + const newValue = !hasDurationUI; + setHasDurationUI(newValue); if (!newValue) setFormData({ ...formData, priceDurationDays: 1 }); }} - className={`flex items-center justify-between p-3 rounded-xl border-2 cursor-pointer transition-all ${hasDuration ? 'border-blue-500 bg-blue-50' : 'border-slate-100 bg-slate-50 opacity-60' + className={`flex items-center justify-between p-3 rounded-xl border-2 cursor-pointer transition-all ${hasDurationUI ? 'border-blue-500 bg-blue-50' : 'border-slate-100 bg-slate-50 opacity-60' }`} >
Precio por Duración Habilitar si el precio base cubre varios días
-
-
+
+
- {hasDuration && ( + {hasDurationUI && (
diff --git a/frontend/admin-panel/src/components/Products/ProductTypeModal.tsx b/frontend/admin-panel/src/components/Products/ProductTypeModal.tsx new file mode 100644 index 0000000..e97d12d --- /dev/null +++ b/frontend/admin-panel/src/components/Products/ProductTypeModal.tsx @@ -0,0 +1,124 @@ +import { useState, useEffect } from 'react'; +import type { ProductType } from '../../types/Product'; +import { productTypeService } from '../../services/productTypeService'; +import { X, Save, Settings2 } from 'lucide-react'; +import { motion } from 'framer-motion'; + +interface Props { + type: ProductType | null; + onClose: (refresh?: boolean) => void; +} + +export default function ProductTypeModal({ type, onClose }: Props) { + const [formData, setFormData] = useState>({ + code: '', + name: '', + description: '', + icon: 'Package', + hasDuration: false, + requiresText: false, + requiresCategory: false, + isBundle: false, + isActive: true + }); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (type) setFormData(type); + }, [type]); + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + if (type) await productTypeService.update(type.id, formData); + else await productTypeService.create(formData); + onClose(true); + } catch (error) { + alert("Error al guardar tipo de producto"); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+

+ {type ? `Configurar: ${type.name}` : 'Nuevo Tipo de Producto'} +

+ +
+ +
+
+
+ + setFormData({ ...formData, code: e.target.value.toUpperCase() })} /> +
+
+ + setFormData({ ...formData, name: e.target.value })} /> +
+
+ +
+ +