feat: selector de producto por rubro en FastEntryPage y Mostrador Universal

This commit is contained in:
2026-02-21 19:59:19 -03:00
parent 16f84237fb
commit e21028ee9f
5 changed files with 108 additions and 3 deletions

View File

@@ -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<HTMLDivElement>(null);
// Estado del selector de productos por rubro
const [categoryProducts, setCategoryProducts] = useState<Product[]>([]);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(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() {
</div>
</div>
{/* SELECTOR DE PRODUCTO */}
{formData.categoryId && (
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-1.5 tracking-widest flex items-center gap-2">
<Package size={12} /> Producto / Tarifa
</label>
{loadingProducts ? (
<div className="py-2 px-4 border-2 border-slate-100 rounded-xl bg-slate-50 text-slate-400 text-xs font-bold animate-pulse">
Cargando productos del rubro...
</div>
) : categoryProducts.length === 0 ? (
<div className="py-2 px-4 border-2 border-amber-100 rounded-xl bg-amber-50 text-amber-600 text-xs font-bold flex items-center gap-2">
Sin productos en este rubro el precio base será $0
</div>
) : (
<div className="grid gap-2" style={{ gridTemplateColumns: `repeat(${Math.min(categoryProducts.length, 3)}, 1fr)` }}>
{categoryProducts.map(prod => (
<button
key={prod.id}
type="button"
onClick={() => setSelectedProduct(prod)}
className={clsx(
"py-2 px-3 border-2 rounded-xl text-left transition-all duration-200 group",
selectedProduct?.id === prod.id
? "border-blue-500 bg-blue-600 text-white shadow-lg shadow-blue-200"
: "border-slate-100 bg-slate-50 hover:border-blue-300 hover:bg-blue-50"
)}
>
<div className={clsx("text-[10px] font-black uppercase tracking-tighter truncate", selectedProduct?.id === prod.id ? "text-blue-100" : "text-slate-500")}>
{prod.typeCode}
</div>
<div className={clsx("text-xs font-black truncate leading-tight mt-0.5", selectedProduct?.id === prod.id ? "text-white" : "text-slate-800")}>
{prod.name}
</div>
<div className={clsx("text-sm font-mono font-black mt-1", selectedProduct?.id === prod.id ? "text-green-300" : "text-blue-600")}>
${prod.basePrice.toLocaleString()}
</div>
</button>
))}
</div>
)}
</div>
)}
<div className="grid grid-cols-12 gap-6">
<div className="col-span-8">
<label className="block text-[10px] font-black text-slate-500 uppercase mb-1.5 tracking-widest">Título Web (Opcional)</label>
@@ -562,6 +634,12 @@ export default function FastEntryPage() {
<div className="text-4xl font-mono font-black text-green-400 flex items-start gap-1">
<span className="text-lg mt-1 opacity-50">$</span>{pricing.totalPrice.toLocaleString()}
</div>
{selectedProduct && (
<div className="mt-2 flex items-center gap-2 py-1.5 px-2 bg-blue-600/20 rounded-lg border border-blue-500/30">
<Package size={10} className="text-blue-400 shrink-0" />
<span className="text-[9px] font-black text-blue-300 truncate uppercase tracking-tight">{selectedProduct.name}</span>
</div>
)}
<div className="mt-3 pt-3 border-t border-slate-800 space-y-1.5 text-[10px] font-bold uppercase tracking-tighter">
<div className="flex justify-between text-slate-500 italic"><span>Tarifa Base</span><span className="text-slate-300">${pricing.baseCost.toLocaleString()}</span></div>
{pricing.extraCost > 0 && <div className="flex justify-between text-orange-500"><span>Recargos Texto</span><span>+${pricing.extraCost.toLocaleString()}</span></div>}

View File

@@ -12,6 +12,12 @@ export const productService = {
return response.data;
},
// Obtiene los productos clasificados vinculados a un rubro
getByCategory: async (categoryId: number): Promise<Product[]> => {
const response = await api.get<Product[]>(`/products/by-category/${categoryId}`);
return response.data;
},
create: async (product: Partial<Product>): Promise<Product> => {
const response = await api.post<Product>('/products', product);
return response.data;