216 lines
10 KiB
TypeScript
216 lines
10 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import api from '../../services/api';
|
|
import { Save, DollarSign, FileText, Type, AlertCircle } from 'lucide-react';
|
|
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
|
|
|
|
interface PricingConfig {
|
|
basePrice: number;
|
|
baseWordCount: number;
|
|
extraWordPrice: number;
|
|
specialChars: string;
|
|
specialCharPrice: number;
|
|
boldSurcharge: number;
|
|
frameSurcharge: number;
|
|
}
|
|
|
|
export default function PricingManager() {
|
|
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
|
|
const [selectedCat, setSelectedCat] = useState<number | null>(null);
|
|
|
|
// Configuración por defecto
|
|
const defaultConfig: PricingConfig = {
|
|
basePrice: 0, baseWordCount: 15, extraWordPrice: 0,
|
|
specialChars: '!', specialCharPrice: 0, boldSurcharge: 0, frameSurcharge: 0
|
|
};
|
|
|
|
const [config, setConfig] = useState<PricingConfig>(defaultConfig);
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Cargar categorías y procesar árbol
|
|
api.get('/categories').then(res => {
|
|
const processed = processCategories(res.data);
|
|
setFlatCategories(processed);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (selectedCat) {
|
|
setLoading(true);
|
|
// Cargar config existente
|
|
api.get(`/pricing/${selectedCat}`)
|
|
.then(res => {
|
|
if (res.data) setConfig(res.data);
|
|
else setConfig(defaultConfig); // Reset si es nuevo
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}
|
|
}, [selectedCat]);
|
|
|
|
const handleSave = async () => {
|
|
if (!selectedCat) return;
|
|
setSaving(true);
|
|
try {
|
|
await api.post('/pricing', { ...config, categoryId: selectedCat });
|
|
alert('Configuración guardada correctamente.');
|
|
} catch (e) {
|
|
alert('Error al guardar.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// Helper para el nombre del rubro seleccionado
|
|
const selectedCatName = flatCategories.find(c => c.id === selectedCat)?.path;
|
|
|
|
return (
|
|
<div className="max-w-5xl mx-auto">
|
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 text-gray-800">
|
|
<DollarSign className="text-green-600" />
|
|
Gestor de Tarifas y Reglas
|
|
</h2>
|
|
|
|
{/* 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>
|
|
)}
|
|
</div>
|
|
|
|
{selectedCat && (
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 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">
|
|
<div>
|
|
<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>
|
|
</div>
|
|
|
|
<div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 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>
|
|
|
|
</div>
|
|
|
|
{/* 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>
|
|
<button
|
|
onClick={handleSave}
|
|
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"
|
|
>
|
|
<Save size={20} /> {saving ? 'Guardando...' : 'GUARDAR CAMBIOS'}
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |