Files
SIG-CM/frontend/admin-panel/src/pages/Pricing/PricingManager.tsx

216 lines
10 KiB
TypeScript
Raw Normal View History

2025-12-18 13:32:50 -03:00
import { useState, useEffect } from 'react';
import api from '../../services/api';
2025-12-23 15:12:57 -03:00
import { Save, DollarSign, FileText, Type, AlertCircle } from 'lucide-react';
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
2025-12-18 13:32:50 -03:00
interface PricingConfig {
basePrice: number;
baseWordCount: number;
extraWordPrice: number;
specialChars: string;
specialCharPrice: number;
boldSurcharge: number;
frameSurcharge: number;
}
export default function PricingManager() {
2025-12-23 15:12:57 -03:00
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
2025-12-18 13:32:50 -03:00
const [selectedCat, setSelectedCat] = useState<number | null>(null);
2025-12-23 15:12:57 -03:00
// Configuración por defecto
const defaultConfig: PricingConfig = {
2025-12-18 13:32:50 -03:00
basePrice: 0, baseWordCount: 15, extraWordPrice: 0,
specialChars: '!', specialCharPrice: 0, boldSurcharge: 0, frameSurcharge: 0
2025-12-23 15:12:57 -03:00
};
const [config, setConfig] = useState<PricingConfig>(defaultConfig);
2025-12-18 13:32:50 -03:00
const [loading, setLoading] = useState(false);
2025-12-23 15:12:57 -03:00
const [saving, setSaving] = useState(false);
2025-12-18 13:32:50 -03:00
useEffect(() => {
2025-12-23 15:12:57 -03:00
// Cargar categorías y procesar árbol
api.get('/categories').then(res => {
const processed = processCategories(res.data);
setFlatCategories(processed);
});
2025-12-18 13:32:50 -03:00
}, []);
useEffect(() => {
if (selectedCat) {
2025-12-23 15:12:57 -03:00
setLoading(true);
2025-12-18 13:32:50 -03:00
// Cargar config existente
2025-12-23 15:12:57 -03:00
api.get(`/pricing/${selectedCat}`)
.then(res => {
if (res.data) setConfig(res.data);
else setConfig(defaultConfig); // Reset si es nuevo
})
.finally(() => setLoading(false));
2025-12-18 13:32:50 -03:00
}
}, [selectedCat]);
const handleSave = async () => {
if (!selectedCat) return;
2025-12-23 15:12:57 -03:00
setSaving(true);
2025-12-18 13:32:50 -03:00
try {
await api.post('/pricing', { ...config, categoryId: selectedCat });
alert('Configuración guardada correctamente.');
} catch (e) {
alert('Error al guardar.');
} finally {
2025-12-23 15:12:57 -03:00
setSaving(false);
2025-12-18 13:32:50 -03:00
}
};
2025-12-23 15:12:57 -03:00
// Helper para el nombre del rubro seleccionado
const selectedCatName = flatCategories.find(c => c.id === selectedCat)?.path;
2025-12-18 13:32:50 -03:00
return (
2025-12-23 15:12:57 -03:00
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 text-gray-800">
2025-12-18 13:32:50 -03:00
<DollarSign className="text-green-600" />
Gestor de Tarifas y Reglas
</h2>
2025-12-23 15:12:57 -03:00
{/* SELECTOR DE RUBRO */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mb-6">
<label className="block text-sm font-bold text-gray-700 mb-2">Seleccionar Rubro a Configurar</label>
<div className="relative">
<select
className="w-full border border-gray-300 p-3 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-medium appearance-none bg-white"
onChange={e => setSelectedCat(Number(e.target.value) || null)}
value={selectedCat || ''}
>
<option value="">-- Seleccione un Rubro --</option>
{flatCategories.map(cat => (
<option
key={cat.id}
value={cat.id}
disabled={!cat.isSelectable} // Bloqueamos padres para forzar config en hojas
className={cat.isSelectable ? "text-gray-900 font-medium" : "text-gray-400 font-bold bg-gray-50"}
>
{'\u00A0\u00A0'.repeat(cat.level)}
{cat.hasChildren ? `📂 ${cat.name}` : `${cat.name}`}
</option>
))}
</select>
{/* Flecha custom para estilo */}
<div className="absolute right-4 top-3.5 pointer-events-none text-gray-500"></div>
</div>
{selectedCat && (
<p className="mt-2 text-sm text-blue-600 bg-blue-50 p-2 rounded inline-block">
Editando: <strong>{selectedCatName}</strong>
</p>
)}
2025-12-18 13:32:50 -03:00
</div>
{selectedCat && (
2025-12-23 15:12:57 -03:00
<div className="space-y-6 animate-fade-in">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* TARIFA BASE */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 className="font-bold text-lg mb-4 pb-2 border-b border-gray-100 flex items-center gap-2 text-gray-800">
<FileText size={20} className="text-blue-500" /> Tarifa Base (Texto)
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">Precio Mínimo ($)</label>
<div className="relative">
<span className="absolute left-3 top-2 text-gray-500">$</span>
<input type="number" className="pl-6 border p-2 rounded w-full focus:ring-2 focus:ring-blue-500 outline-none"
value={config.basePrice} onChange={e => setConfig({ ...config, basePrice: parseFloat(e.target.value) })} />
</div>
<p className="text-xs text-gray-400 mt-1">Costo por el aviso básico por día.</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">Palabras Incluidas</label>
<input type="number" className="border p-2 rounded w-full focus:ring-2 focus:ring-blue-500 outline-none"
value={config.baseWordCount} onChange={e => setConfig({ ...config, baseWordCount: parseInt(e.target.value) })} />
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">Costo Palabra Extra ($)</label>
<input type="number" className="border p-2 rounded w-full focus:ring-2 focus:ring-blue-500 outline-none"
value={config.extraWordPrice} onChange={e => setConfig({ ...config, extraWordPrice: parseFloat(e.target.value) })} />
</div>
</div>
2025-12-18 13:32:50 -03:00
</div>
</div>
2025-12-23 15:12:57 -03:00
{/* CONTENIDO ESPECIAL */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 className="font-bold text-lg mb-4 pb-2 border-b border-gray-100 flex items-center gap-2 text-gray-800">
<AlertCircle size={20} className="text-orange-500" /> Caracteres Especiales
</h3>
<div className="space-y-4">
2025-12-18 13:32:50 -03:00
<div>
2025-12-23 15:12:57 -03:00
<label className="block text-sm font-medium text-gray-600 mb-1">Caracteres a cobrar (ej: !$%)</label>
<input type="text" className="border p-2 rounded w-full font-mono tracking-widest focus:ring-2 focus:ring-orange-500 outline-none"
value={config.specialChars} onChange={e => setConfig({ ...config, specialChars: e.target.value })} />
<p className="text-xs text-gray-400 mt-1">Cada uno de estos símbolos se cobrará aparte.</p>
2025-12-18 13:32:50 -03:00
</div>
2025-12-23 15:12:57 -03:00
2025-12-18 13:32:50 -03:00
<div>
2025-12-23 15:12:57 -03:00
<label className="block text-sm font-medium text-gray-600 mb-1">Costo por Símbolo ($)</label>
<div className="relative">
<span className="absolute left-3 top-2 text-gray-500">$</span>
<input type="number" className="pl-6 border p-2 rounded w-full focus:ring-2 focus:ring-orange-500 outline-none"
value={config.specialCharPrice} onChange={e => setConfig({ ...config, specialCharPrice: parseFloat(e.target.value) })} />
</div>
2025-12-18 13:32:50 -03:00
</div>
</div>
</div>
2025-12-23 15:12:57 -03:00
{/* ESTILOS VISUALES */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 lg:col-span-2">
<h3 className="font-bold text-lg mb-4 pb-2 border-b border-gray-100 flex items-center gap-2 text-gray-800">
<Type size={20} className="text-purple-500" /> Estilos Visuales (Recargos)
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="flex items-center gap-4 bg-gray-50 p-4 rounded border border-gray-200">
<div className="font-bold text-xl px-3 py-1 border-2 border-transparent bg-white shadow-sm">N</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-600 mb-1">Recargo Negrita ($)</label>
<input type="number" className="border p-2 rounded w-full focus:ring-2 focus:ring-purple-500 outline-none"
value={config.boldSurcharge} onChange={e => setConfig({ ...config, boldSurcharge: parseFloat(e.target.value) })} />
</div>
</div>
<div className="flex items-center gap-4 bg-gray-50 p-4 rounded border border-gray-200">
<div className="font-bold text-xl px-3 py-1 border-2 border-black bg-white shadow-sm">A</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-600 mb-1">Recargo Recuadro ($)</label>
<input type="number" className="border p-2 rounded w-full focus:ring-2 focus:ring-purple-500 outline-none"
value={config.frameSurcharge} onChange={e => setConfig({ ...config, frameSurcharge: parseFloat(e.target.value) })} />
</div>
</div>
</div>
</div>
2025-12-18 13:32:50 -03:00
</div>
2025-12-23 15:12:57 -03:00
{/* BARRA DE ACCIÓN FLOTANTE */}
<div className="sticky bottom-4 bg-gray-900 text-white p-4 rounded-lg shadow-lg flex justify-between items-center z-10">
<div className="text-sm">
Configurando tarifas para: <span className="font-bold text-green-400">{selectedCatName}</span>
</div>
2025-12-18 13:32:50 -03:00
<button
onClick={handleSave}
2025-12-23 15:12:57 -03:00
disabled={saving}
className="bg-green-600 text-white px-8 py-2 rounded font-bold hover:bg-green-500 disabled:opacity-50 transition flex items-center gap-2"
2025-12-18 13:32:50 -03:00
>
2025-12-23 15:12:57 -03:00
<Save size={20} /> {saving ? 'Guardando...' : 'GUARDAR CAMBIOS'}
2025-12-18 13:32:50 -03:00
</button>
</div>
2025-12-23 15:12:57 -03:00
2025-12-18 13:32:50 -03:00
</div>
)}
</div>
);
}