import { createContext, useContext, useMemo, useState } from 'react' import type { ReactNode } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { AttendeeDto, CreateAttendee, CreateAttendeeResult, CreateGroupWaitlistEntry, GroupDto, GroupWaitlistEntryDto, GroupWaitlistList, GroupRiskLevel, } from '@gruperly/shared' import { useToast } from '../../../components/ui' import type { ParsedContactRow } from '../../../lib/file-parser' import { addToGroupWaitlist, ApiError, bulkCreateAttendees, createAttendee, getGroup, getGroupAttendees, getGroupsRiskOverview, getGroupWaitlist, getInviteToken, promoteGroupWaitlistEntry, removeGroupAttendee, removeGroupWaitlistEntry, } from '../../../lib/api' export type BulkAttendeeRow = { firstName: string lastName: string fullName: string phone: string email?: string notes?: string } export type ListSection = 'members' | 'waitlist' export type AddAttendeeTab = 'quick' | 'bulk' type GroupDetailContextValue = { groupId: string group: GroupDto | undefined groupQueryPending: boolean riskLevel: GroupRiskLevel attendees: AttendeeDto[] attendeesTotal: number attendeesPending: boolean waitlistEntries: GroupWaitlistEntryDto[] waitlistTotal: number waitlistPending: boolean firstWaitlistEntry: GroupWaitlistEntryDto | null hasFreeCapacity: boolean inviteUrl: string | undefined invitePending: boolean whatsappMessage: string searchFilter: string setSearchFilter: (value: string) => void listSection: ListSection setListSection: (value: ListSection) => void activeTab: AddAttendeeTab setActiveTab: (value: AddAttendeeTab) => void isInviteModalOpen: boolean openInviteModal: () => void closeInviteModal: () => void isAddAttendeeModalOpen: boolean openAddAttendeeModal: () => void closeAddAttendeeModal: () => void selectedAttendee: AttendeeDto | null setSelectedAttendee: (attendee: AttendeeDto | null) => void attendeeToRemove: AttendeeDto | null requestRemoveAttendee: (attendee: AttendeeDto) => void cancelRemoveAttendee: () => void confirmRemoveAttendee: (promoteFromWaitlist: boolean) => void selectedWaitlistEntry: GroupWaitlistEntryDto | null setSelectedWaitlistEntry: (entry: GroupWaitlistEntryDto | null) => void isCapacityModalOpen: boolean capacityPayload: CreateAttendee | null closeCapacityModal: () => void isBulkCapacityModalOpen: boolean closeBulkCapacityModal: () => void parsedContacts: ParsedContactRow[] setParsedContacts: (contacts: ParsedContactRow[]) => void removeParsedContact: (id: string) => void fileName: string | null setFileName: (name: string | null) => void isParsingFile: boolean setIsParsingFile: (value: boolean) => void isDragging: boolean setIsDragging: (value: boolean) => void validRowsCount: number invalidRowsCount: number isSubmittingAttendee: boolean isSubmittingForcedAttendee: boolean isSubmittingWaitlistEntry: boolean isSubmittingBulkImport: boolean isSubmittingRemoval: boolean isSubmittingPromotion: boolean isSubmittingWaitlistRemoval: boolean isRegeneratingInvite: boolean submitQuickAttendee: (payload: CreateAttendee) => void submitForcedAttendee: () => void submitWaitlistEntry: () => void requestBulkImport: () => void confirmBulkImportOverCapacity: () => void promoteEntry: (entry: GroupWaitlistEntryDto) => void removeEntry: (entry: GroupWaitlistEntryDto) => void regenerateInvite: () => void copyInviteLink: () => Promise copyWhatsappMessage: () => Promise openWhatsapp: () => Promise copyAbsenceNotifyUrl: (url: string) => void } const GroupDetailContext = createContext(null) export function GroupDetailProvider({ groupId, children }: { groupId: string; children: ReactNode }) { const queryClient = useQueryClient() const toast = useToast() const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) const [isAddAttendeeModalOpen, setIsAddAttendeeModalOpen] = useState(false) const [selectedAttendee, setSelectedAttendee] = useState(null) const [activeTab, setActiveTab] = useState('quick') const [listSection, setListSection] = useState('members') const [attendeeToRemove, setAttendeeToRemove] = useState(null) const [selectedWaitlistEntry, setSelectedWaitlistEntry] = useState(null) const [isCapacityModalOpen, setIsCapacityModalOpen] = useState(false) const [pendingCapacityPayload, setPendingCapacityPayload] = useState(null) const [isBulkCapacityModalOpen, setIsBulkCapacityModalOpen] = useState(false) const [searchFilter, setSearchFilter] = useState('') const [parsedContacts, setParsedContacts] = useState([]) const [fileName, setFileName] = useState(null) const [isParsingFile, setIsParsingFile] = useState(false) const [isDragging, setIsDragging] = useState(false) const groupQuery = useQuery({ queryKey: ['group', groupId], queryFn: () => getGroup(groupId), enabled: Boolean(groupId), }) const riskOverviewQuery = useQuery({ queryKey: ['groups-risk-overview'], queryFn: getGroupsRiskOverview, enabled: Boolean(groupId), }) const inviteTokenQuery = useQuery({ queryKey: ['invite-token', groupId], queryFn: () => getInviteToken(groupId), enabled: Boolean(groupId) && isInviteModalOpen, }) const attendeesQuery = useQuery({ queryKey: ['group-attendees', groupId], queryFn: () => getGroupAttendees(groupId, 1, 100), enabled: Boolean(groupId), }) const waitlistQuery = useQuery({ queryKey: ['group-waitlist', groupId], queryFn: () => getGroupWaitlist(groupId, 1, 100), enabled: Boolean(groupId), }) const group = groupQuery.data const attendees = useMemo(() => attendeesQuery.data?.data ?? [], [attendeesQuery.data]) const attendeesTotal = attendeesQuery.data?.pagination?.total ?? attendees.length const waitlistEntries = useMemo(() => waitlistQuery.data?.data ?? [], [waitlistQuery.data]) const waitlistTotal = waitlistQuery.data?.pagination?.total ?? waitlistEntries.length const firstWaitlistEntry = waitlistEntries[0] ?? null const hasFreeCapacity = group?.capacity == null || attendeesTotal < group.capacity const closeAddAttendeeFlow = () => { setPendingCapacityPayload(null) setIsCapacityModalOpen(false) setIsAddAttendeeModalOpen(false) } const invalidateGroupQueries = () => { void queryClient.invalidateQueries({ queryKey: ['group-attendees', groupId] }) void queryClient.invalidateQueries({ queryKey: ['group-waitlist', groupId] }) void queryClient.invalidateQueries({ queryKey: ['group', groupId] }) } const handleCreateSuccess = (result: CreateAttendeeResult) => { if (result.outcome === 'created') { toast.success(`Miembro ${result.attendee.fullName} agregado con éxito.`) } else { toast.info(result.message) } void queryClient.invalidateQueries({ queryKey: ['group-attendees', groupId] }) closeAddAttendeeFlow() } const regenerateTokenMutation = useMutation({ mutationFn: () => getInviteToken(groupId, true), onSuccess: (data) => { queryClient.setQueryData(['invite-token', groupId], data) toast.success('Se generó un nuevo enlace de invitación.') }, onError: () => { toast.error('No se pudo regenerar el enlace de invitación.') }, }) const createAttendeeMutation = useMutation({ mutationFn: (payload: CreateAttendee) => createAttendee(groupId, payload), onMutate: (payload) => setPendingCapacityPayload(payload), onSuccess: handleCreateSuccess, onError: (err: Error) => { if (err instanceof ApiError && err.problem?.code === 'group_capacity_reached') { setIsCapacityModalOpen(true) return } toast.error(err.message || 'Error al agregar miembro.') }, }) const createAttendeeForceMutation = useMutation({ mutationFn: (payload: CreateAttendee) => createAttendee(groupId, payload, { allowOverflow: true }), onSuccess: handleCreateSuccess, onError: (err: Error) => { toast.error(err.message || 'Error al agregar miembro.') }, }) const addToWaitlistMutation = useMutation({ mutationFn: (payload: CreateGroupWaitlistEntry) => addToGroupWaitlist(groupId, payload), onMutate: async (payload: CreateGroupWaitlistEntry) => { await queryClient.cancelQueries({ queryKey: ['group-waitlist', groupId] }) const previousWaitlist = queryClient.getQueryData(['group-waitlist', groupId]) if (previousWaitlist) { const optimisticEntry: GroupWaitlistEntryDto = { id: `temp-${Date.now()}`, groupId, fullName: `${payload.firstName} ${payload.lastName ?? ''}`.trim(), phone: payload.phone, email: payload.email || null, notes: payload.notes ?? null, status: 'PENDING', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), } queryClient.setQueryData(['group-waitlist', groupId], { data: [optimisticEntry, ...previousWaitlist.data], pagination: { ...previousWaitlist.pagination, total: previousWaitlist.pagination.total + 1, }, }) } return { previousWaitlist } }, onSuccess: (entry) => { toast.info(`${entry.fullName} fue agregado a la lista de espera del grupo.`) invalidateGroupQueries() closeAddAttendeeFlow() }, onError: (err: Error, _payload, context) => { if (context?.previousWaitlist) { queryClient.setQueryData(['group-waitlist', groupId], context.previousWaitlist) } if (err instanceof ApiError && err.problem?.code === 'already_waitlisted') { toast.info('Este número ya está en la lista de espera del grupo.') } else { toast.error(err.message || 'Error al agregar a la lista de espera.') } setPendingCapacityPayload(null) setIsCapacityModalOpen(false) }, }) const bulkImportMutation = useMutation({ mutationFn: (rows: BulkAttendeeRow[]) => bulkCreateAttendees(groupId, { attendees: rows }), onSuccess: (res) => { toast.success(res.message) setParsedContacts([]) setFileName(null) void queryClient.invalidateQueries({ queryKey: ['group-attendees', groupId] }) setIsAddAttendeeModalOpen(false) }, onError: (err: Error) => { toast.error(err.message || 'Error al importar miembros.') }, }) const removeAttendeeMutation = useMutation({ mutationFn: (payload: { attendeeId: string; promoteFromWaitlist: boolean }) => removeGroupAttendee(groupId, payload.attendeeId, { promoteFromWaitlist: payload.promoteFromWaitlist }), onSuccess: (result, payload) => { const removedName = attendees.find((a) => a.id === payload.attendeeId)?.fullName ?? 'El miembro' if (result.promoted) { toast.success( `${removedName} fue quitado del grupo y ${result.promoted.fullName} pasó de la lista de espera al grupo.`, ) } else { toast.success(`${removedName} fue quitado del grupo.`) } invalidateGroupQueries() setSelectedAttendee(null) setAttendeeToRemove(null) }, onError: (err: Error) => { if (err instanceof ApiError && err.problem?.code === 'attendee_has_payments') { toast.error(err.problem.detail ?? 'Este miembro tiene cobros asociados.') } else { toast.error(err.message || 'Error al quitar al miembro del grupo.') } setAttendeeToRemove(null) }, }) const promoteWaitlistMutation = useMutation({ mutationFn: (entryId: string) => promoteGroupWaitlistEntry(groupId, entryId), onSuccess: (result) => { toast.success(`${result.attendee.fullName} pasó de la lista de espera al grupo.`) invalidateGroupQueries() }, onError: (err: Error) => { if (err instanceof ApiError && err.problem?.code === 'group_capacity_reached') { toast.error('El grupo alcanzó su cupo. Quita un miembro o aumenta el cupo primero.') } else { toast.error(err.message || 'No se pudo pasar al miembro al grupo.') } }, }) const removeWaitlistEntryMutation = useMutation({ mutationFn: (entry: GroupWaitlistEntryDto) => removeGroupWaitlistEntry(groupId, entry.id), onSuccess: (_result, entry) => { toast.success(`${entry.fullName} fue quitado de la lista de espera.`) invalidateGroupQueries() }, onError: (err: Error) => { toast.error(err.message || 'No se pudo quitar de la lista de espera.') }, }) const inviteUrl = inviteTokenQuery.data?.inviteUrl const whatsappMessage = group && inviteUrl ? `¡Hola! 👋 Te invito a unirte al grupo *${group.name}*.\n\nCompleta tus datos de inscripción en el siguiente enlace:\n${inviteUrl}` : '' const whatsappShareUrl = whatsappMessage ? `https://wa.me/?text=${encodeURIComponent(whatsappMessage)}` : '' const copyInviteLink = async () => { if (!inviteUrl) return try { await navigator.clipboard.writeText(inviteUrl) toast.success('¡Enlace de invitación copiado al portapapeles!') } catch { toast.error('No se pudo copiar automáticamente. Copia el texto manualmente.') } } const copyWhatsappMessage = async () => { if (!whatsappMessage) return try { await navigator.clipboard.writeText(whatsappMessage) toast.success('¡Mensaje copiado al portapapeles!') } catch { toast.error('No se pudo copiar automáticamente. Copia el texto manualmente.') } } const openWhatsapp = async () => { if (!whatsappShareUrl) return try { if (whatsappMessage) { await navigator.clipboard.writeText(whatsappMessage) toast.success('¡Abriendo WhatsApp! Mensaje copiado al portapapeles.') } } catch { // Si el portapapeles falla, igual abrimos WhatsApp con el mensaje. } window.open(whatsappShareUrl, '_blank', 'noopener,noreferrer') } const copyAbsenceNotifyUrl = (url: string) => { void navigator.clipboard.writeText(url).then(() => { toast.success('Link copiado al portapapeles.') }) } const validRows: BulkAttendeeRow[] = useMemo( () => parsedContacts .filter((contact) => contact.isValid) .map((contact) => ({ firstName: contact.firstName, lastName: contact.lastName, fullName: contact.fullName, phone: contact.phone, email: contact.email || undefined, notes: contact.notes || undefined, })), [parsedContacts], ) const runBulkImport = () => { if (validRows.length === 0) { toast.error('No hay miembros válidos para importar.') return } bulkImportMutation.mutate(validRows) } const requestBulkImport = () => { if (validRows.length === 0) { toast.error('No hay miembros válidos para importar.') return } if (group?.capacity != null && attendeesTotal + validRows.length > group.capacity) { setIsBulkCapacityModalOpen(true) return } runBulkImport() } const value: GroupDetailContextValue = { groupId, group, groupQueryPending: groupQuery.isPending, riskLevel: riskOverviewQuery.data?.items.find((item) => item.groupId === groupId)?.riskLevel ?? 'NONE', attendees, attendeesTotal, attendeesPending: attendeesQuery.isPending, waitlistEntries, waitlistTotal, waitlistPending: waitlistQuery.isPending, firstWaitlistEntry, hasFreeCapacity, inviteUrl, invitePending: inviteTokenQuery.isPending, whatsappMessage, searchFilter, setSearchFilter, listSection, setListSection, activeTab, setActiveTab, isInviteModalOpen, openInviteModal: () => setIsInviteModalOpen(true), closeInviteModal: () => setIsInviteModalOpen(false), isAddAttendeeModalOpen, openAddAttendeeModal: () => setIsAddAttendeeModalOpen(true), closeAddAttendeeModal: () => setIsAddAttendeeModalOpen(false), selectedAttendee, setSelectedAttendee, attendeeToRemove, requestRemoveAttendee: (attendee) => setAttendeeToRemove(attendee), cancelRemoveAttendee: () => setAttendeeToRemove(null), confirmRemoveAttendee: (promoteFromWaitlist) => { if (!attendeeToRemove) return removeAttendeeMutation.mutate({ attendeeId: attendeeToRemove.id, promoteFromWaitlist, }) }, selectedWaitlistEntry, setSelectedWaitlistEntry, isCapacityModalOpen, capacityPayload: pendingCapacityPayload, closeCapacityModal: () => { setPendingCapacityPayload(null) setIsCapacityModalOpen(false) }, isBulkCapacityModalOpen, closeBulkCapacityModal: () => setIsBulkCapacityModalOpen(false), parsedContacts, setParsedContacts, removeParsedContact: (id) => setParsedContacts((prev) => prev.filter((c) => c.id !== id)), fileName, setFileName, isParsingFile, setIsParsingFile, isDragging, setIsDragging, validRowsCount: validRows.length, invalidRowsCount: parsedContacts.length - validRows.length, isSubmittingAttendee: createAttendeeMutation.isPending, isSubmittingForcedAttendee: createAttendeeForceMutation.isPending, isSubmittingWaitlistEntry: addToWaitlistMutation.isPending, isSubmittingBulkImport: bulkImportMutation.isPending, isSubmittingRemoval: removeAttendeeMutation.isPending, isSubmittingPromotion: promoteWaitlistMutation.isPending, isSubmittingWaitlistRemoval: removeWaitlistEntryMutation.isPending, isRegeneratingInvite: regenerateTokenMutation.isPending, submitQuickAttendee: (payload) => createAttendeeMutation.mutate(payload), submitForcedAttendee: () => { if (pendingCapacityPayload) { createAttendeeForceMutation.mutate({ ...pendingCapacityPayload }) } }, submitWaitlistEntry: () => { if (pendingCapacityPayload) { addToWaitlistMutation.mutate({ ...pendingCapacityPayload }) } }, requestBulkImport, confirmBulkImportOverCapacity: () => { setIsBulkCapacityModalOpen(false) runBulkImport() }, promoteEntry: (entry) => { setSelectedWaitlistEntry(null) promoteWaitlistMutation.mutate(entry.id) }, removeEntry: (entry) => { setSelectedWaitlistEntry(null) removeWaitlistEntryMutation.mutate(entry) }, regenerateInvite: () => regenerateTokenMutation.mutate(), copyInviteLink, copyWhatsappMessage, openWhatsapp, copyAbsenceNotifyUrl, } return {children} } export function useGroupDetail() { const context = useContext(GroupDetailContext) if (!context) { throw new Error('useGroupDetail debe usarse dentro de ') } return context }