diff --git a/.gitignore b/.gitignore index 923dc6b..03f3964 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ yarn-error.log* # Configuración de desarrollo que puede contener secretos. # Es mejor usar "User Secrets" en desarrollo para las claves. -appsettings.Development.json +#appsettings.Development.json # Archivos de publicación de Visual Studio [Pp]roperties/[Pp]ublish[Pp]rofiles/ diff --git a/frontend/admin-panel/src/components/Products/ProductModal.tsx b/frontend/admin-panel/src/components/Products/ProductModal.tsx index 22a82af..1fa97aa 100644 --- a/frontend/admin-panel/src/components/Products/ProductModal.tsx +++ b/frontend/admin-panel/src/components/Products/ProductModal.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import type { Product, ProductBundleComponent } from '../../types/Product'; import type { Company } from '../../types/Company'; +import type { Category } from '../../types/Category'; import { productService } from '../../services/productService'; import { X, Save, Layers, Plus, Trash2, AlertCircle, Package } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; @@ -8,6 +9,7 @@ import { motion, AnimatePresence } from 'framer-motion'; interface Props { product: Product | null; companies: Company[]; + categories: Category[]; allProducts: Product[]; onClose: (refresh?: boolean) => void; } @@ -21,11 +23,12 @@ const PRODUCT_TYPES = [ { id: 6, code: 'BUNDLE', name: 'Paquete Promocional (Combo)' }, ]; -export default function ProductModal({ product, companies, allProducts, onClose }: Props) { +export default function ProductModal({ product, companies, categories, allProducts, onClose }: Props) { const [formData, setFormData] = useState>({ name: '', description: '', companyId: 0, + categoryId: undefined, productTypeId: 4, // Default Physical basePrice: 0, taxRate: 21, @@ -169,6 +172,16 @@ export default function ProductModal({ product, companies, allProducts, onClose +
+ + +
+
-
- -
- $ - setConfig({ ...config, basePrice: parseFloat(e.target.value) })} /> -
-

Costo por el aviso básico por día.

-
+

+ El Precio Base (Precio Mínimo) ahora se define directamente en los Productos del Catálogo. +

