Added routes
This commit is contained in:
537
apps/web/src/features/groups/detail/GroupDetailProvider.tsx
Normal file
537
apps/web/src/features/groups/detail/GroupDetailProvider.tsx
Normal file
@@ -0,0 +1,537 @@
|
||||
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<void>
|
||||
copyWhatsappMessage: () => Promise<void>
|
||||
openWhatsapp: () => Promise<void>
|
||||
copyAbsenceNotifyUrl: (url: string) => void
|
||||
}
|
||||
|
||||
const GroupDetailContext = createContext<GroupDetailContextValue | null>(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<AttendeeDto | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<AddAttendeeTab>('quick')
|
||||
const [listSection, setListSection] = useState<ListSection>('members')
|
||||
const [attendeeToRemove, setAttendeeToRemove] = useState<AttendeeDto | null>(null)
|
||||
const [selectedWaitlistEntry, setSelectedWaitlistEntry] = useState<GroupWaitlistEntryDto | null>(null)
|
||||
const [isCapacityModalOpen, setIsCapacityModalOpen] = useState(false)
|
||||
const [pendingCapacityPayload, setPendingCapacityPayload] = useState<CreateAttendee | null>(null)
|
||||
const [isBulkCapacityModalOpen, setIsBulkCapacityModalOpen] = useState(false)
|
||||
const [searchFilter, setSearchFilter] = useState('')
|
||||
const [parsedContacts, setParsedContacts] = useState<ParsedContactRow[]>([])
|
||||
const [fileName, setFileName] = useState<string | null>(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<GroupWaitlistList>(['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<GroupWaitlistList>(['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<GroupWaitlistList>(['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 <GroupDetailContext.Provider value={value}>{children}</GroupDetailContext.Provider>
|
||||
}
|
||||
|
||||
export function useGroupDetail() {
|
||||
const context = useContext(GroupDetailContext)
|
||||
if (!context) {
|
||||
throw new Error('useGroupDetail debe usarse dentro de <GroupDetailProvider>')
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user