diff --git a/apps/api/src/controllers/client-invitations.controller.ts b/apps/api/src/controllers/client-invitations.controller.ts index a5f7804..e185105 100644 --- a/apps/api/src/controllers/client-invitations.controller.ts +++ b/apps/api/src/controllers/client-invitations.controller.ts @@ -68,6 +68,24 @@ export async function registerFromInvitation(req: Request, res: Response, next: } } +export async function resendInvitation(req: Request, res: Response, next: NextFunction) { + try { + const isAdmin = await hasAnyPlatformRole(req.user!.userId, 'platform_admin'); + if (!isAdmin) { + return res.status(403).json({ message: 'No autorizado' }); + } + + const id = String(req.params.id); + const result = await clientInvitationService.resendInvitation( + id, + (req.user as any)?.nombre || 'Horux Despachos' + ); + res.json(result); + } catch (error: any) { + res.status(400).json({ message: error.message }); + } +} + export async function listInvitations(req: Request, res: Response, next: NextFunction) { try { const isAdmin = await hasAnyPlatformRole(req.user!.userId, 'platform_admin'); diff --git a/apps/api/src/routes/client-invitations.routes.ts b/apps/api/src/routes/client-invitations.routes.ts index c6a3ddd..aeaebe4 100644 --- a/apps/api/src/routes/client-invitations.routes.ts +++ b/apps/api/src/routes/client-invitations.routes.ts @@ -8,8 +8,9 @@ const router: Router = Router(); router.get('/validate/:token', controller.validateToken); router.post('/register/:token', controller.registerFromInvitation); -// Protegido: admin global crea y lista invitaciones +// Protegido: admin global crea, reenvia y lista invitaciones router.post('/', authenticate, controller.createInvitation); +router.post('/:id/resend', authenticate, controller.resendInvitation); router.get('/', authenticate, controller.listInvitations); export default router; diff --git a/apps/api/src/services/client-invitations.service.ts b/apps/api/src/services/client-invitations.service.ts index b6c314a..14de102 100644 --- a/apps/api/src/services/client-invitations.service.ts +++ b/apps/api/src/services/client-invitations.service.ts @@ -207,6 +207,46 @@ export async function registerFromInvitation( }; } +export async function resendInvitation(invitationId: string, invitedByName: string) { + const invitation = await prisma.clientInvitation.findUnique({ + where: { id: invitationId }, + }); + if (!invitation) { + throw new Error('Invitación no encontrada'); + } + if (invitation.status !== 'pending') { + throw new Error('Solo se pueden reenviar invitaciones pendientes'); + } + + const newToken = crypto.randomBytes(32).toString('hex'); + const newExpiresAt = new Date(Date.now() + INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000); + + await prisma.clientInvitation.update({ + where: { id: invitationId }, + data: { + token: newToken, + expiresAt: newExpiresAt, + sentAt: new Date(), + }, + }); + + const baseUrl = process.env.WEB_URL || 'https://horuxfin.com'; + const registerUrl = `${baseUrl}/invitacion/registro/${newToken}`; + + await emailService.sendClientInvitation(invitation.email, { + invitedByName, + registerUrl, + expiresAt: newExpiresAt.toLocaleDateString('es-MX', { + day: 'numeric', + month: 'long', + year: 'numeric', + }), + nombreDespacho: invitation.nombreDespacho, + }); + + return { message: 'Invitación reenviada' }; +} + export async function listInvitations() { return prisma.clientInvitation.findMany({ orderBy: { createdAt: 'desc' }, diff --git a/apps/web/app/(dashboard)/admin/invitar-cliente/page.tsx b/apps/web/app/(dashboard)/admin/invitar-cliente/page.tsx index 423fb27..5cbc049 100644 --- a/apps/web/app/(dashboard)/admin/invitar-cliente/page.tsx +++ b/apps/web/app/(dashboard)/admin/invitar-cliente/page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Header } from '@/components/layouts/header'; -import { useCreateInvitation, useClientInvitations } from '@/lib/hooks/use-client-invitations'; +import { useCreateInvitation, useClientInvitations, useResendInvitation } from '@/lib/hooks/use-client-invitations'; import { Button, Input, Card, CardContent, CardHeader, CardTitle } from '@horux/shared-ui'; import { format } from 'date-fns'; import { es } from 'date-fns/locale'; @@ -14,6 +14,7 @@ export default function InvitarClientePage() { const [message, setMessage] = useState<{ kind: 'ok' | 'error'; text: string } | null>(null); const createMut = useCreateInvitation(); + const resendMut = useResendInvitation(); const { data: invitations, isLoading } = useClientInvitations(); const handleSubmit = async (e: React.FormEvent) => { @@ -114,6 +115,7 @@ export default function InvitarClientePage() {