Feat: Cambios Varios
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user