diff --git a/apps/web/app/(dashboard)/admin/_components/invitaciones-trial-tab.tsx b/apps/web/app/(dashboard)/admin/_components/invitaciones-trial-tab.tsx new file mode 100644 index 0000000..17f4cc1 --- /dev/null +++ b/apps/web/app/(dashboard)/admin/_components/invitaciones-trial-tab.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Button, Input, Label, Card, CardContent, CardHeader, CardTitle, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@horux/shared-ui'; +import { getAllInvitations, createInvitation, cancelInvitation } from '@/lib/api/trial-invitations'; +import { getTenants } from '@/lib/api/tenants'; +import { Gift, X, Clock, CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'; + +interface TenantOption { + id: string; + nombre: string; + rfc: string; +} + +interface Invitation { + id: string; + tenantId: string; + plan: string; + durationDays: number; + status: string; + token: string; + sentAt: string; + expiresAt: string; + acceptedAt: string | null; + tenant: { + nombre: string; + rfc: string; + } | null; +} + +export default function InvitacionesTrialTab() { + const [tenants, setTenants] = useState([]); + const [invitations, setInvitations] = useState([]); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const [selectedTenantId, setSelectedTenantId] = useState(''); + const [durationDays, setDurationDays] = useState('30'); + const [plan, setPlan] = useState('business_control'); + const [message, setMessage] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null); + + useEffect(() => { + loadData(); + }, []); + + async function loadData() { + setLoading(true); + try { + const [tenantsData, invitationsData] = await Promise.all([ + getTenants(), + getAllInvitations(), + ]); + setTenants(tenantsData); + setInvitations(invitationsData); + } catch (err: any) { + setMessage({ kind: 'err', text: err?.response?.data?.message || 'Error al cargar datos' }); + } finally { + setLoading(false); + } + } + + async function handleCreate() { + if (!selectedTenantId || !durationDays) { + setMessage({ kind: 'err', text: 'Selecciona un despacho y duración' }); + return; + } + setCreating(true); + setMessage(null); + try { + await createInvitation({ + tenantId: selectedTenantId, + plan, + durationDays: parseInt(durationDays, 10), + }); + setMessage({ kind: 'ok', text: 'Invitación enviada correctamente' }); + setSelectedTenantId(''); + setDurationDays('30'); + loadData(); + } catch (err: any) { + setMessage({ kind: 'err', text: err?.response?.data?.message || 'Error al crear invitación' }); + } finally { + setCreating(false); + } + } + + async function handleCancel(id: string) { + if (!confirm('¿Seguro que quieres cancelar esta invitación?')) return; + try { + await cancelInvitation(id); + setMessage({ kind: 'ok', text: 'Invitación cancelada' }); + loadData(); + } catch (err: any) { + setMessage({ kind: 'err', text: err?.response?.data?.message || 'Error al cancelar' }); + } + } + + function statusIcon(status: string) { + switch (status) { + case 'pending': return ; + case 'accepted': return ; + case 'expired': return ; + case 'cancelled': return ; + default: return null; + } + } + + function statusLabel(status: string) { + switch (status) { + case 'pending': return 'Pendiente'; + case 'accepted': return 'Aceptada'; + case 'expired': return 'Expirada'; + case 'cancelled': return 'Cancelada'; + default: return status; + } + } + + return ( +
+ {/* Toast de resultado */} + {message && ( +
+ {message.text} +
+ )} + + {/* Formulario de creación */} + + + Nueva invitación + + +
+
+ + +
+
+ + +
+
+ + setDurationDays(e.target.value)} + /> +
+
+ +
+
+ + {/* Tabla de invitaciones */} + + + Historial de invitaciones + + + {loading ? ( +
+ +
+ ) : invitations.length === 0 ? ( +

No hay invitaciones enviadas

+ ) : ( +
+ + + + + + + + + + + + + + {invitations.map((inv) => ( + + + + + + + + + + ))} + +
DespachoPlanDíasEstadoEnviadoExpira
+
{inv.tenant?.nombre || '—'}
+
{inv.tenant?.rfc || '—'}
+
+ {inv.plan === 'business_control' ? 'Business Control' : inv.plan === 'business_cloud' ? 'Enterprise' : inv.plan} + {inv.durationDays} + + {statusIcon(inv.status)} + {statusLabel(inv.status)} + + + {new Date(inv.sentAt).toLocaleDateString('es-MX')} + + {new Date(inv.expiresAt).toLocaleDateString('es-MX')} + + {inv.status === 'pending' && ( + + )} +
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/app/(dashboard)/admin/usuarios/page.tsx b/apps/web/app/(dashboard)/admin/usuarios/page.tsx index a557adf..95f8693 100644 --- a/apps/web/app/(dashboard)/admin/usuarios/page.tsx +++ b/apps/web/app/(dashboard)/admin/usuarios/page.tsx @@ -2,13 +2,14 @@ import { useState, useEffect } from 'react'; import { DashboardShell } from '@/components/layouts/dashboard-shell'; -import { Card, CardContent, CardHeader, CardTitle, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@horux/shared-ui'; +import { Card, CardContent, CardHeader, CardTitle, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, TabsContent } from '@horux/shared-ui'; import { useAllUsuarios, useCreateUsuarioGlobal, useUpdateUsuarioGlobal, useDeleteUsuarioGlobal } from '@/lib/hooks/use-usuarios'; import { getTenants, type Tenant } from '@/lib/api/tenants'; import { useAuthStore } from '@/stores/auth-store'; import { isGlobalAdminRfc } from '@horux/shared'; import { Users, Pencil, Trash2, Shield, Eye, Calculator, Building2, X, Check, UserCog, UserCheck, User, Briefcase, Plus } from 'lucide-react'; import { cn } from '@horux/shared-ui'; +import InvitacionesTrialTab from '../_components/invitaciones-trial-tab'; // Mapa de roles + fallback defensivo. El fork despacho introduce roles // adicionales (cfo, supervisor, auxiliar, cliente) que no estaban en @@ -43,6 +44,7 @@ export default function AdminUsuariosPage() { const [editingUser, setEditingUser] = useState(null); const [filterTenant, setFilterTenant] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); + const [activeTab, setActiveTab] = useState('usuarios'); const [showCreateForm, setShowCreateForm] = useState(false); const [createFormData, setCreateFormData] = useState({ email: '', @@ -152,6 +154,13 @@ export default function AdminUsuariosPage() { return ( + + + Usuarios + Invitaciones Trial + + +
{/* Filtros */} @@ -425,6 +434,12 @@ export default function AdminUsuariosPage() { )) )}
+
+ + + + +
); } diff --git a/apps/web/components/layouts/sidebar-compact.tsx b/apps/web/components/layouts/sidebar-compact.tsx index 21ac020..73ecd94 100644 --- a/apps/web/components/layouts/sidebar-compact.tsx +++ b/apps/web/components/layouts/sidebar-compact.tsx @@ -22,6 +22,7 @@ import { ClipboardList, CreditCard, Gift, + CheckSquare2, UserCog, Shield, FileWarning, @@ -56,6 +57,7 @@ const navigation: NavItem[] = [ { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Usuarios', href: '/usuarios', icon: Users, roles: ['owner', 'cfo'] }, + { name: 'Tareas', href: '/tareas', icon: CheckSquare2, roles: ['owner', 'cfo', 'contador', 'auxiliar', 'supervisor'] }, { name: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] }, { name: 'Configuracion', href: '/configuracion', icon: Settings, roles: ['owner', 'cfo'] }, ]; @@ -64,7 +66,6 @@ const adminNavigation: NavItem[] = [ { name: 'Clientes', href: '/clientes', icon: Building2 }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Staff', href: '/admin/staff', icon: Shield }, - { name: 'Invitaciones Trial', href: '/admin/invitaciones-trial', icon: Gift }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning }, ]; diff --git a/apps/web/components/layouts/sidebar-floating.tsx b/apps/web/components/layouts/sidebar-floating.tsx index d04229c..b20435c 100644 --- a/apps/web/components/layouts/sidebar-floating.tsx +++ b/apps/web/components/layouts/sidebar-floating.tsx @@ -22,6 +22,7 @@ import { ClipboardList, CreditCard, Gift, + CheckSquare2, UserCog, Shield, FileWarning, @@ -55,6 +56,7 @@ const navigation: NavItem[] = [ { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Usuarios', href: '/usuarios', icon: Users, roles: ['owner', 'cfo'] }, + { name: 'Tareas', href: '/tareas', icon: CheckSquare2, roles: ['owner', 'cfo', 'contador', 'auxiliar', 'supervisor'] }, { name: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] }, { name: 'Configuracion', href: '/configuracion', icon: Settings, roles: ['owner', 'cfo'] }, ]; @@ -63,7 +65,6 @@ const adminNavigation: NavItem[] = [ { name: 'Clientes', href: '/clientes', icon: Building2 }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Staff', href: '/admin/staff', icon: Shield }, - { name: 'Invitaciones Trial', href: '/admin/invitaciones-trial', icon: Gift }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning }, ]; diff --git a/apps/web/components/layouts/sidebar.tsx b/apps/web/components/layouts/sidebar.tsx index 74424e9..734a692 100644 --- a/apps/web/components/layouts/sidebar.tsx +++ b/apps/web/components/layouts/sidebar.tsx @@ -27,6 +27,7 @@ import { ClipboardList, ListChecks, Gift, + CheckSquare2, } from 'lucide-react'; import { useAuthStore } from '@/stores/auth-store'; import { logout } from '@/lib/api/auth'; @@ -59,6 +60,7 @@ const navigation: NavItem[] = [ { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Usuarios', href: '/usuarios', icon: Users, roles: ['owner', 'cfo'] }, + { name: 'Tareas', href: '/tareas', icon: CheckSquare2, roles: ['owner', 'cfo', 'contador', 'auxiliar', 'supervisor'] }, { name: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] }, { name: 'Configuracion', href: '/configuracion', icon: Settings, roles: ['owner', 'cfo'] }, ]; @@ -67,7 +69,6 @@ const adminNavigation: NavItem[] = [ { name: 'Clientes', href: '/clientes', icon: Building2 }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Staff', href: '/admin/staff', icon: Shield }, - { name: 'Invitaciones Trial', href: '/admin/invitaciones-trial', icon: Gift }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning }, ]; diff --git a/apps/web/components/layouts/topnav.tsx b/apps/web/components/layouts/topnav.tsx index 625edef..66fdf5c 100644 --- a/apps/web/components/layouts/topnav.tsx +++ b/apps/web/components/layouts/topnav.tsx @@ -22,6 +22,7 @@ import { ClipboardList, CreditCard, Gift, + CheckSquare2, UserCog, Shield, FileWarning, @@ -56,6 +57,7 @@ const navigation: NavItem[] = [ { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Usuarios', href: '/usuarios', icon: Users, roles: ['owner', 'cfo'] }, + { name: 'Tareas', href: '/tareas', icon: CheckSquare2, roles: ['owner', 'cfo', 'contador', 'auxiliar', 'supervisor'] }, { name: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] }, { name: 'Configuracion', href: '/configuracion', icon: Settings, roles: ['owner', 'cfo'] }, ]; @@ -64,7 +66,6 @@ const adminNavigation: NavItem[] = [ { name: 'Clientes', href: '/clientes', icon: Building2 }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Staff', href: '/admin/staff', icon: Shield }, - { name: 'Invitaciones Trial', href: '/admin/invitaciones-trial', icon: Gift }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning }, ];