@@ -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 > }