1- import { useState } from 'react'
2- import { Event } from '../../../types'
3- import { Box , Button , Card , Container , DialogContentText , Typography } from '@mui/material'
1+ import { useCallback , useState } from 'react'
2+ import { Event , Speaker } from '../../../types'
3+ import {
4+ Box ,
5+ Button ,
6+ Card ,
7+ Checkbox ,
8+ Container ,
9+ DialogContentText ,
10+ FormControlLabel ,
11+ List ,
12+ ListItem ,
13+ Typography ,
14+ } from '@mui/material'
415import { useSession } from '../../../services/hooks/useSession'
516import { useLocation , useRoute } from 'wouter'
617import { FirestoreQueryLoaderAndErrorDisplay } from '../../../components/FirestoreQueryLoaderAndErrorDisplay'
718import { ArrowBack } from '@mui/icons-material'
819import { getQueryParams } from '../../../utils/getQuerySearchParameters'
920import { EventSessionForm } from './EventSessionForm'
10- import { doc } from 'firebase/firestore'
21+ import { deleteDoc , doc , getDocs , query , where } from 'firebase/firestore'
1122import { collections } from '../../../services/firebase'
1223import { ConfirmDialog } from '../../../components/ConfirmDialog'
1324import {
1425 useFirestoreDocumentDeletion ,
1526 useFirestoreDocumentMutation ,
1627} from '../../../services/hooks/firestoreMutationHooks'
28+ import { useSpeakersMap } from '../../../services/hooks/useSpeakersMap'
1729
1830export type EventSessionProps = {
1931 event : Event
@@ -24,10 +36,49 @@ export const EventSession = ({ event }: EventSessionProps) => {
2436
2537 const sessionId = params ?. sessionId || ''
2638 const sessionResult = useSession ( event . id , sessionId )
39+ const speakersMap = useSpeakersMap ( event . id )
2740 const [ deleteOpen , setDeleteOpen ] = useState ( false )
41+ const [ orphanedSpeakers , setOrphanedSpeakers ] = useState < Speaker [ ] > ( [ ] )
42+ const [ orphanDeleteOpen , setOrphanDeleteOpen ] = useState ( false )
43+ const [ orphanDeleting , setOrphanDeleting ] = useState ( false )
44+ const [ selectedOrphanIds , setSelectedOrphanIds ] = useState < Set < string > > ( new Set ( ) )
2845 const documentDeletion = useFirestoreDocumentDeletion ( doc ( collections . sessions ( event . id ) , sessionId ) )
2946 const mutation = useFirestoreDocumentMutation ( doc ( collections . sessions ( event . id ) , sessionId ) )
3047
48+ const findOrphanedSpeakers = useCallback (
49+ async ( speakerIds : string [ ] ) => {
50+ const orphaned : Speaker [ ] = [ ]
51+ for ( const speakerId of speakerIds ) {
52+ const sessionsQuery = query (
53+ collections . sessions ( event . id ) ,
54+ where ( 'speakers' , 'array-contains' , speakerId )
55+ )
56+ const snapshot = await getDocs ( sessionsQuery )
57+ if ( snapshot . empty ) {
58+ const speaker = speakersMap . data ?. [ speakerId ]
59+ if ( speaker ) {
60+ orphaned . push ( speaker )
61+ }
62+ }
63+ }
64+ return orphaned
65+ } ,
66+ [ event . id , speakersMap . data ]
67+ )
68+
69+ const deleteSelectedOrphans = useCallback ( async ( ) => {
70+ setOrphanDeleting ( true )
71+ try {
72+ for ( const speakerId of selectedOrphanIds ) {
73+ await deleteDoc ( doc ( collections . speakers ( event . id ) , speakerId ) )
74+ }
75+ } finally {
76+ setOrphanDeleting ( false )
77+ setOrphanDeleteOpen ( false )
78+ setLocation ( '/sessions' )
79+ }
80+ } , [ event . id , selectedOrphanIds ] )
81+
3182 if ( sessionResult . isLoading || ! sessionResult . data ) {
3283 return < FirestoreQueryLoaderAndErrorDisplay hookResult = { sessionResult } />
3384 }
@@ -73,15 +124,68 @@ export const EventSession = ({ event }: EventSessionProps) => {
73124 cancelButton = "cancel"
74125 handleClose = { ( ) => setDeleteOpen ( false ) }
75126 handleAccept = { async ( ) => {
127+ const speakerIds = session . speakers || [ ]
76128 await documentDeletion . mutate ( )
77129 setDeleteOpen ( false )
130+
131+ if ( speakerIds . length > 0 ) {
132+ const orphaned = await findOrphanedSpeakers ( speakerIds )
133+ if ( orphaned . length > 0 ) {
134+ setOrphanedSpeakers ( orphaned )
135+ setSelectedOrphanIds ( new Set ( orphaned . map ( ( s ) => s . id ) ) )
136+ setOrphanDeleteOpen ( true )
137+ return
138+ }
139+ }
78140 setLocation ( '/sessions' )
79141 } } >
80142 < DialogContentText id = "alert-dialog-description" >
81143 { ' ' }
82144 Delete the session { session . title } from this event (not the session's speaker(s))
83145 </ DialogContentText >
84146 </ ConfirmDialog >
147+
148+ < ConfirmDialog
149+ open = { orphanDeleteOpen }
150+ title = "Delete orphaned speaker(s)?"
151+ acceptButton = "Delete selected speaker(s)"
152+ disabled = { orphanDeleting || selectedOrphanIds . size === 0 }
153+ loading = { orphanDeleting }
154+ cancelButton = "Keep all"
155+ handleClose = { ( ) => {
156+ setOrphanDeleteOpen ( false )
157+ setLocation ( '/sessions' )
158+ } }
159+ handleAccept = { deleteSelectedOrphans } >
160+ < DialogContentText >
161+ The following speaker(s) are not linked to any other session. Do you want to delete them?
162+ </ DialogContentText >
163+ < List dense >
164+ { orphanedSpeakers . map ( ( speaker ) => (
165+ < ListItem key = { speaker . id } disablePadding >
166+ < FormControlLabel
167+ control = {
168+ < Checkbox
169+ checked = { selectedOrphanIds . has ( speaker . id ) }
170+ onChange = { ( e ) => {
171+ setSelectedOrphanIds ( ( prev ) => {
172+ const next = new Set ( prev )
173+ if ( e . target . checked ) {
174+ next . add ( speaker . id )
175+ } else {
176+ next . delete ( speaker . id )
177+ }
178+ return next
179+ } )
180+ } }
181+ />
182+ }
183+ label = { speaker . name }
184+ />
185+ </ ListItem >
186+ ) ) }
187+ </ List >
188+ </ ConfirmDialog >
85189 </ Container >
86190 )
87191}
0 commit comments