822 lines
37 KiB
TypeScript
822 lines
37 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { Users, UserPlus, Shield, ShieldCheck, UserCheck, MessageCircle, Mail, Phone, MapPin, Eye, EyeOff, Trash2, Edit, Check, X, Lock, Key, AlertTriangle, Sparkles } from 'lucide-react';
|
||
import { User } from '@/types';
|
||
|
||
export default function AdminUsersPage() {
|
||
const [users, setUsers] = useState<User[]>([]);
|
||
const [search, setSearch] = useState('');
|
||
const [roleFilter, setRoleFilter] = useState<'all' | 'admin' | 'operator' | 'customer'>('all');
|
||
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||
|
||
// Modal para configurar Garantía de Envío / Anticipo
|
||
const [depositModalUser, setDepositModalUser] = useState<User | null>(null);
|
||
const [depositRequires, setDepositRequires] = useState<boolean>(false);
|
||
const [depositReasonInput, setDepositReasonInput] = useState<string>('');
|
||
|
||
// New user modal form
|
||
const [name, setName] = useState('');
|
||
const [email, setEmail] = useState('');
|
||
const [phone, setPhone] = useState('');
|
||
const [city, setCity] = useState('Cali');
|
||
const [address, setAddress] = useState('');
|
||
const [role, setRole] = useState<'admin' | 'operator' | 'customer'>('operator');
|
||
const [password, setPassword] = useState('');
|
||
|
||
// Edit user modal form
|
||
const [editName, setEditName] = useState('');
|
||
const [editEmail, setEditEmail] = useState('');
|
||
const [editPhone, setEditPhone] = useState('');
|
||
const [editCity, setEditCity] = useState('Cali');
|
||
const [editAddress, setEditAddress] = useState('');
|
||
const [editRole, setEditRole] = useState<'admin' | 'operator' | 'customer'>('operator');
|
||
const [editPassword, setEditPassword] = useState('');
|
||
|
||
useEffect(() => {
|
||
fetchUsers();
|
||
}, []);
|
||
|
||
const fetchUsers = async () => {
|
||
try {
|
||
const res = await fetch('/api/users');
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
setUsers(data.users);
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleToggleStatus = async (user: User) => {
|
||
const newStatus = !user.isActive;
|
||
setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, isActive: newStatus } : u)));
|
||
|
||
try {
|
||
await fetch('/api/users', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: user.id, updates: { isActive: newStatus } }),
|
||
});
|
||
setToastMessage(`Estado de ${user.name} actualizado a ${newStatus ? '🟢 ACTIVO' : '⚪ INACTIVO'}.`);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleChangeRole = async (user: User, newRole: 'admin' | 'operator' | 'customer') => {
|
||
setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, role: newRole } : u)));
|
||
|
||
try {
|
||
await fetch('/api/users', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: user.id, updates: { role: newRole } }),
|
||
});
|
||
setToastMessage(`Rol de ${user.name} actualizado a [${newRole.toUpperCase()}].`);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleDeleteUser = async (id: string, name: string) => {
|
||
if (!confirm(`¿Estás seguro de eliminar permanentemente al usuario ${name}?`)) return;
|
||
|
||
setUsers((prev) => prev.filter((u) => u.id !== id));
|
||
try {
|
||
await fetch(`/api/users?id=${id}`, { method: 'DELETE' });
|
||
setToastMessage(`🗑️ Usuario ${name} eliminado del sistema.`);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleOpenDepositModal = (user: User) => {
|
||
setDepositModalUser(user);
|
||
setDepositRequires(user.requiresShippingDeposit === true);
|
||
setDepositReasonInput(user.depositReason || '');
|
||
};
|
||
|
||
const handleSaveDepositSettings = async () => {
|
||
if (!depositModalUser) return;
|
||
|
||
const updates = {
|
||
requiresShippingDeposit: depositRequires,
|
||
depositReason: depositReasonInput.trim() || (depositRequires ? 'Garantía requerida por política administrativa' : 'Exonerado por administrador'),
|
||
};
|
||
|
||
setUsers((prev) =>
|
||
prev.map((u) => (u.id === depositModalUser.id ? { ...u, ...updates } : u))
|
||
);
|
||
|
||
try {
|
||
await fetch('/api/users', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: depositModalUser.id, updates }),
|
||
});
|
||
setToastMessage(
|
||
depositRequires
|
||
? `🔴 Garantía de flete ACTIVADA para "${depositModalUser.name}".`
|
||
: `🟢 Garantía de flete DESACTIVADA para "${depositModalUser.name}" (Pago 100% Contraentrega).`
|
||
);
|
||
setDepositModalUser(null);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleCreateUser = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
try {
|
||
const res = await fetch('/api/users', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, email, phone, city, address, role, password }),
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
setUsers((prev) => [data.user, ...prev]);
|
||
setIsModalOpen(false);
|
||
setName('');
|
||
setEmail('');
|
||
setPhone('');
|
||
setPassword('');
|
||
setToastMessage(`🎉 Usuario "${name}" registrado exitosamente.`);
|
||
} else {
|
||
alert(data.error || 'Error al crear usuario');
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleOpenEdit = (user: User) => {
|
||
setEditingUser(user);
|
||
setEditName(user.name);
|
||
setEditEmail(user.email);
|
||
setEditPhone(user.phone);
|
||
setEditCity(user.city || 'Cali');
|
||
setEditAddress(user.address || '');
|
||
setEditRole(user.role);
|
||
setEditPassword('');
|
||
};
|
||
|
||
const handleSaveEdit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!editingUser) return;
|
||
|
||
const updates: Partial<User> = {
|
||
name: editName,
|
||
email: editEmail,
|
||
phone: editPhone,
|
||
city: editCity,
|
||
address: editAddress,
|
||
role: editRole,
|
||
};
|
||
|
||
setUsers((prev) => prev.map((u) => (u.id === editingUser.id ? { ...u, ...updates } : u)));
|
||
|
||
try {
|
||
const res = await fetch('/api/users', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: editingUser.id, updates }),
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
setEditingUser(null);
|
||
setToastMessage(`✅ Datos de "${editName}" actualizados correctamente.`);
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleWhatsApp = (user: User) => {
|
||
const cleanPhone = user.phone.startsWith('57') ? user.phone : `57${user.phone}`;
|
||
const text = encodeURIComponent(`Hola ${user.name}! Te escribimos del equipo de DripZone 🟣`);
|
||
window.open(`https://wa.me/${cleanPhone}?text=${text}`, '_blank');
|
||
};
|
||
|
||
const filtered = users.filter((u) => {
|
||
const matchSearch =
|
||
u.name.toLowerCase().includes(search.toLowerCase()) ||
|
||
u.email.toLowerCase().includes(search.toLowerCase()) ||
|
||
u.phone.includes(search) ||
|
||
(u.city && u.city.toLowerCase().includes(search.toLowerCase()));
|
||
|
||
if (!matchSearch) return false;
|
||
if (roleFilter !== 'all' && u.role !== roleFilter) return false;
|
||
if (statusFilter === 'active' && !u.isActive) return false;
|
||
if (statusFilter === 'inactive' && u.isActive) return false;
|
||
return true;
|
||
});
|
||
|
||
const adminsCount = users.filter((u) => u.role === 'admin').length;
|
||
const operatorsCount = users.filter((u) => u.role === 'operator').length;
|
||
const customersCount = users.filter((u) => u.role === 'customer').length;
|
||
const depositsRequiredCount = users.filter((u) => u.requiresShippingDeposit === true).length;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 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">
|
||
<Users className="w-6 h-6 text-purple-400" />
|
||
<span>Control de Usuarios, Roles & Garantías ({users.length})</span>
|
||
</h1>
|
||
<p className="text-xs text-neutral-400 mt-1">
|
||
Administra accesos, roles de equipo y control inteligente de <strong>Garantías de Envío Anticipado</strong> para clientes.
|
||
</p>
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => setIsModalOpen(true)}
|
||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold text-xs px-4 py-2.5 rounded-xl flex items-center space-x-1.5 shadow-[0_0_15px_rgba(168,85,247,0.3)] cursor-pointer"
|
||
>
|
||
<UserPlus className="w-4 h-4" />
|
||
<span>+ Registrar Nuevo Usuario Admin</span>
|
||
</button>
|
||
</div>
|
||
|
||
{toastMessage && (
|
||
<div className="bg-purple-950/80 border border-purple-500 text-purple-300 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-md animate-in fade-in">
|
||
<span>{toastMessage}</span>
|
||
<button onClick={() => setToastMessage(null)} className="font-bold text-white cursor-pointer ml-3">
|
||
✕
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* KPI Stats Cards */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
|
||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center space-x-3 shadow-lg">
|
||
<div className="w-10 h-10 rounded-xl bg-purple-500/20 border border-purple-500/30 flex items-center justify-center text-purple-400">
|
||
<Shield className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<p className="text-[11px] text-neutral-400 font-semibold">Super Admins</p>
|
||
<p className="text-xl font-black text-white">{adminsCount}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center space-x-3 shadow-lg">
|
||
<div className="w-10 h-10 rounded-xl bg-fuchsia-500/20 border border-fuchsia-500/30 flex items-center justify-center text-fuchsia-400">
|
||
<ShieldCheck className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<p className="text-[11px] text-neutral-400 font-semibold">Operadores</p>
|
||
<p className="text-xl font-black text-white">{operatorsCount}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center space-x-3 shadow-lg">
|
||
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center text-emerald-400">
|
||
<UserCheck className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<p className="text-[11px] text-neutral-400 font-semibold">Clientes Totales</p>
|
||
<p className="text-xl font-black text-white">{customersCount}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center space-x-3 shadow-lg">
|
||
<div className="w-10 h-10 rounded-xl bg-amber-500/20 border border-amber-500/30 flex items-center justify-center text-amber-400">
|
||
<AlertTriangle className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<p className="text-[11px] text-neutral-400 font-semibold">Garantía Exigida</p>
|
||
<p className="text-xl font-black text-amber-300">{depositsRequiredCount}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Filters Toolbar */}
|
||
<div className="flex flex-col md:flex-row items-stretch md: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={() => setRoleFilter('all')}
|
||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||
roleFilter === 'all' ? 'bg-white text-neutral-950 shadow' : 'text-neutral-400 hover:text-white'
|
||
}`}
|
||
>
|
||
Todos ({users.length})
|
||
</button>
|
||
<button
|
||
onClick={() => setRoleFilter('admin')}
|
||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||
roleFilter === 'admin' ? 'bg-purple-500/30 text-purple-300 border border-purple-500/50 shadow' : 'text-neutral-400 hover:text-purple-300'
|
||
}`}
|
||
>
|
||
Admins ({adminsCount})
|
||
</button>
|
||
<button
|
||
onClick={() => setRoleFilter('operator')}
|
||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||
roleFilter === 'operator' ? 'bg-fuchsia-500/30 text-fuchsia-300 border border-fuchsia-500/50 shadow' : 'text-neutral-400 hover:text-fuchsia-300'
|
||
}`}
|
||
>
|
||
Operadores ({operatorsCount})
|
||
</button>
|
||
<button
|
||
onClick={() => setRoleFilter('customer')}
|
||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||
roleFilter === 'customer' ? 'bg-emerald-500/30 text-emerald-300 border border-emerald-500/50 shadow' : 'text-neutral-400 hover:text-emerald-300'
|
||
}`}
|
||
>
|
||
Clientes ({customersCount})
|
||
</button>
|
||
</div>
|
||
|
||
<div className="max-w-xs w-full">
|
||
<input
|
||
type="text"
|
||
placeholder="Buscar por nombre, correo o celular..."
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Users Table */}
|
||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl overflow-hidden shadow-xl">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left text-xs text-neutral-300">
|
||
<thead className="bg-neutral-950 text-neutral-400 font-bold uppercase text-[10px] border-b border-neutral-800">
|
||
<tr>
|
||
<th className="py-3.5 px-4">Estado</th>
|
||
<th className="py-3.5 px-4">Usuario</th>
|
||
<th className="py-3.5 px-4">Contacto Directo</th>
|
||
<th className="py-3.5 px-4">Garantía de Flete</th>
|
||
<th className="py-3.5 px-4">Ciudad / Dirección</th>
|
||
<th className="py-3.5 px-4">Rol & Permisos</th>
|
||
<th className="py-3.5 px-4 text-center">Acciones</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-neutral-800/60">
|
||
{filtered.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={7} className="text-center py-8 text-neutral-500">
|
||
No se encontraron usuarios con el filtro seleccionado.
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
filtered.map((user) => {
|
||
const isDepositRequired = user.requiresShippingDeposit === true;
|
||
return (
|
||
<tr
|
||
key={user.id}
|
||
className={`hover:bg-neutral-800/50 transition-colors ${
|
||
!user.isActive ? 'opacity-65 bg-neutral-950/40' : ''
|
||
}`}
|
||
>
|
||
{/* Active Status Toggle */}
|
||
<td className="py-3.5 px-4">
|
||
<button
|
||
onClick={() => handleToggleStatus(user)}
|
||
className={`text-[11px] font-bold px-2.5 py-1 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
|
||
user.isActive
|
||
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-300 hover:bg-emerald-500/30'
|
||
: 'bg-neutral-950 border-neutral-700 text-neutral-400 hover:text-white'
|
||
}`}
|
||
title="Activar o suspender acceso"
|
||
>
|
||
{user.isActive ? (
|
||
<>
|
||
<Eye className="w-3.5 h-3.5 text-emerald-400" />
|
||
<span>Activo</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<EyeOff className="w-3.5 h-3.5 text-neutral-400" />
|
||
<span>Inactivo</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
</td>
|
||
|
||
{/* User Avatar & Name */}
|
||
<td className="py-3.5 px-4 flex items-center space-x-3">
|
||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-purple-600 to-fuchsia-500 flex items-center justify-center text-white font-bold text-xs shadow-sm shrink-0">
|
||
{user.name.charAt(0).toUpperCase()}
|
||
</div>
|
||
<div>
|
||
<p className="font-bold text-white text-xs">{user.name}</p>
|
||
<span className="text-[10px] text-neutral-400">{user.email}</span>
|
||
</div>
|
||
</td>
|
||
|
||
{/* Contact */}
|
||
<td className="py-3.5 px-4">
|
||
<div className="space-y-1">
|
||
<button
|
||
onClick={() => handleWhatsApp(user)}
|
||
className="text-[11px] text-emerald-400 hover:text-emerald-300 font-bold flex items-center gap-1 cursor-pointer"
|
||
>
|
||
<MessageCircle className="w-3.5 h-3.5" />
|
||
<span>{user.phone}</span>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
|
||
{/* Shipping Guarantee Status (Dynamic Risk Policy) */}
|
||
<td className="py-3.5 px-4">
|
||
<button
|
||
onClick={() => handleOpenDepositModal(user)}
|
||
className={`text-[11px] font-bold px-2.5 py-1 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
|
||
isDepositRequired
|
||
? 'bg-amber-500/20 border-amber-500/50 text-amber-300 hover:bg-amber-500/30'
|
||
: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20'
|
||
}`}
|
||
title={user.depositReason || (isDepositRequired ? 'Garantía exigida' : 'Contraentrega libre')}
|
||
>
|
||
<ShieldCheck className="w-3.5 h-3.5" />
|
||
<span>{isDepositRequired ? 'Exigir Anticipo' : 'Libre (Contraentrega)'}</span>
|
||
{user.rejectedOrdersCount && user.rejectedOrdersCount > 0 ? (
|
||
<span className="bg-red-500/40 text-red-200 text-[9px] px-1.5 py-0.5 rounded-full font-black">
|
||
{user.rejectedOrdersCount} rechazos
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
{user.depositReason && (
|
||
<p className="text-[10px] text-neutral-400 truncate max-w-xs mt-0.5 italic">
|
||
{user.depositReason}
|
||
</p>
|
||
)}
|
||
</td>
|
||
|
||
{/* City */}
|
||
<td className="py-3.5 px-4">
|
||
<div className="text-[11px]">
|
||
<span className="text-white font-medium">{user.city || 'Cali'}</span>
|
||
{user.address && <p className="text-[10px] text-neutral-400 truncate max-w-xs">{user.address}</p>}
|
||
</div>
|
||
</td>
|
||
|
||
{/* Role Dropdown */}
|
||
<td className="py-3.5 px-4">
|
||
<select
|
||
value={user.role}
|
||
onChange={(e) => handleChangeRole(user, e.target.value as any)}
|
||
className={`text-[11px] font-bold px-2 py-1 rounded-lg border outline-hidden cursor-pointer ${
|
||
user.role === 'admin'
|
||
? 'bg-purple-500/20 border-purple-500/40 text-purple-300'
|
||
: user.role === 'operator'
|
||
? 'bg-fuchsia-500/20 border-fuchsia-500/40 text-fuchsia-300'
|
||
: 'bg-emerald-500/20 border-emerald-500/40 text-emerald-300'
|
||
}`}
|
||
>
|
||
<option value="admin" className="bg-neutral-900 text-white">👑 Admin Total</option>
|
||
<option value="operator" className="bg-neutral-900 text-white">📦 Operador Logística</option>
|
||
<option value="customer" className="bg-neutral-900 text-white">🛍️ Cliente Comprador</option>
|
||
</select>
|
||
</td>
|
||
|
||
{/* Actions (CRUD: Edit & Delete) */}
|
||
<td className="py-3.5 px-4 text-center">
|
||
<div className="flex items-center justify-center space-x-1.5">
|
||
<button
|
||
onClick={() => handleOpenEdit(user)}
|
||
className="text-neutral-400 hover:text-purple-300 p-1.5 rounded-lg hover:bg-purple-500/10 cursor-pointer transition-colors"
|
||
title="Editar datos del usuario"
|
||
>
|
||
<Edit className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteUser(user.id, user.name)}
|
||
className="text-neutral-400 hover:text-red-400 p-1.5 rounded-lg hover:bg-red-500/10 cursor-pointer transition-colors"
|
||
title="Eliminar usuario"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Modal: Configuración de Garantía de Envío */}
|
||
{depositModalUser && (
|
||
<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-md 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-bold text-white text-base flex items-center gap-2">
|
||
<ShieldCheck className="w-5 h-5 text-amber-400" />
|
||
<span>Garantía de Envío / Anticipo de Flete</span>
|
||
</h3>
|
||
<button onClick={() => setDepositModalUser(null)} className="text-neutral-400 hover:text-white cursor-pointer">
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="bg-neutral-950 p-3.5 rounded-2xl border border-neutral-800 space-y-1 text-xs">
|
||
<p className="font-bold text-white">{depositModalUser.name}</p>
|
||
<p className="text-neutral-400">{depositModalUser.phone} • {depositModalUser.email}</p>
|
||
{depositModalUser.rejectedOrdersCount && depositModalUser.rejectedOrdersCount > 0 ? (
|
||
<p className="text-red-400 font-bold text-[11px] pt-1">
|
||
⚠️ Historial: {depositModalUser.rejectedOrdersCount} pedido(s) rechazado(s) en entrega.
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="space-y-4 text-xs">
|
||
<div className="flex items-center justify-between bg-neutral-950 p-3.5 rounded-2xl border border-neutral-800">
|
||
<div>
|
||
<p className="font-bold text-white text-sm">Exigir Anticipo por Flete</p>
|
||
<p className="text-[11px] text-neutral-400">
|
||
Se le cobrará el flete de su ciudad como anticipo descontable al recibir.
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="checkbox"
|
||
checked={depositRequires}
|
||
onChange={(e) => setDepositRequires(e.target.checked)}
|
||
className="w-5 h-5 accent-purple-600 rounded cursor-pointer"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-neutral-300 font-bold mb-1">
|
||
Observación / Motivo Interno (Discreta)
|
||
</label>
|
||
<textarea
|
||
rows={3}
|
||
value={depositReasonInput}
|
||
onChange={(e) => setDepositReasonInput(e.target.value)}
|
||
placeholder="Ej. 1 pedido rechazado en portería / Exonerado por recomendación..."
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl p-3 text-xs text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
<p className="text-[10px] text-neutral-500 mt-1">
|
||
* Esta observación es para control interno del equipo administrativo.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-2 pt-3 border-t border-neutral-800">
|
||
<button
|
||
type="button"
|
||
onClick={() => setDepositModalUser(null)}
|
||
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold py-2 px-4 rounded-xl cursor-pointer text-xs"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleSaveDepositSettings}
|
||
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:opacity-90 text-white font-bold py-2 px-4 rounded-xl shadow-md cursor-pointer text-xs"
|
||
>
|
||
Guardar Configuración
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Modal Create User (POST) */}
|
||
{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-md 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-bold text-white text-base flex items-center gap-2">
|
||
<UserPlus className="w-5 h-5 text-purple-400" />
|
||
<span>Registrar Nuevo Usuario / Admin</span>
|
||
</h3>
|
||
<button onClick={() => setIsModalOpen(false)} className="text-neutral-400 hover:text-white cursor-pointer">
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleCreateUser} className="space-y-3 text-xs">
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Nombre Completo *</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
placeholder="Ej: Mateo Lasso"
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
|
||
<input
|
||
type="email"
|
||
required
|
||
placeholder="correo@ejemplo.com"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Celular / WhatsApp *</label>
|
||
<input
|
||
type="tel"
|
||
required
|
||
placeholder="3001234567"
|
||
value={phone}
|
||
onChange={(e) => setPhone(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Ciudad Principal</label>
|
||
<input
|
||
type="text"
|
||
placeholder="Cali / Medellín / Bogotá"
|
||
value={city}
|
||
onChange={(e) => setCity(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Contraseña Inicial</label>
|
||
<input
|
||
type="password"
|
||
placeholder="••••••••"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Dirección de Sede / Despachos</label>
|
||
<input
|
||
type="text"
|
||
placeholder="Calle 5 # 34-12"
|
||
value={address}
|
||
onChange={(e) => setAddress(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-purple-400 font-bold mb-1">Rol Asignado *</label>
|
||
<select
|
||
value={role}
|
||
onChange={(e) => setRole(e.target.value as any)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden font-bold"
|
||
>
|
||
<option value="admin">👑 Super Administrador (Acceso Total)</option>
|
||
<option value="operator">📦 Operador de Despachos & Logística</option>
|
||
<option value="customer">🛍️ Cliente Registrado</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-2 pt-3 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 px-4 rounded-xl cursor-pointer"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-2 px-4 rounded-xl shadow-md cursor-pointer"
|
||
>
|
||
Crear Usuario
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Modal Edit User (PUT) */}
|
||
{editingUser && (
|
||
<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-md 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-bold text-white text-base flex items-center gap-2">
|
||
<Edit className="w-5 h-5 text-purple-400" />
|
||
<span>Editar Usuario: {editingUser.name}</span>
|
||
</h3>
|
||
<button onClick={() => setEditingUser(null)} className="text-neutral-400 hover:text-white cursor-pointer">
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSaveEdit} className="space-y-3 text-xs">
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Nombre Completo *</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
value={editName}
|
||
onChange={(e) => setEditName(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
|
||
<input
|
||
type="email"
|
||
required
|
||
value={editEmail}
|
||
onChange={(e) => setEditEmail(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Celular / WhatsApp *</label>
|
||
<input
|
||
type="tel"
|
||
required
|
||
value={editPhone}
|
||
onChange={(e) => setEditPhone(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Ciudad Principal</label>
|
||
<input
|
||
type="text"
|
||
value={editCity}
|
||
onChange={(e) => setEditCity(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Nueva Contraseña</label>
|
||
<input
|
||
type="password"
|
||
placeholder="•••••••• (dejar vacio si no cambia)"
|
||
value={editPassword}
|
||
onChange={(e) => setEditPassword(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-neutral-400 font-bold mb-1">Dirección</label>
|
||
<input
|
||
type="text"
|
||
value={editAddress}
|
||
onChange={(e) => setEditAddress(e.target.value)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-purple-400 font-bold mb-1">Rol y Privilegios *</label>
|
||
<select
|
||
value={editRole}
|
||
onChange={(e) => setEditRole(e.target.value as any)}
|
||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden font-bold"
|
||
>
|
||
<option value="admin">👑 Super Administrador (Acceso Total)</option>
|
||
<option value="operator">📦 Operador de Despachos & Logística</option>
|
||
<option value="customer">🛍️ Cliente Registrado</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-2 pt-3 border-t border-neutral-800">
|
||
<button
|
||
type="button"
|
||
onClick={() => setEditingUser(null)}
|
||
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold py-2 px-4 rounded-xl cursor-pointer"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-2 px-4 rounded-xl shadow-md cursor-pointer"
|
||
>
|
||
Guardar Cambios
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|