feat: invitaciones trial como pestaña en admin usuarios + sidebar

- Quitado Invitaciones Trial del sidebar (4 layouts)
- Agregado tab Invitaciones Trial dentro de /admin/usuarios
- Componente reutilizable invitaciones-trial-tab.tsx
- Agregada nueva opcion Tareas en el sidebar principal
This commit is contained in:
Horux Dev
2026-05-23 23:41:58 +00:00
parent bba000d308
commit be96ecc324
6 changed files with 276 additions and 5 deletions

View File

@@ -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<TenantOption[]>([]);
const [invitations, setInvitations] = useState<Invitation[]>([]);
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 <Clock className="h-4 w-4 text-amber-500" />;
case 'accepted': return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case 'expired': return <AlertTriangle className="h-4 w-4 text-red-500" />;
case 'cancelled': return <X className="h-4 w-4 text-gray-500" />;
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 (
<div className="space-y-6">
{/* Toast de resultado */}
{message && (
<div
className={`max-w-3xl rounded-lg px-4 py-3 text-sm ${
message.kind === 'ok'
? 'bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 text-green-800 dark:text-green-300'
: 'bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-300'
}`}
>
{message.text}
</div>
)}
{/* Formulario de creación */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Nueva invitación</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Despacho</Label>
<Select value={selectedTenantId} onValueChange={setSelectedTenantId}>
<SelectTrigger>
<SelectValue placeholder="Selecciona un despacho" />
</SelectTrigger>
<SelectContent>
{tenants.map((t) => (
<SelectItem key={t.id} value={t.id}>
{t.nombre} ({t.rfc})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Plan</Label>
<Select value={plan} onValueChange={setPlan}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="business_control">Business Control</SelectItem>
<SelectItem value="business_cloud">Enterprise</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Duración (días)</Label>
<Input
type="number"
min={1}
max={365}
value={durationDays}
onChange={(e) => setDurationDays(e.target.value)}
/>
</div>
</div>
<Button onClick={handleCreate} disabled={creating}>
{creating ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Gift className="h-4 w-4 mr-2" />}
Enviar invitación
</Button>
</CardContent>
</Card>
{/* Tabla de invitaciones */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Historial de invitaciones</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
</div>
) : invitations.length === 0 ? (
<p className="text-muted-foreground text-center py-8">No hay invitaciones enviadas</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-3">Despacho</th>
<th className="text-left py-2 px-3">Plan</th>
<th className="text-left py-2 px-3">Días</th>
<th className="text-left py-2 px-3">Estado</th>
<th className="text-left py-2 px-3">Enviado</th>
<th className="text-left py-2 px-3">Expira</th>
<th className="text-left py-2 px-3"></th>
</tr>
</thead>
<tbody>
{invitations.map((inv) => (
<tr key={inv.id} className="border-b hover:bg-muted/50">
<td className="py-2 px-3">
<div className="font-medium">{inv.tenant?.nombre || '—'}</div>
<div className="text-xs text-muted-foreground">{inv.tenant?.rfc || '—'}</div>
</td>
<td className="py-2 px-3">
{inv.plan === 'business_control' ? 'Business Control' : inv.plan === 'business_cloud' ? 'Enterprise' : inv.plan}
</td>
<td className="py-2 px-3">{inv.durationDays}</td>
<td className="py-2 px-3">
<span className="flex items-center gap-1">
{statusIcon(inv.status)}
{statusLabel(inv.status)}
</span>
</td>
<td className="py-2 px-3">
{new Date(inv.sentAt).toLocaleDateString('es-MX')}
</td>
<td className="py-2 px-3">
{new Date(inv.expiresAt).toLocaleDateString('es-MX')}
</td>
<td className="py-2 px-3">
{inv.status === 'pending' && (
<button
onClick={() => handleCancel(inv.id)}
className="text-destructive hover:underline text-xs"
>
Cancelar
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -2,13 +2,14 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { DashboardShell } from '@/components/layouts/dashboard-shell'; 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 { useAllUsuarios, useCreateUsuarioGlobal, useUpdateUsuarioGlobal, useDeleteUsuarioGlobal } from '@/lib/hooks/use-usuarios';
import { getTenants, type Tenant } from '@/lib/api/tenants'; import { getTenants, type Tenant } from '@/lib/api/tenants';
import { useAuthStore } from '@/stores/auth-store'; import { useAuthStore } from '@/stores/auth-store';
import { isGlobalAdminRfc } from '@horux/shared'; import { isGlobalAdminRfc } from '@horux/shared';
import { Users, Pencil, Trash2, Shield, Eye, Calculator, Building2, X, Check, UserCog, UserCheck, User, Briefcase, Plus } from 'lucide-react'; 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 { cn } from '@horux/shared-ui';
import InvitacionesTrialTab from '../_components/invitaciones-trial-tab';
// Mapa de roles + fallback defensivo. El fork despacho introduce roles // Mapa de roles + fallback defensivo. El fork despacho introduce roles
// adicionales (cfo, supervisor, auxiliar, cliente) que no estaban en // adicionales (cfo, supervisor, auxiliar, cliente) que no estaban en
@@ -43,6 +44,7 @@ export default function AdminUsuariosPage() {
const [editingUser, setEditingUser] = useState<EditingUser | null>(null); const [editingUser, setEditingUser] = useState<EditingUser | null>(null);
const [filterTenant, setFilterTenant] = useState<string>('all'); const [filterTenant, setFilterTenant] = useState<string>('all');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [activeTab, setActiveTab] = useState('usuarios');
const [showCreateForm, setShowCreateForm] = useState(false); const [showCreateForm, setShowCreateForm] = useState(false);
const [createFormData, setCreateFormData] = useState({ const [createFormData, setCreateFormData] = useState({
email: '', email: '',
@@ -152,6 +154,13 @@ export default function AdminUsuariosPage() {
return ( return (
<DashboardShell title="Administracion de Usuarios"> <DashboardShell title="Administracion de Usuarios">
<Tabs value={activeTab} onValueChange={setActiveTab} defaultValue="usuarios" className="space-y-4">
<TabsList>
<TabsTrigger value="usuarios">Usuarios</TabsTrigger>
<TabsTrigger value="invitaciones-trial">Invitaciones Trial</TabsTrigger>
</TabsList>
<TabsContent value="usuarios">
<div className="space-y-4"> <div className="space-y-4">
{/* Filtros */} {/* Filtros */}
<Card> <Card>
@@ -425,6 +434,12 @@ export default function AdminUsuariosPage() {
)) ))
)} )}
</div> </div>
</TabsContent>
<TabsContent value="invitaciones-trial">
<InvitacionesTrialTab />
</TabsContent>
</Tabs>
</DashboardShell> </DashboardShell>
); );
} }

View File

@@ -22,6 +22,7 @@ import {
ClipboardList, ClipboardList,
CreditCard, CreditCard,
Gift, Gift,
CheckSquare2,
UserCog, UserCog,
Shield, Shield,
FileWarning, FileWarning,
@@ -56,6 +57,7 @@ const navigation: NavItem[] = [
{ name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] },
{ name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] },
{ name: 'Usuarios', href: '/usuarios', icon: Users, 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: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] },
{ name: 'Configuracion', href: '/configuracion', icon: Settings, 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: 'Clientes', href: '/clientes', icon: Building2 },
{ name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog },
{ name: 'Staff', href: '/admin/staff', icon: Shield }, { 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 }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning },
]; ];

View File

@@ -22,6 +22,7 @@ import {
ClipboardList, ClipboardList,
CreditCard, CreditCard,
Gift, Gift,
CheckSquare2,
UserCog, UserCog,
Shield, Shield,
FileWarning, FileWarning,
@@ -55,6 +56,7 @@ const navigation: NavItem[] = [
{ name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] },
{ name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] },
{ name: 'Usuarios', href: '/usuarios', icon: Users, 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: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] },
{ name: 'Configuracion', href: '/configuracion', icon: Settings, 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: 'Clientes', href: '/clientes', icon: Building2 },
{ name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog },
{ name: 'Staff', href: '/admin/staff', icon: Shield }, { 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 }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning },
]; ];

