Files
SIG-CM/frontend/counter-panel/src/components/POS/AdEditorModal.tsx

299 lines
14 KiB
TypeScript
Raw Normal View History

2026-01-07 17:52:10 -03:00
import { useState, useEffect } from 'react';
import { X, Save, Calendar, Type, AlignLeft, AlignCenter, AlignRight, AlignJustify, Bold, Square as FrameIcon } from 'lucide-react';
import { useDebounce } from '../../hooks/useDebounce';
import api from '../../services/api';
import clsx from 'clsx';
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
interface AdEditorModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (listingId: number, price: number, description: string) => void;
clientId: number | null; // El aviso se vinculará a este cliente
}
interface PricingResult {
totalPrice: number;
wordCount: number;
details: string;
}
export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId }: AdEditorModalProps) {
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
const [operations, setOperations] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [calculating, setCalculating] = useState(false);
// Form State
const [categoryId, setCategoryId] = useState('');
const [operationId, setOperationId] = useState('');
const [text, setText] = useState('');
const debouncedText = useDebounce(text, 500);
const [days, setDays] = useState(3);
const [startDate, setStartDate] = useState(new Date(Date.now() + 86400000).toISOString().split('T')[0]); // Mañana default
// Styles
const [styles, setStyles] = useState({
isBold: false,
isFrame: false,
fontSize: 'normal',
alignment: 'left'
});
const [pricing, setPricing] = useState<PricingResult>({ totalPrice: 0, wordCount: 0, details: '' });
// Carga inicial de datos
useEffect(() => {
if (isOpen) {
// Reset state on open
setText('');
setDays(3);
setPricing({ totalPrice: 0, wordCount: 0, details: '' });
const loadData = async () => {
try {
const [catRes, opRes] = await Promise.all([api.get('/categories'), api.get('/operations')]);
setFlatCategories(processCategories(catRes.data));
setOperations(opRes.data);
} catch (e) {
console.error("Error cargando configuración", e);
}
};
loadData();
}
}, [isOpen]);
// Calculadora de Precio en Tiempo Real
useEffect(() => {
if (!categoryId || !text) return;
const calculate = async () => {
setCalculating(true);
try {
const res = await api.post('/pricing/calculate', {
categoryId: parseInt(categoryId),
text: debouncedText,
days: days,
isBold: styles.isBold,
isFrame: styles.isFrame,
startDate: startDate
});
setPricing(res.data);
} catch (e) {
console.error(e);
} finally {
setCalculating(false);
}
};
calculate();
}, [debouncedText, categoryId, days, styles, startDate]);
const handleSave = async () => {
if (!categoryId || !operationId || !text) return alert("Complete los campos obligatorios");
if (!clientId) return alert("Error interno: Cliente no identificado");
setLoading(true);
try {
// Creamos el aviso en estado 'Draft' (Borrador) o 'PendingPayment'
const payload = {
categoryId: parseInt(categoryId),
operationId: parseInt(operationId),
title: text.substring(0, 30) + '...',
description: text,
price: 0, // Precio del bien (opcional, no lo pedimos en este form simple)
adFee: pricing.totalPrice, // Costo del aviso
status: 'Draft', // Importante: Nace como borrador hasta que se paga la Orden
origin: 'Mostrador',
clientId: clientId,
// Datos técnicos de impresión
printText: text,
printStartDate: startDate,
printDaysCount: days,
isBold: styles.isBold,
isFrame: styles.isFrame,
printFontSize: styles.fontSize,
printAlignment: styles.alignment
};
const res = await api.post('/listings', payload);
// Devolvemos el ID y el Precio al Carrito
onConfirm(res.data.id, pricing.totalPrice, `Aviso: ${text.substring(0, 20)}... (${days} días)`);
onClose();
} catch (error) {
console.error(error);
alert("Error al guardar el borrador del aviso");
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[200] flex items-center justify-center p-4">
<div className="bg-white w-full max-w-4xl rounded-[2rem] shadow-2xl flex flex-col max-h-[90vh] overflow-hidden border border-slate-200">
{/* Header */}
<div className="bg-slate-50 px-8 py-5 border-b border-slate-100 flex justify-between items-center">
<div>
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">Redacción de Aviso</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Configuración Técnica e Impresión</p>
</div>
<button onClick={onClose} className="p-2 hover:bg-white rounded-xl text-slate-400 transition-colors"><X /></button>
</div>
{/* Body */}
<div className="p-8 overflow-y-auto flex-1 custom-scrollbar space-y-6">
{/* Clasificación */}
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Rubro</label>
<select
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-bold text-sm"
value={categoryId} onChange={e => setCategoryId(e.target.value)}
>
<option value="">Seleccione Rubro...</option>
{flatCategories.map(c => (
<option key={c.id} value={c.id} disabled={!c.isSelectable}>
{'\u00A0'.repeat(c.level * 2)} {c.name}
</option>
))}
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Operación</label>
<select
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-bold text-sm"
value={operationId} onChange={e => setOperationId(e.target.value)}
>
<option value="">Seleccione Operación...</option>
{operations.map(op => <option key={op.id} value={op.id}>{op.name}</option>)}
</select>
</div>
</div>
{/* Editor de Texto */}
<div className="flex gap-6 min-h-[200px]">
<div className="flex-[2] flex flex-col gap-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Texto del Aviso</label>
<textarea
className={clsx(
"w-full h-full p-4 border-2 border-slate-100 rounded-2xl resize-none outline-none focus:border-blue-500 transition-all font-mono text-sm leading-relaxed",
styles.isBold && "font-bold",
styles.fontSize === 'small' ? 'text-xs' : styles.fontSize === 'large' ? 'text-base' : 'text-sm',
styles.alignment === 'center' ? 'text-center' : styles.alignment === 'right' ? 'text-right' : styles.alignment === 'justify' ? 'text-justify' : 'text-left'
)}
placeholder="Escriba el contenido aquí..."
value={text} onChange={e => setText(e.target.value)}
/>
</div>
{/* Panel Lateral de Estilos */}
<div className="flex-1 bg-slate-50 p-4 rounded-2xl border border-slate-100 space-y-4 h-fit">
<div>
<label className="text-[9px] font-black text-slate-400 uppercase mb-2 block">Alineación</label>
<div className="flex bg-white p-1 rounded-lg border border-slate-200 justify-between">
{['left', 'center', 'right', 'justify'].map(align => (
<button
key={align}
onClick={() => setStyles({ ...styles, alignment: align })}
className={clsx("p-2 rounded-md transition-all", styles.alignment === align ? "bg-blue-100 text-blue-600" : "text-slate-400 hover:text-slate-600")}
>
{align === 'left' && <AlignLeft size={16} />}
{align === 'center' && <AlignCenter size={16} />}
{align === 'right' && <AlignRight size={16} />}
{align === 'justify' && <AlignJustify size={16} />}
</button>
))}
</div>
</div>
<div>
<label className="text-[9px] font-black text-slate-400 uppercase mb-2 block">Estilos</label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setStyles({ ...styles, isBold: !styles.isBold })}
className={clsx("p-2 rounded-lg border text-[10px] font-black uppercase flex items-center justify-center gap-1 transition-all", styles.isBold ? "bg-slate-800 text-white border-slate-800" : "bg-white border-slate-200 text-slate-500")}
>
<Bold size={12} /> Negrita
</button>
<button
onClick={() => setStyles({ ...styles, isFrame: !styles.isFrame })}
className={clsx("p-2 rounded-lg border text-[10px] font-black uppercase flex items-center justify-center gap-1 transition-all", styles.isFrame ? "bg-slate-800 text-white border-slate-800" : "bg-white border-slate-200 text-slate-500")}
>
<FrameIcon size={12} /> Recuadro
</button>
</div>
</div>
<div>
<label className="text-[9px] font-black text-slate-400 uppercase mb-2 block">Tamaño Fuente</label>
<div className="flex bg-white p-1 rounded-lg border border-slate-200 gap-1">
{['small', 'normal', 'large'].map(size => (
<button
key={size}
onClick={() => setStyles({ ...styles, fontSize: size })}
className={clsx("flex-1 p-1.5 rounded-md flex justify-center items-end transition-all", styles.fontSize === size ? "bg-blue-100 text-blue-600" : "text-slate-400 hover:text-slate-600")}
>
<Type size={size === 'small' ? 12 : size === 'normal' ? 16 : 20} />
</button>
))}
</div>
</div>
</div>
</div>
{/* Configuración de Fecha */}
<div className="grid grid-cols-3 gap-6 p-4 bg-blue-50/50 rounded-2xl border border-blue-100">
<div className="col-span-1">
<label className="text-[10px] font-black text-blue-400 uppercase tracking-widest ml-1 mb-1 block">Inicio Publicación</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 text-blue-300" size={16} />
<input
type="date"
className="w-full pl-10 pr-4 py-2 bg-white border border-blue-200 rounded-xl text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-blue-500/20"
value={startDate} onChange={e => setStartDate(e.target.value)}
/>
</div>
</div>
<div className="col-span-1">
<label className="text-[10px] font-black text-blue-400 uppercase tracking-widest ml-1 mb-1 block">Cantidad Días</label>
<div className="flex items-center bg-white border border-blue-200 rounded-xl overflow-hidden h-[38px]">
<button onClick={() => setDays(Math.max(1, days - 1))} className="px-3 hover:bg-blue-50 text-blue-400 transition-colors">-</button>
<input
type="number"
className="w-full text-center font-black text-slate-700 text-sm outline-none"
value={days} onChange={e => setDays(parseInt(e.target.value) || 1)}
/>
<button onClick={() => setDays(days + 1)} className="px-3 hover:bg-blue-50 text-blue-400 transition-colors">+</button>
</div>
</div>
<div className="col-span-1 flex flex-col justify-center items-end">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Costo Estimado</span>
<div className="text-2xl font-mono font-black text-slate-900">
{calculating ? <span className="animate-pulse">...</span> : `$ ${pricing.totalPrice.toLocaleString()}`}
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="p-6 bg-slate-50 border-t border-slate-100 flex justify-end gap-3">
<button onClick={onClose} className="px-6 py-3 rounded-xl font-bold text-slate-500 hover:bg-white transition-all text-xs uppercase tracking-wider">
Cancelar
</button>
<button
onClick={handleSave}
disabled={loading || calculating || !text}
className="bg-blue-600 text-white px-8 py-3 rounded-xl font-black text-xs uppercase tracking-widest shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save size={16} /> {loading ? 'Procesando...' : 'Confirmar y Agregar al Carrito'}
</button>
</div>
</div>
</div>
);
}