feat: add reservation import script for Excel data and install xlsx dependency

This commit is contained in:
2026-05-08 17:03:39 +01:00
parent 5912955b9c
commit b3c374251e
6 changed files with 365 additions and 32 deletions

View File

@@ -13,6 +13,9 @@ interface UserProfileRow {
created_at: string;
}
// Toggle para reactivar el borrado de usuarios desde la UI.
const SHOW_DELETE_USER = false as boolean;
const ROLE_OPTIONS: { value: UserRole; label: string; description: string; icon: typeof Shield }[] = [
{ value: 'admin', label: 'Admin', description: 'Acceso total y gestión de usuarios', icon: Shield },
{ value: 'internal_staff', label: 'Staff', description: 'Gestión completa de reservas', icon: UsersIcon },
@@ -34,7 +37,8 @@ export function UserManagement() {
const [users, setUsers] = useState<UserProfileRow[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [inviteOpen, setInviteOpen] = useState(false);
// Invitación deshabilitada por ahora (sin SMTP). El modal y endpoint se conservan.
const [inviteOpen, _setInviteOpen] = useState(false);
const [savingId, setSavingId] = useState<string | null>(null);
const loadUsers = async () => {
@@ -84,6 +88,7 @@ export function UserManagement() {
}
};
// Borrado deshabilitado en UI; se conserva para reactivar.
const handleDelete = async (user: UserProfileRow) => {
if (!confirm(`¿Eliminar a ${user.email}? Esta acción no se puede deshacer.`)) return;
setSavingId(user.id);
@@ -104,19 +109,9 @@ export function UserManagement() {
return (
<div className="max-w-4xl mx-auto p-4 md:p-6">
<header className="mb-6 flex flex-col md:flex-row md:items-end justify-between gap-3">
<div>
<h1 className="text-3xl md:text-4xl font-black text-white tracking-tight mb-1">Usuarios</h1>
<p className="text-slate-400 text-sm">Gestiona accesos y roles del equipo.</p>
</div>
<button
type="button"
onClick={() => setInviteOpen(true)}
className="inline-flex items-center gap-2 px-5 py-3 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-2xl shadow-lg shadow-emerald-900/30 transition-all"
>
<UserPlus className="w-4 h-4" />
Invitar usuario
</button>
<header className="mb-6">
<h1 className="text-3xl md:text-4xl font-black text-white tracking-tight mb-1">Usuarios</h1>
<p className="text-slate-400 text-sm">Gestiona accesos y roles del equipo. Las altas se hacen desde Supabase.</p>
</header>
{error && (
@@ -138,13 +133,13 @@ export function UserManagement() {
<th className="px-4 py-3 text-left font-bold">Email / Nombre</th>
<th className="px-4 py-3 text-left font-bold">Rol</th>
<th className="px-4 py-3 text-left font-bold hidden md:table-cell">Creado</th>
<th className="px-4 py-3 text-right font-bold">Acciones</th>
{SHOW_DELETE_USER && <th className="px-4 py-3 text-right font-bold">Acciones</th>}
</tr>
</thead>
<tbody>
{users.length === 0 ? (
<tr>
<td colSpan={4} className="px-4 py-12 text-center text-slate-500">
<td colSpan={SHOW_DELETE_USER ? 4 : 3} className="px-4 py-12 text-center text-slate-500">
No hay usuarios registrados.
</td>
</tr>
@@ -171,17 +166,19 @@ export function UserManagement() {
<td className="px-4 py-3 text-slate-400 text-xs hidden md:table-cell">
{new Date(user.created_at).toLocaleDateString('es-ES')}
</td>
<td className="px-4 py-3 text-right">
<button
type="button"
disabled={savingId === user.id}
onClick={() => handleDelete(user)}
className="inline-flex p-2 text-slate-500 hover:text-red-400 transition-colors"
title="Eliminar"
>
{savingId === user.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
</button>
</td>
{SHOW_DELETE_USER && (
<td className="px-4 py-3 text-right">
<button
type="button"
disabled={savingId === user.id}
onClick={() => handleDelete(user)}
className="inline-flex p-2 text-slate-500 hover:text-red-400 transition-colors"
title="Eliminar"
>
{savingId === user.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
</button>
</td>
)}
</tr>
))}
</tbody>
@@ -192,10 +189,10 @@ export function UserManagement() {
<AnimatePresence>
{inviteOpen && (
<InviteModal
onClose={() => setInviteOpen(false)}
onClose={() => _setInviteOpen(false)}
onInvited={(newUser) => {
setUsers(prev => [newUser, ...prev]);
setInviteOpen(false);
_setInviteOpen(false);
toast.success('Invitación enviada');
}}
/>