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

603 lines
26 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 {
Building2,
Plus,
Search,
CheckCircle2,
XCircle,
Phone,
MapPin,
MessageCircle,
Edit2,
Power,
Package,
ShieldCheck,
FileText,
User,
Info,
Layers,
Sparkles,
} from 'lucide-react';
import { Supplier, Product } from '@/types';
import { INITIAL_SUPPLIERS } from '@/lib/data-service';
import { renderClientTemplate, initClientTemplates } from '@/lib/templates-client';
export default function AdminSuppliersPage() {
const [suppliers, setSuppliers] = useState<Supplier[]>(INITIAL_SUPPLIERS);
const [products, setProducts] = useState<Product[]>([]);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
const [toast, setToast] = useState<string | null>(null);
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingSupplier, setEditingSupplier] = useState<Supplier | null>(null);
// Form fields
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [contactPerson, setContactPerson] = useState('');
const [address, setAddress] = useState('');
const [notes, setNotes] = useState('');
const [isActive, setIsActive] = useState(true);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
fetchSuppliers();
fetchProducts();
initClientTemplates();
}, []);
const fetchSuppliers = async () => {
try {
const res = await fetch('/api/suppliers?all=true');
const data = await res.json();
if (data.success && data.suppliers) {
setSuppliers(data.suppliers);
}
} catch (e) {
console.error('Error fetching suppliers:', e);
}
};
const fetchProducts = async () => {
try {
const res = await fetch('/api/products?all=true');
const data = await res.json();
if (data.success && data.products) {
setProducts(data.products);
}
} catch (e) {
console.error('Error fetching products:', e);
}
};
const handleOpenCreateModal = () => {
setEditingSupplier(null);
setName('');
setPhone('57');
setContactPerson('');
setAddress('');
setNotes('');
setIsActive(true);
setIsModalOpen(true);
};
const handleOpenEditModal = (supplier: Supplier) => {
setEditingSupplier(supplier);
setName(supplier.name);
setPhone(supplier.phone);
setContactPerson(supplier.contactPerson || '');
setAddress(supplier.address || '');
setNotes(supplier.notes || '');
setIsActive(supplier.isActive !== false);
setIsModalOpen(true);
};
const handleToggleActive = async (supplier: Supplier) => {
const nextStatus = !supplier.isActive;
// Optimistic update
setSuppliers((prev) =>
prev.map((s) => (s.id === supplier.id ? { ...s, isActive: nextStatus } : s))
);
try {
const res = await fetch('/api/suppliers', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: supplier.id,
updates: { isActive: nextStatus },
}),
});
const data = await res.json();
if (data.success) {
setToast(
nextStatus
? `🟢 Proveedor "${supplier.name}" ACTIVADO exitosamente.`
: `⚪ Proveedor "${supplier.name}" DESACTIVADO. Su información histórica y pedidos continúan 100% seguros.`
);
setTimeout(() => setToast(null), 4500);
}
} catch (e) {
console.error('Error toggling supplier active status:', e);
fetchSuppliers();
}
};
const handleSaveSupplier = async (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
const cleanPhone = phone.replace(/\D/g, '');
const finalPhone = cleanPhone.startsWith('57') ? cleanPhone : `57${cleanPhone}`;
try {
if (editingSupplier) {
// Edit existing
const res = await fetch('/api/suppliers', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: editingSupplier.id,
name,
phone: finalPhone,
contactPerson,
address,
notes,
isActive,
}),
});
const data = await res.json();
if (data.success) {
setToast(`✅ Proveedor "${name}" actualizado con éxito.`);
setTimeout(() => setToast(null), 4000);
setIsModalOpen(false);
fetchSuppliers();
}
} else {
// Create new
const res = await fetch('/api/suppliers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
phone: finalPhone,
contactPerson,
address,
notes,
isActive,
}),
});
const data = await res.json();
if (data.success) {
setToast(`✨ Proveedor "${name}" registrado correctamente.`);
setTimeout(() => setToast(null), 4000);
setIsModalOpen(false);
fetchSuppliers();
}
}
} catch (err) {
console.error(err);
} finally {
setIsSaving(false);
}
};
const handleOpenWhatsApp = (supplier: Supplier) => {
const cleanPhone = supplier.phone.startsWith('57') ? supplier.phone : `57${supplier.phone}`;
const rawText = renderClientTemplate('supplier_general_contact', {
proveedor_contacto: supplier.contactPerson || supplier.name,
proveedor_nombre: supplier.name,
tienda_nombre: 'DripZone',
});
const text = encodeURIComponent(rawText);
window.open(`https://wa.me/${cleanPhone}?text=${text}`, '_blank');
};
// Metrics
const activeSuppliers = suppliers.filter((s) => s.isActive !== false);
const inactiveSuppliers = suppliers.filter((s) => s.isActive === false);
const filteredSuppliers = suppliers.filter((s) => {
const matchesSearch =
s.name.toLowerCase().includes(search.toLowerCase()) ||
s.phone.includes(search) ||
(s.contactPerson && s.contactPerson.toLowerCase().includes(search.toLowerCase())) ||
(s.address && s.address.toLowerCase().includes(search.toLowerCase())) ||
(s.notes && s.notes.toLowerCase().includes(search.toLowerCase()));
if (!matchesSearch) return false;
if (statusFilter === 'active' && s.isActive === false) return false;
if (statusFilter === 'inactive' && s.isActive !== false) return false;
return true;
});
const getProductCountForSupplier = (supplierId: string, supplierName: string) => {
return products.filter(
(p) => p.supplierId === supplierId || (p.supplierName && p.supplierName.toLowerCase() === supplierName.toLowerCase())
).length;
};
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-black text-white flex items-center gap-2">
<Building2 className="w-6 h-6 text-purple-400" />
<span>Proveedores & Bodegas Aliadas ({suppliers.length})</span>
</h1>
<p className="text-xs text-neutral-400 mt-1">
Activa o desactiva proveedores en 1 clic <strong>sin perder el histórico de pedidos, productos ni trazabilidad logística</strong>.
</p>
</div>
<button
onClick={handleOpenCreateModal}
className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:from-purple-500 hover:to-fuchsia-500 text-white font-bold text-xs px-4 py-2.5 rounded-2xl flex items-center space-x-2 shadow-lg cursor-pointer transition-all active:scale-95 self-start sm:self-auto"
>
<Plus className="w-4 h-4" />
<span>Nuevo Proveedor</span>
</button>
</div>
{toast && (
<div className="bg-purple-950/90 border border-purple-500 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
<span>{toast}</span>
<button onClick={() => setToast(null)} className="font-bold text-white cursor-pointer ml-3">
✕
</button>
</div>
)}
{/* KPI Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
<div className="bg-neutral-900 border border-neutral-800 p-4 rounded-3xl space-y-1 shadow-lg">
<div className="flex items-center justify-between text-neutral-400 text-xs font-bold uppercase tracking-wider">
<span>Total Proveedores</span>
<Building2 className="w-4 h-4 text-purple-400" />
</div>
<p className="text-2xl font-black text-white">{suppliers.length}</p>
<p className="text-[10px] text-neutral-400">Bodegas y mayoristas registrados</p>
</div>
<div className="bg-neutral-900 border border-emerald-500/30 p-4 rounded-3xl space-y-1 shadow-lg">
<div className="flex items-center justify-between text-emerald-400 text-xs font-bold uppercase tracking-wider">
<span>Proveedores Activos</span>
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
</div>
<p className="text-2xl font-black text-emerald-300">{activeSuppliers.length}</p>
<p className="text-[10px] text-emerald-400/80">Disponibles para nuevas zapatillas y pedidos</p>
</div>
<div className="bg-neutral-900 border border-amber-500/30 p-4 rounded-3xl space-y-1 shadow-lg">
<div className="flex items-center justify-between text-amber-400 text-xs font-bold uppercase tracking-wider">
<span>Desactivados / Históricos</span>
<XCircle className="w-4 h-4 text-amber-400" />
</div>
<p className="text-2xl font-black text-amber-300">{inactiveSuppliers.length}</p>
<p className="text-[10px] text-neutral-400">Historial preservado 100% intacto</p>
</div>
<div className="bg-neutral-900 border border-neutral-800 p-4 rounded-3xl space-y-1 shadow-lg">
<div className="flex items-center justify-between text-neutral-400 text-xs font-bold uppercase tracking-wider">
<span>Zapatillas Vinculadas</span>
<Package className="w-4 h-4 text-fuchsia-400" />
</div>
<p className="text-2xl font-black text-fuchsia-300">{products.length}</p>
<p className="text-[10px] text-neutral-400">Modelos en catálogo</p>
</div>
</div>
{/* Info Banner About Historical Safety */}
<div className="bg-gradient-to-r from-neutral-900 via-neutral-900 to-purple-950/40 border border-purple-500/20 rounded-3xl p-4 flex items-start space-x-3 text-xs text-neutral-300 shadow-md">
<Info className="w-5 h-5 text-purple-400 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="font-bold text-white">
🛡️ Trazabilidad & Protección Histórica Garantizada
</p>
<p className="text-[11px] text-neutral-400">
Al desactivar un proveedor, <strong>ningún pedido pasado se altera ni se pierde</strong>. Los manifiestos de despacho, reportes financieros y costos mayoristas de pedidos anteriores conservan su información original. El proveedor simplemente se oculta para nuevas asignaciones de zapatillas.
</p>
</div>
</div>
{/* Filter and Search Bar */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
<div className="flex items-center space-x-1.5 bg-neutral-900 border border-neutral-800 p-1 rounded-2xl overflow-x-auto">
<button
onClick={() => setStatusFilter('all')}
className={`text-xs font-bold px-3.5 py-1.5 rounded-xl cursor-pointer transition-all ${
statusFilter === 'all' ? 'bg-white text-neutral-950 shadow' : 'text-neutral-400 hover:text-white'
}`}
>
Todos ({suppliers.length})
</button>
<button
onClick={() => setStatusFilter('active')}
className={`text-xs font-bold px-3.5 py-1.5 rounded-xl cursor-pointer transition-all flex items-center gap-1.5 ${
statusFilter === 'active'
? 'bg-emerald-500/30 border border-emerald-500/50 text-emerald-300 shadow'
: 'text-neutral-400 hover:text-emerald-400'
}`}
>
<span className="w-2 h-2 rounded-full bg-emerald-400" />
<span>Activos ({activeSuppliers.length})</span>
</button>
<button
onClick={() => setStatusFilter('inactive')}
className={`text-xs font-bold px-3.5 py-1.5 rounded-xl cursor-pointer transition-all flex items-center gap-1.5 ${
statusFilter === 'inactive'
? 'bg-amber-500/30 border border-amber-500/50 text-amber-300 shadow'
: 'text-neutral-400 hover:text-amber-400'
}`}
>
<span className="w-2 h-2 rounded-full bg-amber-400" />
<span>Inactivos ({inactiveSuppliers.length})</span>
</button>
</div>
<div className="max-w-xs w-full">
<div className="relative">
<input
type="text"
placeholder="Buscar proveedor, teléfono o bodega..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl pl-9 pr-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
/>
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
</div>
</div>
</div>
{/* Supplier Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredSuppliers.length === 0 ? (
<div className="col-span-full bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500 space-y-2">
<Building2 className="w-12 h-12 text-neutral-600 mx-auto" />
<p className="font-bold text-white text-sm">No se encontraron proveedores</p>
<p className="text-xs">Prueba con otro término de búsqueda o cambia los filtros.</p>
</div>
) : (
filteredSuppliers.map((supplier) => {
const isSupActive = supplier.isActive !== false;
const linkedProductsCount = getProductCountForSupplier(supplier.id, supplier.name);
return (
<div
key={supplier.id}
className={`bg-neutral-900 border rounded-3xl p-5 shadow-xl transition-all flex flex-col justify-between space-y-4 ${
isSupActive
? 'border-neutral-800 hover:border-purple-500/40'
: 'border-neutral-800/80 opacity-75 bg-neutral-950/60'
}`}
>
{/* Card Top: Status & Name */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span
className={`text-[10px] font-bold px-2.5 py-0.5 rounded-full border flex items-center gap-1 ${
isSupActive
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-300'
: 'bg-amber-500/20 border-amber-500/40 text-amber-300'
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${
isSupActive ? 'bg-emerald-400 animate-pulse' : 'bg-amber-400'
}`}
/>
<span>{isSupActive ? 'Activo para Pedidos' : 'Inactivo (Histórico Seguro)'}</span>
</span>
<button
onClick={() => handleToggleActive(supplier)}
className={`text-[11px] font-bold px-2.5 py-1 rounded-xl border flex items-center gap-1 cursor-pointer transition-all ${
isSupActive
? 'bg-red-500/10 border-red-500/30 text-red-400 hover:bg-red-500/20'
: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20'
}`}
title={isSupActive ? 'Desactivar proveedor (conserva historial)' : 'Reactivar proveedor'}
>
<Power className="w-3 h-3" />
<span>{isSupActive ? 'Desactivar' : 'Activar'}</span>
</button>
</div>
<h3 className="font-black text-white text-base leading-snug">{supplier.name}</h3>
{/* Details List */}
<div className="space-y-1.5 text-xs pt-1">
{supplier.contactPerson && (
<p className="text-neutral-300 flex items-center gap-1.5">
<User className="w-3.5 h-3.5 text-purple-400 shrink-0" />
<span>Contacto: <strong className="text-white">{supplier.contactPerson}</strong></span>
</p>
)}
<p className="text-neutral-300 flex items-center gap-1.5">
<Phone className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
<span className="font-mono text-emerald-300 font-semibold">{supplier.phone}</span>
</p>
{supplier.address && (
<p className="text-neutral-400 flex items-center gap-1.5 text-[11px]">
<MapPin className="w-3.5 h-3.5 text-pink-400 shrink-0" />
<span>{supplier.address}</span>
</p>
)}
{supplier.notes && (
<p className="text-neutral-400 italic text-[11px] bg-neutral-950 p-2 rounded-xl border border-neutral-800 mt-1">
"{supplier.notes}"
</p>
)}
</div>
</div>
{/* Card Bottom: Metrics & Actions */}
<div className="pt-3 border-t border-neutral-800 space-y-3">
<div className="flex items-center justify-between text-xs text-neutral-400">
<span className="flex items-center gap-1">
<Package className="w-3.5 h-3.5 text-fuchsia-400" />
<span>Modelos asignados:</span>
</span>
<strong className="text-white bg-neutral-950 px-2 py-0.5 rounded-lg border border-neutral-800">
{linkedProductsCount} pares
</strong>
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => handleOpenWhatsApp(supplier)}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs py-2 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all active:scale-95"
>
<MessageCircle className="w-3.5 h-3.5" />
<span>WhatsApp</span>
</button>
<button
onClick={() => handleOpenEditModal(supplier)}
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-200 font-bold text-xs py-2 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all active:scale-95"
>
<Edit2 className="w-3.5 h-3.5 text-purple-400" />
<span>Modificar</span>
</button>
</div>
</div>
</div>
);
})
)}
</div>
{/* Modal Crear / Editar Proveedor */}
{isModalOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in">
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-lg w-full p-6 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-black text-white text-base flex items-center gap-2">
<Building2 className="w-5 h-5 text-purple-400" />
<span>{editingSupplier ? 'Modificar Proveedor' : 'Registrar Nuevo Proveedor'}</span>
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-neutral-400 hover:text-white cursor-pointer"
>
✕
</button>
</div>
<form onSubmit={handleSaveSupplier} className="space-y-4 text-xs">
<div>
<label className="block text-neutral-400 font-bold mb-1">Nombre Comercial / Bodega *</label>
<input
type="text"
required
placeholder="Ej: Bodega Central Mayorista"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3.5 py-2.5 text-white outline-hidden focus:border-purple-400 font-semibold"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-emerald-400 font-bold mb-1">Celular / WhatsApp *</label>
<input
type="tel"
required
placeholder="Ej: 573XXXXXXXXX"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3.5 py-2.5 text-emerald-300 outline-hidden focus:border-emerald-400 font-mono font-bold"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Persona de Contacto</label>
<input
type="text"
placeholder="Ej: Asesor de Despachos"
value={contactPerson}
onChange={(e) => setContactPerson(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3.5 py-2.5 text-white outline-hidden focus:border-purple-400"
/>
</div>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Ubicación / Dirección de la Bodega</label>
<input
type="text"
placeholder="Ej: Centro Comercial Mayorista Local 102"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3.5 py-2.5 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Notas / Políticas de Garantía & Horarios</label>
<textarea
rows={3}
placeholder="Ej: Horario de despacho y recogida de 9:00 AM a 6:00 PM."
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3.5 py-2.5 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div className="bg-neutral-950 p-3 rounded-2xl border border-neutral-800 flex items-center justify-between">
<div>
<p className="font-bold text-white text-xs">Estado del Proveedor</p>
<p className="text-[10px] text-neutral-400">
{isActive
? '🟢 Activo: disponible para asignar nuevas zapatillas y recibir pedidos'
: '⚪ Desactivado: no aparecerá en nuevas publicaciones (el historial se mantiene)'}
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-neutral-800 peer-focus:outline-hidden rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-neutral-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-emerald-500"></div>
</label>
</div>
<div className="flex justify-end space-x-2 pt-2 border-t border-neutral-800">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold py-2.5 px-4 rounded-xl cursor-pointer"
>
Cancelar
</button>
<button
type="submit"
disabled={isSaving}
className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:from-purple-500 hover:to-fuchsia-500 text-white font-bold py-2.5 px-5 rounded-xl shadow-lg cursor-pointer transition-all disabled:opacity-50"
>
{isSaving ? 'Guardando...' : editingSupplier ? 'Actualizar Proveedor' : 'Guardar Proveedor'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}