almeira-web/dripzone-app/src/app/admin/cupones/page.tsx

1068 lines
47 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useState, useEffect } from 'react';
import {
Tag,
Ticket,
Percent,
DollarSign,
Calendar,
Clock,
CheckCircle2,
AlertCircle,
Plus,
Search,
Filter,
Trash2,
Edit2,
Copy,
Check,
Share2,
ExternalLink,
ShieldCheck,
Sparkles,
RefreshCw,
Power,
BarChart3,
TrendingUp,
Flame,
} from 'lucide-react';
import { Coupon } from '@/types';
import { formatCOP } from '@/lib/pricing-engine';
export default function AdminCuponesPage() {
const [coupons, setCoupons] = useState<Coupon[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive' | 'expired' | 'exhausted'>('all');
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingCoupon, setEditingCoupon] = useState<Coupon | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [copiedCode, setCopiedCode] = useState<string | null>(null);
const [copiedLink, setCopiedLink] = useState<string | null>(null);
// Form Fields
const [code, setCode] = useState('');
const [description, setDescription] = useState('');
const [discountType, setDiscountType] = useState<'percentage' | 'fixed' | 'per_item'>('percentage');
const [discountValue, setDiscountValue] = useState<number>(10);
const [minSpend, setMinSpend] = useState<number>(0);
const [maxDiscount, setMaxDiscount] = useState<number>(0);
// Expiration config
const [hasExpiry, setHasExpiry] = useState(true);
const [expiresAt, setExpiresAt] = useState('');
// Quantity limit config
const [hasUsageLimit, setHasUsageLimit] = useState(true);
const [usageLimit, setUsageLimit] = useState<number>(50);
const [isActive, setIsActive] = useState(true);
const [formError, setFormError] = useState<string | null>(null);
const fetchCoupons = async () => {
setLoading(true);
try {
const res = await fetch('/api/coupons');
const data = await res.json();
if (data.success && Array.isArray(data.coupons)) {
setCoupons(data.coupons);
}
} catch (err) {
console.error('Error cargando cupones:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCoupons();
}, []);
const openCreateModal = () => {
setEditingCoupon(null);
setCode('');
setDescription('');
setDiscountType('percentage');
setDiscountValue(10);
setMinSpend(0);
setMaxDiscount(0);
setHasUsageLimit(true);
setUsageLimit(50);
// Default expiration: in 15 days
setHasExpiry(true);
const in15Days = new Date();
in15Days.setDate(in15Days.getDate() + 15);
in15Days.setHours(23, 59, 0, 0);
setExpiresAt(in15Days.toISOString().slice(0, 16));
setIsActive(true);
setFormError(null);
setIsModalOpen(true);
};
const openEditModal = (c: Coupon) => {
setEditingCoupon(c);
setCode(c.code);
setDescription(c.description || '');
setDiscountType(c.discountType);
setDiscountValue(c.discountValue);
setMinSpend(c.minSpend || 0);
setMaxDiscount(c.maxDiscount || 0);
if (c.usageLimit && c.usageLimit > 0) {
setHasUsageLimit(true);
setUsageLimit(c.usageLimit);
} else {
setHasUsageLimit(false);
setUsageLimit(0);
}
if (c.expiresAt) {
setHasExpiry(true);
try {
const d = new Date(c.expiresAt);
setExpiresAt(d.toISOString().slice(0, 16));
} catch (_) {
setExpiresAt('');
}
} else {
setHasExpiry(false);
setExpiresAt('');
}
setIsActive(c.isActive !== false);
setFormError(null);
setIsModalOpen(true);
};
const handleSetExpiryPreset = (days: number) => {
setHasExpiry(true);
const target = new Date();
target.setDate(target.getDate() + days);
target.setHours(23, 59, 0, 0);
setExpiresAt(target.toISOString().slice(0, 16));
};
const handleSetEndOfMonth = () => {
setHasExpiry(true);
const now = new Date();
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
lastDay.setHours(23, 59, 0, 0);
setExpiresAt(lastDay.toISOString().slice(0, 16));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
const cleanCode = code.toUpperCase().trim();
if (!cleanCode) {
setFormError('El código del cupón es obligatorio.');
return;
}
if (discountValue <= 0) {
setFormError('El valor del descuento debe ser mayor a 0.');
return;
}
if (discountType === 'percentage' && discountValue > 100) {
setFormError('El porcentaje de descuento no puede ser superior al 100%.');
return;
}
if (hasExpiry && !expiresAt) {
setFormError('Debes seleccionar una fecha y hora de expiración.');
return;
}
if (hasUsageLimit && usageLimit <= 0) {
setFormError('El límite de usos debe ser al menos 1.');
return;
}
setIsSubmitting(true);
const payload: Partial<Coupon> = {
code: cleanCode,
description,
discountType,
discountValue: Number(discountValue),
minSpend: Number(minSpend) || 0,
maxDiscount: Number(maxDiscount) || 0,
usageLimit: hasUsageLimit ? Number(usageLimit) : 0,
expiresAt: hasExpiry && expiresAt ? new Date(expiresAt).toISOString() : undefined,
isActive,
};
try {
if (editingCoupon) {
const res = await fetch('/api/coupons', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: editingCoupon.id, ...payload }),
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Error al actualizar cupón.');
}
} else {
const res = await fetch('/api/coupons', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Error al crear cupón.');
}
}
setIsModalOpen(false);
await fetchCoupons();
} catch (err: any) {
setFormError(err.message || 'Error guardando cupón.');
} finally {
setIsSubmitting(false);
}
};
const handleToggleActive = async (c: Coupon) => {
try {
const nextState = !c.isActive;
// Optimistic update
setCoupons((prev) =>
prev.map((item) => (item.id === c.id ? { ...item, isActive: nextState } : item))
);
await fetch('/api/coupons', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: c.id, isActive: nextState }),
});
} catch (err) {
console.error('Error alternando estado del cupón:', err);
fetchCoupons();
}
};
const handleDelete = async (c: Coupon) => {
if (!confirm(`¿Estás seguro de eliminar el cupón "${c.code}"? Esta acción no se puede deshacer.`)) {
return;
}
try {
setCoupons((prev) => prev.filter((item) => item.id !== c.id));
await fetch(`/api/coupons?id=${c.id}`, { method: 'DELETE' });
} catch (err) {
console.error('Error eliminando cupón:', err);
fetchCoupons();
}
};
const handleCopyCode = (couponCode: string) => {
navigator.clipboard.writeText(couponCode);
setCopiedCode(couponCode);
setTimeout(() => setCopiedCode(null), 2500);
};
const handleCopyLink = (couponCode: string) => {
const origin = typeof window !== 'undefined' ? window.location.origin : 'https://drip-zone.online';
const link = `${origin}/checkout?coupon=${couponCode}`;
navigator.clipboard.writeText(link);
setCopiedLink(couponCode);
setTimeout(() => setCopiedLink(null), 2500);
};
// Helper status calculations
const isCouponExpired = (c: Coupon) => {
if (!c.expiresAt) return false;
return new Date(c.expiresAt).getTime() < Date.now();
};
const isCouponExhausted = (c: Coupon) => {
if (!c.usageLimit || c.usageLimit <= 0) return false;
return (c.usageCount || 0) >= c.usageLimit;
};
const filteredCoupons = coupons.filter((c) => {
const matchesSearch =
c.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
(c.description || '').toLowerCase().includes(searchTerm.toLowerCase());
if (!matchesSearch) return false;
const expired = isCouponExpired(c);
const exhausted = isCouponExhausted(c);
if (statusFilter === 'active') return c.isActive && !expired && !exhausted;
if (statusFilter === 'inactive') return !c.isActive;
if (statusFilter === 'expired') return expired;
if (statusFilter === 'exhausted') return exhausted;
return true;
});
// KPI Calculations
const totalCoupons = coupons.length;
const activeCoupons = coupons.filter((c) => c.isActive && !isCouponExpired(c) && !isCouponExhausted(c)).length;
const totalRedemptions = coupons.reduce((sum, c) => sum + (c.usageCount || 0), 0);
const expiringSoonCount = coupons.filter((c) => {
if (!c.expiresAt || !c.isActive || isCouponExpired(c)) return false;
const diffDays = (new Date(c.expiresAt).getTime() - Date.now()) / (1000 * 3600 * 24);
return diffDays <= 7;
}).length;
return (
<div className="space-y-6 max-w-7xl mx-auto pb-12 font-sans">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-gradient-to-r from-neutral-900 via-neutral-900/90 to-purple-950/30 p-6 rounded-3xl border border-purple-500/20 shadow-xl">
<div className="space-y-1">
<div className="flex items-center gap-2">
<div className="w-9 h-9 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center border border-purple-500/30">
<Ticket className="w-5 h-5" />
</div>
<h1 className="text-xl sm:text-2xl font-black text-white tracking-tight">
Cupones de Descuento & Promociones
</h1>
</div>
<p className="text-xs text-neutral-400 max-w-2xl leading-relaxed">
Administra campañas promocionales para redes sociales, WhatsApp y fechas especiales. Configura límites de
canje por cantidad de usos o fecha de vencimiento.
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchCoupons}
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 p-2.5 rounded-xl border border-neutral-700 transition-all cursor-pointer"
title="Actualizar listado"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin text-purple-400' : ''}`} />
</button>
<button
onClick={openCreateModal}
className="bg-gradient-to-r from-purple-600 via-fuchsia-600 to-pink-600 hover:opacity-95 text-white font-bold text-xs py-2.5 px-4 rounded-xl flex items-center gap-2 shadow-lg shadow-purple-600/30 transition-all cursor-pointer active:scale-95 shrink-0"
>
<Plus className="w-4 h-4" />
<span>+ Crear Nuevo Cupón</span>
</button>
</div>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-4 space-y-1 shadow-sm">
<div className="flex items-center justify-between text-neutral-400 text-xs">
<span>Total Cupones</span>
<Tag className="w-4 h-4 text-purple-400" />
</div>
<p className="text-2xl font-black text-white font-mono">{totalCoupons}</p>
<p className="text-[10px] text-neutral-500">Campañas configuradas</p>
</div>
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-4 space-y-1 shadow-sm">
<div className="flex items-center justify-between text-neutral-400 text-xs">
<span>Cupones Activos</span>
<Sparkles className="w-4 h-4 text-emerald-400" />
</div>
<p className="text-2xl font-black text-emerald-400 font-mono">{activeCoupons}</p>
<p className="text-[10px] text-emerald-500/80">Listos para canjear en la tienda</p>
</div>
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-4 space-y-1 shadow-sm">
<div className="flex items-center justify-between text-neutral-400 text-xs">
<span>Canjes Totales</span>
<TrendingUp className="w-4 h-4 text-fuchsia-400" />
</div>
<p className="text-2xl font-black text-fuchsia-400 font-mono">{totalRedemptions}</p>
<p className="text-[10px] text-neutral-500">Usos aplicados en pedidos</p>
</div>
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-4 space-y-1 shadow-sm">
<div className="flex items-center justify-between text-neutral-400 text-xs">
<span>Por Vencer (7 días)</span>
<Clock className="w-4 h-4 text-amber-400" />
</div>
<p className="text-2xl font-black text-amber-400 font-mono">{expiringSoonCount}</p>
<p className="text-[10px] text-neutral-500">Campañas próximas a caducar</p>
</div>
</div>
{/* Filters & Search */}
<div className="bg-neutral-900/70 border border-neutral-800 rounded-2xl p-4 flex flex-col md:flex-row gap-3 items-stretch md:items-center justify-between">
<div className="relative flex-1">
<Search className="w-4 h-4 text-neutral-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Buscar por código (ej. DRIPZONE10) o campaña..."
className="w-full bg-neutral-950 border border-neutral-800 focus:border-purple-500 rounded-xl pl-9 pr-4 py-2 text-xs text-white placeholder:text-neutral-500 outline-none transition-all"
/>
</div>
<div className="flex flex-wrap items-center gap-1.5 bg-neutral-950 p-1 rounded-xl border border-neutral-800 text-xs">
{[
{ id: 'all', label: 'Todos' },
{ id: 'active', label: 'Activos' },
{ id: 'inactive', label: 'Pausados' },
{ id: 'expired', label: 'Vencidos' },
{ id: 'exhausted', label: 'Agotados' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setStatusFilter(tab.id as any)}
className={`px-3 py-1.5 rounded-lg font-bold transition-all cursor-pointer ${
statusFilter === tab.id
? 'bg-purple-600 text-white shadow-sm'
: 'text-neutral-400 hover:text-white hover:bg-neutral-900'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Coupons List */}
{loading ? (
<div className="py-20 text-center space-y-3">
<div className="w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto" />
<p className="text-xs text-neutral-400">Cargando cupones de promoción...</p>
</div>
) : filteredCoupons.length === 0 ? (
<div className="bg-neutral-900/40 border border-dashed border-neutral-800 rounded-3xl p-12 text-center space-y-4">
<div className="w-14 h-14 rounded-2xl bg-neutral-800 text-neutral-500 flex items-center justify-center mx-auto">
<Ticket className="w-7 h-7" />
</div>
<div className="space-y-1">
<h3 className="font-bold text-white text-sm">No se encontraron cupones</h3>
<p className="text-xs text-neutral-500 max-w-sm mx-auto">
{searchTerm || statusFilter !== 'all'
? 'No hay cupones que coincidan con los filtros aplicados.'
: 'Aún no has creado campañas ni cupones de descuento. Crea el primero para tus promociones.'}
</p>
</div>
<button
onClick={openCreateModal}
className="bg-purple-600 hover:bg-purple-500 text-white text-xs font-bold py-2.5 px-4 rounded-xl transition-all cursor-pointer"
>
+ Crear Mi Primer Cupón
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredCoupons.map((coupon) => {
const expired = isCouponExpired(coupon);
const exhausted = isCouponExhausted(coupon);
const isFullyActive = coupon.isActive && !expired && !exhausted;
const usagePercent =
coupon.usageLimit && coupon.usageLimit > 0
? Math.min(100, Math.round(((coupon.usageCount || 0) / coupon.usageLimit) * 100))
: null;
return (
<div
key={coupon.id}
className={`bg-neutral-900 border rounded-3xl p-5 space-y-4 transition-all flex flex-col justify-between ${
!coupon.isActive
? 'border-neutral-800/80 opacity-70 bg-neutral-950/60'
: expired
? 'border-rose-900/30 bg-rose-950/10'
: exhausted
? 'border-amber-900/30 bg-amber-950/10'
: 'border-purple-500/30 hover:border-purple-500/60 shadow-lg shadow-purple-950/10'
}`}
>
<div className="space-y-3">
{/* Top Bar: Code and Status */}
<div className="flex items-start justify-between gap-2">
<div className="space-y-1">
<div className="flex items-center gap-1.5">
<span className="font-mono font-black text-lg text-white tracking-wider bg-neutral-950 border border-purple-500/30 px-2.5 py-0.5 rounded-xl shadow-xs">
{coupon.code}
</span>
<button
onClick={() => handleCopyCode(coupon.code)}
className="text-neutral-400 hover:text-purple-300 p-1 rounded-lg hover:bg-neutral-800 transition-colors cursor-pointer"
title="Copiar código"
>
{copiedCode === coupon.code ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
</button>
</div>
<p className="text-xs text-neutral-300 line-clamp-2">
{coupon.description || 'Promoción general de la tienda'}
</p>
</div>
{/* Status Pill */}
<span
className={`text-[10px] font-bold px-2 py-0.5 rounded-full shrink-0 uppercase tracking-wider border ${
!coupon.isActive
? 'bg-neutral-800 text-neutral-400 border-neutral-700'
: expired
? 'bg-rose-950/80 text-rose-300 border-rose-500/30'
: exhausted
? 'bg-amber-950/80 text-amber-300 border-amber-500/30'
: 'bg-emerald-950/80 text-emerald-300 border-emerald-500/30'
}`}
>
{!coupon.isActive ? 'Pausado' : expired ? 'Expirado' : exhausted ? 'Agotado' : 'Activo'}
</span>
</div>
{/* Discount Value Badge */}
<div className="bg-neutral-950/80 border border-neutral-800/80 rounded-2xl p-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center font-black text-xs">
{coupon.discountType === 'percentage' ? '%' : coupon.discountType === 'per_item' ? '📦' : '$'}
</div>
<div>
<p className="text-sm font-black text-white">
{coupon.discountType === 'percentage'
? `${coupon.discountValue}% de Descuento`
: coupon.discountType === 'per_item'
? `${formatCOP(coupon.discountValue)} / cada producto`
: `${formatCOP(coupon.discountValue)} de Descuento`}
</p>
<p className="text-[10px] text-neutral-400">
{coupon.discountType === 'per_item'
? `Descuenta ${formatCOP(coupon.discountValue)} por cada producto al saldo contraentrega`
: coupon.minSpend && coupon.minSpend > 0
? `Compra mín: ${formatCOP(coupon.minSpend)}`
: 'Sin monto mínimo'}
{coupon.maxDiscount && coupon.maxDiscount > 0
? ` • Tope: ${formatCOP(coupon.maxDiscount)}`
: ''}
</p>
</div>
</div>
</div>
{/* Quantity Limit Progress */}
<div className="space-y-1.5 pt-1">
<div className="flex justify-between items-center text-xs">
<span className="text-neutral-400 text-[11px] flex items-center gap-1">
<BarChart3 className="w-3 h-3 text-purple-400" />
<span>Límite de Canjes:</span>
</span>
<span className="font-mono font-bold text-white text-[11px]">
{coupon.usageLimit && coupon.usageLimit > 0 ? (
<>
{coupon.usageCount || 0} / {coupon.usageLimit}{' '}
<span className="text-neutral-400">({usagePercent}%)</span>
</>
) : (
<span>{coupon.usageCount || 0} canjes (Ilimitado)</span>
)}
</span>
</div>
{usagePercent !== null && (
<div className="w-full h-1.5 bg-neutral-950 rounded-full overflow-hidden border border-neutral-800">
<div
className={`h-full rounded-full transition-all ${
usagePercent >= 100
? 'bg-amber-500'
: usagePercent >= 75
? 'bg-purple-400'
: 'bg-emerald-500'
}`}
style={{ width: `${usagePercent}%` }}
/>
</div>
)}
</div>
{/* Expiration Details */}
<div className="flex items-center gap-1.5 text-[11px] pt-1 border-t border-neutral-800 text-neutral-400">
<Calendar className="w-3.5 h-3.5 text-purple-400 shrink-0" />
<span>
{coupon.expiresAt ? (
expired ? (
<span className="text-rose-400 font-bold">
Expiró el{' '}
{new Date(coupon.expiresAt).toLocaleDateString('es-CO', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
) : (
<span>
Vence el{' '}
<strong className="text-neutral-200">
{new Date(coupon.expiresAt).toLocaleDateString('es-CO', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</strong>
</span>
)
) : (
<span className="text-neutral-400 font-medium">Sin fecha de caducidad (Indefinido)</span>
)}
</span>
</div>
</div>
{/* Bottom Actions */}
<div className="pt-3 border-t border-neutral-800 flex items-center justify-between gap-2 mt-2">
<div className="flex items-center gap-1">
<button
onClick={() => handleToggleActive(coupon)}
className={`text-xs px-2.5 py-1.5 rounded-xl font-bold flex items-center gap-1 transition-all cursor-pointer ${
coupon.isActive
? 'bg-neutral-800 hover:bg-neutral-700 text-neutral-300'
: 'bg-emerald-950 text-emerald-300 border border-emerald-500/30'
}`}
title={coupon.isActive ? 'Pausar cupón' : 'Reanudar cupón'}
>
<Power className="w-3.5 h-3.5" />
<span>{coupon.isActive ? 'Pausar' : 'Activar'}</span>
</button>
<button
onClick={() => handleCopyLink(coupon.code)}
className="text-xs px-2.5 py-1.5 rounded-xl font-bold bg-neutral-800 hover:bg-neutral-700 text-neutral-300 flex items-center gap-1 transition-all cursor-pointer"
title="Copiar enlace directo con cupón aplicado"
>
{copiedLink === coupon.code ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Share2 className="w-3.5 h-3.5" />
)}
<span>{copiedLink === coupon.code ? '¡Copiado!' : 'Link'}</span>
</button>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => openEditModal(coupon)}
className="p-1.5 text-neutral-400 hover:text-white rounded-lg hover:bg-neutral-800 transition-colors cursor-pointer"
title="Editar cupón"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(coupon)}
className="p-1.5 text-neutral-400 hover:text-rose-400 rounded-lg hover:bg-neutral-800 transition-colors cursor-pointer"
title="Eliminar cupón"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
})}
</div>
)}
{/* Modal: Crear / Editar Cupón */}
{isModalOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 overflow-y-auto animate-in fade-in">
<div className="bg-neutral-900 border border-purple-500/30 rounded-3xl max-w-xl w-full p-6 text-white space-y-5 shadow-2xl my-8">
<div className="flex justify-between items-center border-b border-neutral-800 pb-3">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 text-purple-400 flex items-center justify-center">
<Tag className="w-4 h-4" />
</div>
<h3 className="text-base font-black text-white">
{editingCoupon ? `Editar Cupón: ${editingCoupon.code}` : 'Crear Nuevo Cupón de Descuento'}
</h3>
</div>
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="text-neutral-400 hover:text-white p-1 rounded-lg cursor-pointer"
>
✕
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4 text-xs">
{formError && (
<div className="bg-rose-950/40 border border-rose-500/30 text-rose-300 p-3 rounded-xl flex items-center gap-2">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{formError}</span>
</div>
)}
{/* Código y Nombre */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="space-y-1">
<label className="font-bold text-neutral-300 block">Código del Cupón *</label>
<input
type="text"
required
value={code}
onChange={(e) => setCode(e.target.value.toUpperCase().replace(/\s/g, ''))}
placeholder="Ej. LANZAMIENTO20"
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-3 py-2.5 font-mono font-bold tracking-wider text-white uppercase outline-none"
/>
<p className="text-[10px] text-neutral-500">Sin espacios, en mayúsculas.</p>
</div>
<div className="space-y-1">
<label className="font-bold text-neutral-300 block">Campaña / Descripción</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Ej. Promoción Black Friday Instagram"
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-3 py-2.5 text-white outline-none"
/>
</div>
</div>
{/* Tipo de Descuento */}
<div className="space-y-2">
<label className="font-bold text-neutral-300 block">Tipo de Descuento *</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button
type="button"
onClick={() => {
setDiscountType('percentage');
if (discountValue > 100) setDiscountValue(10);
}}
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-1.5 border transition-all cursor-pointer text-xs ${
discountType === 'percentage'
? 'bg-purple-600 text-white border-purple-400 shadow-md'
: 'bg-neutral-950 text-neutral-400 border-neutral-800 hover:text-white'
}`}
>
<Percent className="w-3.5 h-3.5" />
<span>Porcentaje (%)</span>
</button>
<button
type="button"
onClick={() => {
setDiscountType('fixed');
if (discountValue <= 100) setDiscountValue(20000);
}}
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-1.5 border transition-all cursor-pointer text-xs ${
discountType === 'fixed'
? 'bg-purple-600 text-white border-purple-400 shadow-md'
: 'bg-neutral-950 text-neutral-400 border-neutral-800 hover:text-white'
}`}
>
<DollarSign className="w-3.5 h-3.5" />
<span>Monto Fijo Global</span>
</button>
<button
type="button"
onClick={() => {
setDiscountType('per_item');
if (discountValue <= 100) setDiscountValue(10000);
}}
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-1.5 border transition-all cursor-pointer text-xs ${
discountType === 'per_item'
? 'bg-gradient-to-r from-emerald-600 to-teal-500 text-white border-emerald-400 shadow-md'
: 'bg-neutral-950 text-neutral-400 border-neutral-800 hover:text-white'
}`}
>
<span className="text-sm">📦</span>
<span>Por Cada Producto</span>
</button>
</div>
{discountType === 'per_item' && (
<div className="bg-emerald-950/30 border border-emerald-500/20 rounded-xl p-2.5 text-[11px] text-emerald-300 space-y-1">
<p className="font-bold flex items-center gap-1 text-emerald-400">
<span>⚡ Descuento Progresivo por Unidades:</span>
</p>
<p className="text-neutral-300 leading-relaxed">
Si el cliente compra <strong>1 par</strong> descuenta {formatCOP(discountValue || 10000)}, si compra <strong>2 pares</strong> descuenta {formatCOP((discountValue || 10000) * 2)}, si compra <strong>3 pares</strong> {formatCOP((discountValue || 10000) * 3)} y así sucesivamente.
</p>
<p className="text-emerald-400 font-semibold text-[10px]">
🛡️ El descuento se aplica 100% sobre el saldo pendiente por pagar (contraentrega), protegiendo intacto el anticipo de separación.
</p>
</div>
)}
</div>
{/* Valor del Descuento y Atajos */}
<div className="space-y-1.5">
<label className="font-bold text-neutral-300 block">
Valor a Descontar *{' '}
{discountType === 'percentage'
? '(1% al 100%)'
: discountType === 'per_item'
? '(Pesos COP por cada unidad/par comprado)'
: '(Pesos Colombianos COP)'}
</label>
<div className="relative">
<input
type="number"
min="1"
max={discountType === 'percentage' ? 100 : 999999999}
required
value={discountValue || ''}
onChange={(e) => setDiscountValue(Number(e.target.value))}
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-3 py-2.5 text-white font-mono font-bold text-sm outline-none"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 font-bold text-neutral-400">
{discountType === 'percentage' ? '%' : 'COP'}
</span>
</div>
{/* Quick Presets */}
<div className="flex flex-wrap items-center gap-1.5 pt-1">
<span className="text-[10px] text-neutral-500 font-semibold">Atajos:</span>
{discountType === 'percentage'
? [5, 10, 15, 20, 25, 30].map((v) => (
<button
key={v}
type="button"
onClick={() => setDiscountValue(v)}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] font-mono cursor-pointer transition-colors"
>
{v}%
</button>
))
: discountType === 'per_item'
? [5000, 10000, 15000, 20000, 25000, 30000].map((v) => (
<button
key={v}
type="button"
onClick={() => setDiscountValue(v)}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] font-mono cursor-pointer transition-colors"
>
{formatCOP(v)}/par
</button>
))
: [10000, 15000, 20000, 30000, 50000].map((v) => (
<button
key={v}
type="button"
onClick={() => setDiscountValue(v)}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] font-mono cursor-pointer transition-colors"
>
{formatCOP(v)}
</button>
))}
</div>
</div>
{/* CONDICIONES CLAVE: VENCIMIENTO Y CANTIDAD */}
<div className="bg-neutral-950 border border-purple-500/20 rounded-2xl p-4 space-y-4">
<h4 className="font-bold text-purple-300 flex items-center gap-1.5 text-xs">
<Clock className="w-3.5 h-3.5" />
<span>Control de Vigencia y Límite de Usos</span>
</h4>
{/* 1. TIEMPO DE CADUCIDAD */}
<div className="space-y-2 border-b border-neutral-800 pb-3">
<div className="flex items-center justify-between">
<div>
<label className="font-bold text-white block">Establecer Tiempo de Caducidad</label>
<p className="text-[10px] text-neutral-400">
El cupón dejará de funcionar automáticamente después de esta fecha.
</p>
</div>
<input
type="checkbox"
checked={hasExpiry}
onChange={(e) => setHasExpiry(e.target.checked)}
className="w-4 h-4 accent-purple-600 rounded cursor-pointer"
/>
</div>
{hasExpiry && (
<div className="space-y-2 pt-1">
<input
type="datetime-local"
required={hasExpiry}
value={expiresAt}
onChange={(e) => setExpiresAt(e.target.value)}
className="w-full bg-neutral-900 border border-neutral-700 focus:border-purple-500 rounded-xl px-3 py-2 text-white font-mono text-xs outline-none"
/>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-[10px] text-neutral-500 font-semibold">Vence en:</span>
<button
type="button"
onClick={() => handleSetExpiryPreset(2)}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] cursor-pointer"
>
+48 Horas
</button>
<button
type="button"
onClick={() => handleSetExpiryPreset(7)}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] cursor-pointer"
>
+7 Días
</button>
<button
type="button"
onClick={() => handleSetExpiryPreset(15)}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] cursor-pointer"
>
+15 Días
</button>
<button
type="button"
onClick={handleSetEndOfMonth}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] cursor-pointer"
>
Fin de Mes
</button>
</div>
</div>
)}
</div>
{/* 2. LÍMITE POR CANTIDAD DE USOS */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<label className="font-bold text-white block">Limitar por Cantidad de Canjes</label>
<p className="text-[10px] text-neutral-400">
Cantidad máxima de clientes o compras que pueden redimir este cupón.
</p>
</div>
<input
type="checkbox"
checked={hasUsageLimit}
onChange={(e) => setHasUsageLimit(e.target.checked)}
className="w-4 h-4 accent-purple-600 rounded cursor-pointer"
/>
</div>
{hasUsageLimit && (
<div className="space-y-2 pt-1">
<div className="relative">
<input
type="number"
min="1"
required={hasUsageLimit}
value={usageLimit || ''}
onChange={(e) => setUsageLimit(Number(e.target.value))}
placeholder="Ej. 50"
className="w-full bg-neutral-900 border border-neutral-700 focus:border-purple-500 rounded-xl px-3 py-2 text-white font-mono text-xs outline-none"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 font-semibold text-[11px]">
Canjes máximos
</span>
</div>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-[10px] text-neutral-500 font-semibold">Cupo:</span>
{[10, 25, 50, 100, 200].map((lim) => (
<button
key={lim}
type="button"
onClick={() => setUsageLimit(lim)}
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] font-mono cursor-pointer"
>
{lim} usos
</button>
))}
</div>
</div>
)}
</div>
</div>
{/* Condiciones Opcionales: Compra Mínima y Tope de Descuento */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
<div className="space-y-1">
<label className="font-bold text-neutral-300 block">Compra Mínima en Subtotal (COP)</label>
<input
type="number"
min="0"
step="5000"
value={minSpend || ''}
onChange={(e) => setMinSpend(Number(e.target.value))}
placeholder="0 = Sin mínimo"
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-3 py-2 text-white font-mono outline-none"
/>
<p className="text-[10px] text-neutral-500">Ej. $100.000 para pedidos mayores.</p>
</div>
{discountType === 'percentage' && (
<div className="space-y-1">
<label className="font-bold text-neutral-300 block">Descuento Máximo / Tope (COP)</label>
<input
type="number"
min="0"
step="5000"
value={maxDiscount || ''}
onChange={(e) => setMaxDiscount(Number(e.target.value))}
placeholder="0 = Sin tope"
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-3 py-2 text-white font-mono outline-none"
/>
<p className="text-[10px] text-neutral-500">Tope máximo en pesos para cuidar márgenes.</p>
</div>
)}
</div>
{/* Estado Activo */}
<div className="pt-2 flex items-center justify-between bg-neutral-950 p-3 rounded-xl border border-neutral-800">
<div>
<span className="font-bold text-white block">Estado del Cupón</span>
<span className="text-[10px] text-neutral-400">
{isActive ? 'El cupón estará activo y listo para ser redimido.' : 'El cupón estará pausado.'}
</span>
</div>
<input
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="w-5 h-5 accent-emerald-500 rounded cursor-pointer"
/>
</div>
{/* Submit Buttons */}
<div className="pt-3 flex gap-2 justify-end">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold rounded-xl transition-colors cursor-pointer"
>
Cancelar
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-5 py-2.5 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-bold rounded-xl shadow-lg shadow-purple-600/30 transition-all cursor-pointer disabled:opacity-50 flex items-center gap-1.5"
>
{isSubmitting ? (
<span>Guardando...</span>
) : editingCoupon ? (
<span>Actualizar Cupón</span>
) : (
<span>Crear Cupón</span>
)}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}