142 lines
5.8 KiB
TypeScript
142 lines
5.8 KiB
TypeScript
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import api from '../../services/api';
|
||
|
|
import { Save, DollarSign } from 'lucide-react';
|
||
|
|
|
||
|
|
interface PricingConfig {
|
||
|
|
basePrice: number;
|
||
|
|
baseWordCount: number;
|
||
|
|
extraWordPrice: number;
|
||
|
|
specialChars: string;
|
||
|
|
specialCharPrice: number;
|
||
|
|
boldSurcharge: number;
|
||
|
|
frameSurcharge: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Category { id: number; name: string; }
|
||
|
|
|
||
|
|
export default function PricingManager() {
|
||
|
|
const [categories, setCategories] = useState<Category[]>([]);
|
||
|
|
const [selectedCat, setSelectedCat] = useState<number | null>(null);
|
||
|
|
const [config, setConfig] = useState<PricingConfig>({
|
||
|
|
basePrice: 0, baseWordCount: 15, extraWordPrice: 0,
|
||
|
|
specialChars: '!', specialCharPrice: 0, boldSurcharge: 0, frameSurcharge: 0
|
||
|
|
});
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
// Cargar categorías
|
||
|
|
api.get('/categories').then(res => setCategories(res.data));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (selectedCat) {
|
||
|
|
// 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
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}, [selectedCat]);
|
||
|
|
|
||
|
|
const handleSave = async () => {
|
||
|
|
if (!selectedCat) return;
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
await api.post('/pricing', { ...config, categoryId: selectedCat });
|
||
|
|
alert('Configuración guardada correctamente.');
|
||
|
|
} catch (e) {
|
||
|
|
alert('Error al guardar.');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="max-w-4xl mx-auto">
|
||
|
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
|
||
|
|
<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>
|
||
|
|
</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="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>
|
||
|
|
|
||
|
|
{/* 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">
|
||
|
|
<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) })} />
|
||
|
|
</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>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="mt-8 flex justify-end">
|
||
|
|
<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"
|
||
|
|
>
|
||
|
|
<Save size={18} /> Guardar Configuración
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|