From d76c7e62008f4938afbe65052f7c1c9344d59f90 Mon Sep 17 00:00:00 2001 From: Jose Selesan Date: Fri, 18 Sep 2026 10:49:41 -0300 Subject: [PATCH] Improved Attendee list look and feel --- apps/web/src/routes/group-detail.tsx | 232 ++++++++++++++++++++------- 1 file changed, 176 insertions(+), 56 deletions(-) diff --git a/apps/web/src/routes/group-detail.tsx b/apps/web/src/routes/group-detail.tsx index 9345e2a..b4cba1e 100644 --- a/apps/web/src/routes/group-detail.tsx +++ b/apps/web/src/routes/group-detail.tsx @@ -1,19 +1,26 @@ -import { useState, useRef } from 'react'; +import { useState, useRef, type ReactNode } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate, Link } from '@tanstack/react-router'; +import type { AttendeeDto } from '@gruperly/shared'; import { ArrowLeft, CalendarClock, Check, + ChevronRight, + Clock, Copy, Download, FileSpreadsheet, Loader2, + Mail, MessageCircle, + Phone, Plus, RefreshCw, Search, Share2, + ShieldCheck, + StickyNote, Trash2, UploadCloud, UserPlus, @@ -46,6 +53,7 @@ export function GroupDetailView() { // Modals & Tabs const [isInviteModalOpen, setIsInviteModalOpen] = useState(false); const [isAddAttendeeModalOpen, setIsAddAttendeeModalOpen] = useState(false); + const [selectedAttendee, setSelectedAttendee] = useState(null); const [activeTab, setActiveTab] = useState<'quick' | 'bulk'>('quick'); // Quick form state @@ -100,7 +108,7 @@ export function GroupDetailView() { const createAttendeeMutation = useMutation({ mutationFn: (payload: CreateAttendee) => createAttendee(groupId, payload), onSuccess: (created) => { - toast.success(`Alumno ${created.fullName} agregado con éxito.`); + toast.success(`Miembro ${created.fullName} agregado con éxito.`); queryClient.invalidateQueries({ queryKey: ['group-attendees', groupId] }); setFirstName(''); setLastName(''); @@ -110,7 +118,7 @@ export function GroupDetailView() { setIsAddAttendeeModalOpen(false); }, onError: (err: Error) => { - toast.error(err.message || 'Error al agregar alumno.'); + toast.error(err.message || 'Error al agregar miembro.'); }, }); @@ -126,7 +134,7 @@ export function GroupDetailView() { setIsAddAttendeeModalOpen(false); }, onError: (err: Error) => { - toast.error(err.message || 'Error al importar alumnos.'); + toast.error(err.message || 'Error al importar miembros.'); }, }); @@ -212,7 +220,7 @@ export function GroupDetailView() { const handleConfirmBulkImport = () => { const validRows = parsedContacts.filter((c) => c.isValid); if (validRows.length === 0) { - toast.error('No hay alumnos válidos para importar.'); + toast.error('No hay miembros válidos para importar.'); return; } bulkImportMutation.mutate( @@ -326,7 +334,7 @@ export function GroupDetailView() {
-

Alumnos & Cupo

+

Miembros & Cupo

{attendees.length} inscritos {group.capacity ? `· Cupo de ${group.capacity}` : ''}

@@ -380,9 +388,9 @@ export function GroupDetailView() { {attendeesQuery.isSuccess && attendees.length === 0 ? (
-

Todavía no hay alumnos en este grupo

+

Todavía no hay miembros en este grupo

- Puedes compartir el enlace de invitación único o agregar alumnos de forma manual o masiva. + Puedes compartir el enlace de invitación único o agregar miembros de forma manual o masiva.

@@ -409,7 +417,7 @@ export function GroupDetailView() { {attendeesQuery.isSuccess && attendees.length > 0 && filteredAttendees.length === 0 ? (
- No se encontraron alumnos que coincidan con la búsqueda. + No se encontraron miembros que coincidan con la búsqueda.
) : null} @@ -418,54 +426,34 @@ export function GroupDetailView() { - - - - - + + + - {filteredAttendees.map((attendee) => { - const cleanPhone = attendee.phone?.replace(/\D/g, '') ?? ''; - return ( - - setSelectedAttendee(attendee)} + > + - - - - - - ); - })} + {attendee.fullName} + + + + + + ))}
AlumnoWhatsApp / TeléfonoEmailNotasIncorporadoNombreTeléfono
-
-
- {attendee.fullName.charAt(0).toUpperCase()} -
- {attendee.fullName} + {filteredAttendees.map((attendee) => ( +
+
+
+ {attendee.fullName.charAt(0).toUpperCase()}
-
- {attendee.phone ? ( - - - {attendee.phone} - - ) : ( - — - )} - - {attendee.email || —} - - {attendee.notes || —} - - {new Date(attendee.createdAt).toLocaleDateString('es-ES')} -
+ {attendee.phone || —} + + +
@@ -477,7 +465,7 @@ export function GroupDetailView() { isOpen={isInviteModalOpen} onClose={() => setIsInviteModalOpen(false)} title="Compartir Link de Invitación" - description="Envía este enlace a tus alumnos para que completen sus datos e ingresen directamente al grupo." + description="Envía este enlace a tus miembros para que completen sus datos e ingresen directamente al grupo." maxWidth="md" >
@@ -675,7 +663,7 @@ export function GroupDetailView() { ) : ( )} - Guardar Alumno + Guardar Miembro
@@ -847,6 +835,138 @@ export function GroupDetailView() { ) : null}
+ + {/* MODAL: DETALLE DEL PARTICIPANTE */} + setSelectedAttendee(null)} + title="Detalle del Participante" + maxWidth="sm" + > + {selectedAttendee ? ( +
+ {/* Header: avatar + name */} +
+
+ {selectedAttendee.fullName.charAt(0).toUpperCase()} +
+
+

+ {selectedAttendee.fullName} +

+

+ Incorporado el{' '} + {new Date(selectedAttendee.createdAt).toLocaleDateString('es-ES', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} +

+
+
+ + {/* Info rows */} +
+ } + label="Teléfono" + > + {selectedAttendee.phone ? ( +
+ {selectedAttendee.phone} + e.stopPropagation()} + > + + WhatsApp + +
+ ) : ( + Sin teléfono + )} +
+ + } + label="Email" + > + {selectedAttendee.email ? ( + e.stopPropagation()} + > + {selectedAttendee.email} + + ) : ( + Sin email + )} + + + {(selectedAttendee.guardianName || selectedAttendee.guardianPhone) ? ( + <> + } + label="Responsable" + > + + {selectedAttendee.guardianName || —} + + + {selectedAttendee.guardianPhone ? ( + } + label="Tel. Responsable" + > +
+ {selectedAttendee.guardianPhone} + e.stopPropagation()} + > + + +
+
+ ) : null} + + ) : null} + + } + label="Notas" + > + {selectedAttendee.notes ? ( + {selectedAttendee.notes} + ) : ( + Sin notas + )} + +
+
+ ) : null} +
); } + +function DetailRow({ icon, label, children }: { icon: ReactNode; label: string; children: ReactNode }) { + return ( +
+
{icon}
+
+

+ {label} +

+
{children}
+
+
+ ); +}