View File

@@ -27,6 +27,7 @@ import {
ClipboardList, ClipboardList,
ListChecks, ListChecks,
Gift, Gift,
CheckSquare2,
} from 'lucide-react'; } from 'lucide-react';
import { useAuthStore } from '@/stores/auth-store'; import { useAuthStore } from '@/stores/auth-store';
import { logout } from '@/lib/api/auth'; import { logout } from '@/lib/api/auth';
@@ -59,6 +60,7 @@ const navigation: NavItem[] = [
{ name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] },
{ name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] },
{ name: 'Usuarios', href: '/usuarios', icon: Users, 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: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] },
{ name: 'Configuracion', href: '/configuracion', icon: Settings, 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: 'Clientes', href: '/clientes', icon: Building2 },
{ name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog },
{ name: 'Staff', href: '/admin/staff', icon: Shield }, { 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 }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning },
]; ];

View File

@@ -22,6 +22,7 @@ import {
ClipboardList, ClipboardList,
CreditCard, CreditCard,
Gift, Gift,
CheckSquare2,
UserCog, UserCog,
Shield, Shield,
FileWarning, FileWarning,
@@ -56,6 +57,7 @@ const navigation: NavItem[] = [
{ name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] }, { name: 'Carteras', href: '/carteras', icon: ClipboardList, roles: ['supervisor', 'auxiliar'] },
{ name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] }, { name: 'Contribuyentes', href: '/contribuyentes', icon: Building2, roles: ['owner', 'cfo'] },
{ name: 'Usuarios', href: '/usuarios', icon: Users, 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: 'Planes', href: '/configuracion/planes-despacho', icon: CreditCard, roles: ['owner', 'cfo'] },
{ name: 'Configuracion', href: '/configuracion', icon: Settings, 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: 'Clientes', href: '/clientes', icon: Building2 },
{ name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog }, { name: 'Admin Usuarios', href: '/admin/usuarios', icon: UserCog },
{ name: 'Staff', href: '/admin/staff', icon: Shield }, { 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 }, { name: 'Audit Log', href: '/admin/audit-log', icon: FileWarning },
]; ];