diff --git a/frontend/admin-panel/src/pages/Products/ProductManager.tsx b/frontend/admin-panel/src/pages/Products/ProductManager.tsx index 94d7f52..7de7cb0 100644 --- a/frontend/admin-panel/src/pages/Products/ProductManager.tsx +++ b/frontend/admin-panel/src/pages/Products/ProductManager.tsx @@ -2,14 +2,17 @@ import { useState, useEffect } from 'react'; import { Plus, Search, Edit, Box, Layers } from 'lucide-react'; import { productService } from '../../../../counter-panel/src/services/productService'; import { companyService } from '../../../../counter-panel/src/services/companyService'; +import { categoryService } from '../../services/categoryService'; import type { Product } from '../../../../counter-panel/src/types/Product'; import type { Company } from '../../../../counter-panel/src/types/Company'; +import type { Category } from '../../types/Category'; import ProductModal from '../../components/Products/ProductModal'; import clsx from 'clsx'; export default function ProductManager() { const [products, setProducts] = useState([]); const [companies, setCompanies] = useState([]); + const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); @@ -24,12 +27,14 @@ export default function ProductManager() { const loadData = async () => { setLoading(true); try { - const [prodRes, compRes] = await Promise.all([ + const [prodRes, compRes, catRes] = await Promise.all([ productService.getAll(), - companyService.getAll() + companyService.getAll(), + categoryService.getAll() ]); setProducts(prodRes); setCompanies(compRes); + setCategories(catRes); } catch (error) { console.error("Error cargando catálogo", error); } finally { @@ -150,6 +155,7 @@ export default function ProductManager() { diff --git a/frontend/admin-panel/src/types/Product.ts b/frontend/admin-panel/src/types/Product.ts index a70f789..be1d0a2 100644 --- a/frontend/admin-panel/src/types/Product.ts +++ b/frontend/admin-panel/src/types/Product.ts @@ -2,6 +2,7 @@ export interface Product { id: number; companyId: number; productTypeId: number; // 1:Classified, 2:Graphic, 3:Radio, 4:Physical, 5:Service, 6:Bundle + categoryId?: number; // Para relacionarlo a un rubro name: string; description?: string; sku?: string; diff --git a/frontend/counter-panel/src/components/POS/AdEditorModal.tsx b/frontend/counter-panel/src/components/POS/AdEditorModal.tsx index ea31a36..9814fac 100644 --- a/frontend/counter-panel/src/components/POS/AdEditorModal.tsx +++ b/frontend/counter-panel/src/components/POS/AdEditorModal.tsx @@ -10,6 +10,7 @@ interface AdEditorModalProps { onClose: () => void; onConfirm: (listingId: number, price: number, description: string) => void; clientId: number | null; // El aviso se vinculará a este cliente + productId: number; // Necesario para el precio base } interface PricingResult { @@ -18,7 +19,7 @@ interface PricingResult { details: string; } -export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId }: AdEditorModalProps) { +export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId, productId }: AdEditorModalProps) { const [flatCategories, setFlatCategories] = useState([]); const [operations, setOperations] = useState([]); const [loading, setLoading] = useState(false); @@ -72,6 +73,7 @@ export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId }: try { const res = await api.post('/pricing/calculate', { categoryId: parseInt(categoryId), + productId: productId, text: debouncedText, days: days, isBold: styles.isBold, diff --git a/frontend/counter-panel/src/pages/FastEntryPage.tsx b/frontend/counter-panel/src/pages/FastEntryPage.tsx index e9a0d31..14a934e 100644 --- a/frontend/counter-panel/src/pages/FastEntryPage.tsx +++ b/frontend/counter-panel/src/pages/FastEntryPage.tsx @@ -240,6 +240,7 @@ export default function FastEntryPage() { try { const res = await api.post('/pricing/calculate', { categoryId: parseInt(formData.categoryId), + productId: 0, // En FastEntry no hay producto aún text: debouncedText || "", days: formData.days, isBold: options.isBold, diff --git a/frontend/counter-panel/src/pages/UniversalPosPage.tsx b/frontend/counter-panel/src/pages/UniversalPosPage.tsx index 98bd55f..e2327df 100644 --- a/frontend/counter-panel/src/pages/UniversalPosPage.tsx +++ b/frontend/counter-panel/src/pages/UniversalPosPage.tsx @@ -310,6 +310,7 @@ export default function UniversalPosPage() { onClose={() => setShowAdEditor(false)} onConfirm={handleAdConfirmed} clientId={clientId || 1005} + productId={selectedAdProduct?.id || 0} /> )} diff --git a/frontend/counter-panel/src/types/Product.ts b/frontend/counter-panel/src/types/Product.ts index a70f789..be1d0a2 100644 --- a/frontend/counter-panel/src/types/Product.ts +++ b/frontend/counter-panel/src/types/Product.ts @@ -2,6 +2,7 @@ export interface Product { id: number; companyId: number; productTypeId: number; // 1:Classified, 2:Graphic, 3:Radio, 4:Physical, 5:Service, 6:Bundle + categoryId?: number; // Para relacionarlo a un rubro name: string; description?: string; sku?: string; diff --git a/src/SIGCM.API/appsettings.Development.json b/src/SIGCM.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/SIGCM.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/SIGCM.Application/DTOs/PricingDtos.cs b/src/SIGCM.Application/DTOs/PricingDtos.cs index 7451b33..73518e6 100644 --- a/src/SIGCM.Application/DTOs/PricingDtos.cs +++ b/src/SIGCM.Application/DTOs/PricingDtos.cs @@ -3,6 +3,7 @@ namespace SIGCM.Application.DTOs; public class CalculatePriceRequest { public int CategoryId { get; set; } + public int ProductId { get; set; } // Añadido para obtener el Precio Base public required string Text { get; set; } public int Days { get; set; } public bool IsBold { get; set; } diff --git a/src/SIGCM.Domain/Entities/CategoryPricing.cs b/src/SIGCM.Domain/Entities/CategoryPricing.cs index 8439210..36bd351 100644 --- a/src/SIGCM.Domain/Entities/CategoryPricing.cs +++ b/src/SIGCM.Domain/Entities/CategoryPricing.cs @@ -4,7 +4,6 @@ public class CategoryPricing { public int Id { get; set; } public int CategoryId { get; set; } - public decimal BasePrice { get; set; } public int BaseWordCount { get; set; } public decimal ExtraWordPrice { get; set; } public string SpecialChars { get; set; } = "!"; diff --git a/src/SIGCM.Domain/Entities/Product.cs b/src/SIGCM.Domain/Entities/Product.cs index f774efb..55e627e 100644 --- a/src/SIGCM.Domain/Entities/Product.cs +++ b/src/SIGCM.Domain/Entities/Product.cs @@ -5,6 +5,7 @@ public class Product public int Id { get; set; } public int CompanyId { get; set; } public int ProductTypeId { get; set; } + public int? CategoryId { get; set; } // Propiedad agregada: Vínculo con Rubro public required string Name { get; set; } public string? Description { get; set; } public string? SKU { get; set; } diff --git a/src/SIGCM.Infrastructure/Data/DbInitializer.cs b/src/SIGCM.Infrastructure/Data/DbInitializer.cs index ef8f713..0c51252 100644 --- a/src/SIGCM.Infrastructure/Data/DbInitializer.cs +++ b/src/SIGCM.Infrastructure/Data/DbInitializer.cs @@ -121,6 +121,30 @@ END // --- MIGRACIONES (Schema Update) --- var migrationSql = @" + -- Products Columns + IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'CategoryId' AND Object_ID = Object_ID(N'Products')) + BEGIN + ALTER TABLE Products ADD CategoryId INT NULL; + -- Agregar clave foránea opcionalmente + ALTER TABLE Products ADD CONSTRAINT FK_Products_Categories FOREIGN KEY (CategoryId) REFERENCES Categories(Id); + END + + -- CategoryPricing Columns + IF EXISTS(SELECT * FROM sys.columns WHERE Name = N'BasePrice' AND Object_ID = Object_ID(N'CategoryPricing')) + BEGIN + -- Drop the default constraint before dropping the column. + -- By finding the constraint dynamically: + DECLARE @ConstraintName nvarchar(200) + SELECT @ConstraintName = Name FROM sys.default_constraints + WHERE parent_object_id = object_id('CategoryPricing') + AND parent_column_id = columnproperty(object_id('CategoryPricing'), 'BasePrice', 'ColumnId') + + IF @ConstraintName IS NOT NULL + EXEC('ALTER TABLE CategoryPricing DROP CONSTRAINT ' + @ConstraintName) + + ALTER TABLE CategoryPricing DROP COLUMN BasePrice; + END + -- Listings Columns IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'PublicationStartDate' AND Object_ID = Object_ID(N'Listings')) ALTER TABLE Listings ADD PublicationStartDate DATETIME2 NULL; diff --git a/src/SIGCM.Infrastructure/Repositories/PricingRepository.cs b/src/SIGCM.Infrastructure/Repositories/PricingRepository.cs index eca9f6e..554ea06 100644 --- a/src/SIGCM.Infrastructure/Repositories/PricingRepository.cs +++ b/src/SIGCM.Infrastructure/Repositories/PricingRepository.cs @@ -31,7 +31,7 @@ public class PricingRepository { var updateSql = @" UPDATE CategoryPricing - SET BasePrice = @BasePrice, BaseWordCount = @BaseWordCount, + SET BaseWordCount = @BaseWordCount, ExtraWordPrice = @ExtraWordPrice, SpecialChars = @SpecialChars, SpecialCharPrice = @SpecialCharPrice, BoldSurcharge = @BoldSurcharge, FrameSurcharge = @FrameSurcharge @@ -42,9 +42,9 @@ public class PricingRepository { var insertSql = @" INSERT INTO CategoryPricing - (CategoryId, BasePrice, BaseWordCount, ExtraWordPrice, SpecialChars, SpecialCharPrice, BoldSurcharge, FrameSurcharge) + (CategoryId, BaseWordCount, ExtraWordPrice, SpecialChars, SpecialCharPrice, BoldSurcharge, FrameSurcharge) VALUES - (@CategoryId, @BasePrice, @BaseWordCount, @ExtraWordPrice, @SpecialChars, @SpecialCharPrice, @BoldSurcharge, @FrameSurcharge)"; + (@CategoryId, @BaseWordCount, @ExtraWordPrice, @SpecialChars, @SpecialCharPrice, @BoldSurcharge, @FrameSurcharge)"; await conn.ExecuteAsync(insertSql, pricing); } } diff --git a/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs b/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs index 4873320..5f1830b 100644 --- a/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs +++ b/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs @@ -47,8 +47,8 @@ public class ProductRepository : IProductRepository { using var conn = _db.CreateConnection(); var sql = @" - INSERT INTO Products (CompanyId, ProductTypeId, Name, Description, SKU, BasePrice, TaxRate, IsActive) - VALUES (@CompanyId, @ProductTypeId, @Name, @Description, @SKU, @BasePrice, @TaxRate, @IsActive); + INSERT INTO Products (CompanyId, ProductTypeId, CategoryId, Name, Description, SKU, BasePrice, TaxRate, IsActive) + VALUES (@CompanyId, @ProductTypeId, @CategoryId, @Name, @Description, @SKU, @BasePrice, @TaxRate, @IsActive); SELECT CAST(SCOPE_IDENTITY() as int);"; return await conn.QuerySingleAsync(sql, product); } @@ -58,7 +58,7 @@ public class ProductRepository : IProductRepository using var conn = _db.CreateConnection(); var sql = @" UPDATE Products - SET Name = @Name, Description = @Description, BasePrice = @BasePrice, TaxRate = @TaxRate, IsActive = @IsActive + SET CategoryId = @CategoryId, Name = @Name, Description = @Description, BasePrice = @BasePrice, TaxRate = @TaxRate, IsActive = @IsActive WHERE Id = @Id"; await conn.ExecuteAsync(sql, product); } diff --git a/src/SIGCM.Infrastructure/Services/PricingService.cs b/src/SIGCM.Infrastructure/Services/PricingService.cs index 41c9406..9900870 100644 --- a/src/SIGCM.Infrastructure/Services/PricingService.cs +++ b/src/SIGCM.Infrastructure/Services/PricingService.cs @@ -10,15 +10,25 @@ public class PricingService { private readonly PricingRepository _repo; private readonly ICouponRepository _couponRepo; + private readonly IProductRepository _productRepo; - public PricingService(PricingRepository repo, ICouponRepository couponRepo) + public PricingService(PricingRepository repo, ICouponRepository couponRepo, IProductRepository productRepo) { _repo = repo; _couponRepo = couponRepo; + _productRepo = productRepo; } public async Task CalculateAsync(CalculatePriceRequest request) { + // 0. Obtener el Producto para saber el Precio Base + var product = await _productRepo.GetByIdAsync(request.ProductId); + if (product == null) return new CalculatePriceResponse + { + TotalPrice = 0, + Details = "Producto no encontrado." + }; + // 1. Obtener Reglas var pricing = await _repo.GetByCategoryIdAsync(request.CategoryId); @@ -44,7 +54,10 @@ public class PricingService int realWordCount = words.Length; // 3. Costo Base y Excedente - decimal currentCost = pricing.BasePrice; // Precio base incluye N palabras + // Obtenemos el precio actual del producto vigenciado, fallback a BasePrice (usando el repo) + decimal productBasePrice = await _productRepo.GetCurrentPriceAsync(request.ProductId, request.StartDate == default ? DateTime.UtcNow : request.StartDate); + decimal currentCost = productBasePrice; // Precio base incluye N palabras + // ¿Cuántas palabras extra cobramos? // Nota: Los caracteres especiales se cobran aparte según tu requerimiento, @@ -132,7 +145,7 @@ public class PricingService return new CalculatePriceResponse { TotalPrice = Math.Max(0, totalBeforeDiscount - totalDiscount), - BaseCost = pricing.BasePrice * request.Days, + BaseCost = productBasePrice * request.Days, ExtraCost = (extraWordCost + specialCharCost) * request.Days, Surcharges = ((request.IsBold ? pricing.BoldSurcharge : 0) + (request.IsFrame ? pricing.FrameSurcharge : 0) + (request.IsFeatured ? featuredSurcharge : 0)) * request.Days, Discount = totalDiscount,