206 lines
7.6 KiB
TypeScript
206 lines
7.6 KiB
TypeScript
|
|
import { useEffect, useState } from 'react';
|
||
|
|
import api from '../../services/api';
|
||
|
|
import { Trash2, Plus, Tag } from 'lucide-react';
|
||
|
|
import { formatDate } from '../../utils/formatters';
|
||
|
|
|
||
|
|
interface Coupon {
|
||
|
|
id: number;
|
||
|
|
code: string;
|
||
|
|
discountType: 'Percentage' | 'Fixed';
|
||
|
|
discountValue: number;
|
||
|
|
expiryDate?: string;
|
||
|
|
usageCount: number;
|
||
|
|
maxUsages?: number;
|
||
|
|
isActive: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function CouponsPage() {
|
||
|
|
const [coupons, setCoupons] = useState<Coupon[]>([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [showForm, setShowForm] = useState(false);
|
||
|
|
|
||
|
|
// Form State
|
||
|
|
const [newCoupon, setNewCoupon] = useState({
|
||
|
|
code: '',
|
||
|
|
discountType: 'Percentage',
|
||
|
|
discountValue: 0,
|
||
|
|
expiryDate: '',
|
||
|
|
maxUsages: '',
|
||
|
|
maxUsagesPerUser: ''
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadCoupons();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const loadCoupons = async () => {
|
||
|
|
try {
|
||
|
|
const res = await api.get('/coupons');
|
||
|
|
setCoupons(res.data);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error loading coupons", error);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDelete = async (id: number) => {
|
||
|
|
if (!confirm('¿Eliminar cupón?')) return;
|
||
|
|
try {
|
||
|
|
await api.delete(`/coupons/${id}`);
|
||
|
|
loadCoupons();
|
||
|
|
} catch (error) {
|
||
|
|
alert("Error al eliminar");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCreate = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
try {
|
||
|
|
const payload = {
|
||
|
|
...newCoupon,
|
||
|
|
maxUsages: newCoupon.maxUsages ? parseInt(newCoupon.maxUsages) : null,
|
||
|
|
maxUsagesPerUser: newCoupon.maxUsagesPerUser ? parseInt(newCoupon.maxUsagesPerUser) : null,
|
||
|
|
expiryDate: newCoupon.expiryDate ? newCoupon.expiryDate : null
|
||
|
|
};
|
||
|
|
await api.post('/coupons', payload);
|
||
|
|
setShowForm(false);
|
||
|
|
setNewCoupon({ code: '', discountType: 'Percentage', discountValue: 0, expiryDate: '', maxUsages: '', maxUsagesPerUser: '' });
|
||
|
|
loadCoupons();
|
||
|
|
} catch (error) {
|
||
|
|
alert("Error al crear cupón");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) return <div className="p-10 text-center">Cargando...</div>;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<h1 className="text-2xl font-bold text-slate-800">Gestión de Cupones</h1>
|
||
|
|
<button
|
||
|
|
onClick={() => setShowForm(!showForm)}
|
||
|
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-700 transition"
|
||
|
|
>
|
||
|
|
<Plus size={18} /> Nuevo Cupón
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{showForm && (
|
||
|
|
<form onSubmit={handleCreate} className="bg-white p-6 rounded-xl shadow border border-slate-100 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4 items-end">
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Código</label>
|
||
|
|
<input
|
||
|
|
required
|
||
|
|
type="text"
|
||
|
|
className="w-full border p-2 rounded"
|
||
|
|
placeholder="Ej: CUPON2026"
|
||
|
|
value={newCoupon.code}
|
||
|
|
onChange={e => setNewCoupon({ ...newCoupon, code: e.target.value })}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Tipo Descuento</label>
|
||
|
|
<select
|
||
|
|
className="w-full border p-2 rounded"
|
||
|
|
value={newCoupon.discountType}
|
||
|
|
onChange={e => setNewCoupon({ ...newCoupon, discountType: e.target.value })}
|
||
|
|
>
|
||
|
|
<option value="Percentage">Porcentaje (%)</option>
|
||
|
|
<option value="Fixed">Monto Fijo ($)</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Valor</label>
|
||
|
|
<input
|
||
|
|
required
|
||
|
|
type="number"
|
||
|
|
className="w-full border p-2 rounded"
|
||
|
|
value={newCoupon.discountValue}
|
||
|
|
onChange={e => setNewCoupon({ ...newCoupon, discountValue: Number(e.target.value) })}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Expiración (Opcional)</label>
|
||
|
|
<input
|
||
|
|
type="date"
|
||
|
|
className="w-full border p-2 rounded"
|
||
|
|
value={newCoupon.expiryDate}
|
||
|
|
onChange={e => setNewCoupon({ ...newCoupon, expiryDate: e.target.value })}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Usos Máx.</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
className="w-full border p-2 rounded"
|
||
|
|
placeholder="Ilimitado"
|
||
|
|
value={newCoupon.maxUsages}
|
||
|
|
onChange={e => setNewCoupon({ ...newCoupon, maxUsages: e.target.value })}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Límite x Usuario</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
className="w-full border p-2 rounded"
|
||
|
|
placeholder="Ilimitado"
|
||
|
|
value={newCoupon.maxUsagesPerUser}
|
||
|
|
onChange={e => setNewCoupon({ ...newCoupon, maxUsagesPerUser: e.target.value })}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<button type="submit" className="bg-emerald-500 text-white p-2 rounded font-bold hover:bg-emerald-600">
|
||
|
|
Guardar
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||
|
|
<table className="w-full text-left">
|
||
|
|
<thead className="bg-slate-50 border-b border-slate-200">
|
||
|
|
<tr>
|
||
|
|
<th className="p-4 font-bold text-slate-600 text-xs uppercase">Código</th>
|
||
|
|
<th className="p-4 font-bold text-slate-600 text-xs uppercase">Descuento</th>
|
||
|
|
<th className="p-4 font-bold text-slate-600 text-xs uppercase">Usos</th>
|
||
|
|
<th className="p-4 font-bold text-slate-600 text-xs uppercase">Expira</th>
|
||
|
|
<th className="p-4 font-bold text-slate-600 text-xs uppercase text-right">Acciones</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{coupons.map(coupon => (
|
||
|
|
<tr key={coupon.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50">
|
||
|
|
<td className="p-4 font-mono font-bold text-slate-800 flex items-center gap-2">
|
||
|
|
<Tag size={14} className="text-blue-500" />
|
||
|
|
{coupon.code}
|
||
|
|
</td>
|
||
|
|
<td className="p-4 text-sm text-slate-600">
|
||
|
|
{coupon.discountType === 'Percentage' ? `${coupon.discountValue}%` : `$${coupon.discountValue}`}
|
||
|
|
</td>
|
||
|
|
<td className="p-4 text-sm text-slate-600">
|
||
|
|
{coupon.usageCount} {coupon.maxUsages ? `/ ${coupon.maxUsages}` : ''}
|
||
|
|
</td>
|
||
|
|
<td className="p-4 text-sm text-slate-600">
|
||
|
|
{coupon.expiryDate ? formatDate(coupon.expiryDate) : '-'}
|
||
|
|
</td>
|
||
|
|
<td className="p-4 text-right">
|
||
|
|
<button
|
||
|
|
onClick={() => handleDelete(coupon.id)}
|
||
|
|
className="text-rose-400 hover:text-rose-600 transition"
|
||
|
|
>
|
||
|
|
<Trash2 size={18} />
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
{coupons.length === 0 && (
|
||
|
|
<tr>
|
||
|
|
<td colSpan={5} className="p-8 text-center text-slate-400 text-sm">No hay cupones activos.</td>
|
||
|
|
</tr>
|
||
|
|
)}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|