diff --git a/frontend/counter-panel/src/pages/FastEntryPage.tsx b/frontend/counter-panel/src/pages/FastEntryPage.tsx index 14a934e..56fbd3d 100644 --- a/frontend/counter-panel/src/pages/FastEntryPage.tsx +++ b/frontend/counter-panel/src/pages/FastEntryPage.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import api from '../services/api'; import { useDebounce } from '../hooks/useDebounce'; import { processCategories, type FlatCategory } from '../utils/categoryTreeUtils'; +import { productService } from '../services/productService'; +import type { Product } from '../types/Product'; import { Printer, Save, AlignLeft, AlignCenter, AlignRight, AlignJustify, @@ -13,7 +15,8 @@ import { X, UploadCloud, MessageSquare, - Star + Star, + Package } from 'lucide-react'; import clsx from 'clsx'; import PaymentModal, { type Payment } from '../components/PaymentModal'; @@ -53,6 +56,11 @@ export default function FastEntryPage() { const [isCatDropdownOpen, setIsCatDropdownOpen] = useState(false); const catWrapperRef = useRef(null); + // Estado del selector de productos por rubro + const [categoryProducts, setCategoryProducts] = useState([]); + const [selectedProduct, setSelectedProduct] = useState(null); + const [loadingProducts, setLoadingProducts] = useState(false); + const [formData, setFormData] = useState({ categoryId: '', operationId: '', text: '', title: '', price: '', days: 3, clientName: '', clientDni: '', clientId: null as number | null, startDate: new Date(Date.now() + 86400000).toISOString().split('T')[0], @@ -212,6 +220,25 @@ export default function FastEntryPage() { fetchData(); }, []); + // Cargar productos cuando cambia el rubro seleccionado + useEffect(() => { + if (!formData.categoryId) { + setCategoryProducts([]); + setSelectedProduct(null); + return; + } + setLoadingProducts(true); + productService.getByCategory(parseInt(formData.categoryId)) + .then(prods => { + setCategoryProducts(prods); + // Auto-seleccionar el primero si solo hay uno + if (prods.length === 1) setSelectedProduct(prods[0]); + else setSelectedProduct(null); + }) + .catch(console.error) + .finally(() => setLoadingProducts(false)); + }, [formData.categoryId]); + const handleSubmit = useCallback(async () => { if (!validate()) return; setShowPaymentModal(true); @@ -240,7 +267,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 + productId: selectedProduct?.id || 0, text: debouncedText || "", days: formData.days, isBold: options.isBold, @@ -251,7 +278,7 @@ export default function FastEntryPage() { } catch (error) { console.error(error); } }; calculatePrice(); - }, [debouncedText, formData.categoryId, formData.days, options, formData.startDate]); + }, [debouncedText, formData.categoryId, selectedProduct, formData.days, options, formData.startDate]); useEffect(() => { if (debouncedClientSearch.length > 2 && showSuggestions) { @@ -317,6 +344,7 @@ export default function FastEntryPage() { setOptions({ isBold: false, isFrame: false, fontSize: 'normal', alignment: 'left' }); setSelectedImages([]); setImagePreviews([]); + setSelectedProduct(null); setShowPaymentModal(false); setErrors({}); showToast('Aviso procesado correctamente.', 'success'); @@ -436,6 +464,50 @@ export default function FastEntryPage() { + {/* SELECTOR DE PRODUCTO */} + {formData.categoryId && ( +
+ + {loadingProducts ? ( +
+ Cargando productos del rubro... +
+ ) : categoryProducts.length === 0 ? ( +
+ ⚠️ Sin productos en este rubro — el precio base será $0 +
+ ) : ( +
+ {categoryProducts.map(prod => ( + + ))} +
+ )} +
+ )} +
@@ -562,6 +634,12 @@ export default function FastEntryPage() {
${pricing.totalPrice.toLocaleString()}
+ {selectedProduct && ( +
+ + {selectedProduct.name} +
+ )}
Tarifa Base${pricing.baseCost.toLocaleString()}
{pricing.extraCost > 0 &&
Recargos Texto+${pricing.extraCost.toLocaleString()}
} diff --git a/frontend/counter-panel/src/services/productService.ts b/frontend/counter-panel/src/services/productService.ts index 21fd0fb..d9a74da 100644 --- a/frontend/counter-panel/src/services/productService.ts +++ b/frontend/counter-panel/src/services/productService.ts @@ -12,6 +12,12 @@ export const productService = { return response.data; }, + // Obtiene los productos clasificados vinculados a un rubro + getByCategory: async (categoryId: number): Promise => { + const response = await api.get(`/products/by-category/${categoryId}`); + return response.data; + }, + create: async (product: Partial): Promise => { const response = await api.post('/products', product); return response.data; diff --git a/src/SIGCM.API/Controllers/ProductsController.cs b/src/SIGCM.API/Controllers/ProductsController.cs index 44587f4..af9dbc7 100644 --- a/src/SIGCM.API/Controllers/ProductsController.cs +++ b/src/SIGCM.API/Controllers/ProductsController.cs @@ -40,6 +40,14 @@ public class ProductsController : ControllerBase return Ok(products); } + // Obtener productos clasificados vinculados a un rubro específico + [HttpGet("by-category/{categoryId}")] + public async Task GetByCategory(int categoryId) + { + var products = await _repository.GetByCategoryIdAsync(categoryId); + return Ok(products); + } + [HttpPost] [Authorize(Roles = "Admin")] // Solo Admins crean productos public async Task Create(Product product) diff --git a/src/SIGCM.Domain/Interfaces/IProductRepository.cs b/src/SIGCM.Domain/Interfaces/IProductRepository.cs index e3d17d3..afdc014 100644 --- a/src/SIGCM.Domain/Interfaces/IProductRepository.cs +++ b/src/SIGCM.Domain/Interfaces/IProductRepository.cs @@ -6,6 +6,7 @@ public interface IProductRepository { Task> GetAllAsync(); Task> GetByCompanyIdAsync(int companyId); + Task> GetByCategoryIdAsync(int categoryId); Task GetByIdAsync(int id); Task CreateAsync(Product product); Task UpdateAsync(Product product); diff --git a/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs b/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs index 5f1830b..4cd5055 100644 --- a/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs +++ b/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs @@ -43,6 +43,18 @@ public class ProductRepository : IProductRepository new { Id = companyId }); } + public async Task> GetByCategoryIdAsync(int categoryId) + { + using var conn = _db.CreateConnection(); + var sql = @" + SELECT p.*, c.Name as CompanyName, pt.Code as TypeCode + FROM Products p + JOIN Companies c ON p.CompanyId = c.Id + JOIN ProductTypes pt ON p.ProductTypeId = pt.Id + WHERE p.CategoryId = @CategoryId AND p.IsActive = 1"; + return await conn.QueryAsync(sql, new { CategoryId = categoryId }); + } + public async Task CreateAsync(Product product) { using var conn = _db.CreateConnection();