Feat: Cambios Varios

This commit is contained in:
2025-12-23 15:12:57 -03:00
parent 32663e6324
commit 8bc1308bc5
58 changed files with 4080 additions and 663 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import api from '../../services/api';
import { Save, DollarSign } from 'lucide-react';
import { Save, DollarSign, FileText, Type, AlertCircle } from 'lucide-react';
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
interface PricingConfig {
basePrice: number;
@@ -12,129 +13,202 @@ interface PricingConfig {
frameSurcharge: number;
}
interface Category { id: number; name: string; }
export default function PricingManager() {
const [categories, setCategories] = useState<Category[]>([]);
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
const [selectedCat, setSelectedCat] = useState<number | null>(null);
const [config, setConfig] = useState<PricingConfig>({
// 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
api.get('/categories').then(res => setCategories(res.data));
// 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({ // Default si no existe
basePrice: 0, baseWordCount: 15, extraWordPrice: 0,
specialChars: '!', specialCharPrice: 0, boldSurcharge: 0, frameSurcharge: 0
});
});
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;
setLoading(true);
setSaving(true);
try {
await api.post('/pricing', { ...config, categoryId: selectedCat });
alert('Configuración guardada correctamente.');
} catch (e) {
alert('Error al guardar.');
} finally {
setLoading(false);
setSaving(false);
}
};
// Helper para el nombre del rubro seleccionado
const selectedCatName = flatCategories.find(c => c.id === selectedCat)?.path;
return (
<div className="max-w-4xl mx-auto">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
<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>
<div className="bg-white p-6 rounded shadow mb-6">
<label className="block text-sm font-medium mb-2">Seleccionar Rubro a Configurar</label>
<select
className="w-full border p-2 rounded"
onChange={e => setSelectedCat(Number(e.target.value))}
value={selectedCat || ''}
>
<option value="">-- Seleccione --</option>
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
{/* 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="bg-white p-6 rounded shadow border border-gray-200">
<h3 className="font-bold text-lg mb-4 border-b pb-2">Reglas de Precio</h3>
<div className="space-y-6 animate-fade-in">
<div className="grid grid-cols-2 gap-6">
{/* Tarifa Base */}
<div className="space-y-4">
<h4 className="font-semibold text-blue-600">Base</h4>
<div>
<label className="block text-sm text-gray-600">Precio Mínimo ($)</label>
<input type="number" className="border p-2 rounded w-full"
value={config.basePrice} onChange={e => setConfig({ ...config, basePrice: parseFloat(e.target.value) })} />
</div>
<div>
<label className="block text-sm text-gray-600">Palabras Incluidas (Cant.)</label>
<input type="number" className="border p-2 rounded w-full"
value={config.baseWordCount} onChange={e => setConfig({ ...config, baseWordCount: parseInt(e.target.value) })} />
</div>
<div>
<label className="block text-sm text-gray-600">Costo Palabra Extra ($)</label>
<input type="number" className="border p-2 rounded w-full"
value={config.extraWordPrice} onChange={e => setConfig({ ...config, extraWordPrice: parseFloat(e.target.value) })} />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Extras y Estilos */}
<div className="space-y-4">
<h4 className="font-semibold text-purple-600">Extras y Recargos</h4>
<div>
<label className="block text-sm text-gray-600">Caracteres Especiales (ej: !$%)</label>
<input type="text" className="border p-2 rounded w-full"
value={config.specialChars} onChange={e => setConfig({ ...config, specialChars: e.target.value })} />
</div>
<div>
<label className="block text-sm text-gray-600">Costo por Caracter Especial ($)</label>
<input type="number" className="border p-2 rounded w-full"
value={config.specialCharPrice} onChange={e => setConfig({ ...config, specialCharPrice: parseFloat(e.target.value) })} />
</div>
<div className="grid grid-cols-2 gap-2">
{/* 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 text-gray-600">Recargo Negrita ($)</label>
<input type="number" className="border p-2 rounded w-full"
value={config.boldSurcharge} onChange={e => setConfig({ ...config, boldSurcharge: parseFloat(e.target.value) })} />
<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>
<label className="block text-sm text-gray-600">Recargo Recuadro ($)</label>
<input type="number" className="border p-2 rounded w-full"
value={config.frameSurcharge} onChange={e => setConfig({ ...config, frameSurcharge: parseFloat(e.target.value) })} />
<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>
<div className="mt-8 flex justify-end">
{/* 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={loading}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 flex items-center gap-2"
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={18} /> Guardar Configuración
<Save size={20} /> {saving ? 'Guardando...' : 'GUARDAR CAMBIOS'}
</button>
</div>
</div>
)}
</div>