diff --git a/src/lib/api/adapters/crew.adapter.ts b/src/lib/api/adapters/crew.adapter.ts index 714b0201..21a54114 100644 --- a/src/lib/api/adapters/crew.adapter.ts +++ b/src/lib/api/adapters/crew.adapter.ts @@ -205,6 +205,26 @@ export class CrewAdapter extends BaseAdapter { return response.phantom_player } + /** + * Create multiple phantom players at once (officers only) + */ + async bulkCreatePhantoms( + crewId: string, + phantoms: CreatePhantomPlayerInput[], + options?: RequestOptions + ): Promise { + const response = await this.request<{ phantom_players: PhantomPlayer[] }>( + `/crews/${crewId}/phantom_players/bulk_create`, + { + ...options, + method: 'POST', + body: JSON.stringify({ phantom_players: phantoms }) + } + ) + this.clearCache('/crew/members') + return response.phantom_players + } + /** * Update a phantom player */ diff --git a/src/lib/api/mutations/crew.mutations.ts b/src/lib/api/mutations/crew.mutations.ts index 82ad25e1..899cc09f 100644 --- a/src/lib/api/mutations/crew.mutations.ts +++ b/src/lib/api/mutations/crew.mutations.ts @@ -183,6 +183,21 @@ export function useCreatePhantom() { })) } +/** + * Bulk create phantom players mutation + */ +export function useBulkCreatePhantoms() { + const queryClient = useQueryClient() + + return createMutation(() => ({ + mutationFn: ({ crewId, phantoms }: { crewId: string; phantoms: CreatePhantomPlayerInput[] }) => + crewAdapter.bulkCreatePhantoms(crewId, phantoms), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: crewKeys.membersAll() }) + } + })) +} + /** * Update phantom player mutation */ diff --git a/src/lib/components/crew/BulkPhantomModal.svelte b/src/lib/components/crew/BulkPhantomModal.svelte new file mode 100644 index 00000000..ca8691b2 --- /dev/null +++ b/src/lib/components/crew/BulkPhantomModal.svelte @@ -0,0 +1,263 @@ + + + + + + {#snippet children()} + + + +
+
+ Name + Granblue ID + Join Date + +
+ + {#each rows as row (row.id)} +
+ + + + +
+ {/each} +
+ + + + {#if bulkCreateMutation.isError} +

+ Failed to create phantom players. Please check the inputs and try again. +

+ {/if} +
+ + + {/snippet} +
+ + diff --git a/src/routes/(app)/crew/members/+page.svelte b/src/routes/(app)/crew/members/+page.svelte index d6605549..85706ac2 100644 --- a/src/routes/(app)/crew/members/+page.svelte +++ b/src/routes/(app)/crew/members/+page.svelte @@ -8,7 +8,6 @@ import { useRemoveMember, useUpdateMembership, - useCreatePhantom, useDeletePhantom } from '$lib/api/mutations/crew.mutations' import { crewAdapter } from '$lib/api/adapters/crew.adapter' @@ -19,9 +18,9 @@ import ModalHeader from '$lib/components/ui/ModalHeader.svelte' import ModalBody from '$lib/components/ui/ModalBody.svelte' import ModalFooter from '$lib/components/ui/ModalFooter.svelte' - import Input from '$lib/components/ui/Input.svelte' import CrewHeader from '$lib/components/crew/CrewHeader.svelte' import ScoutUserModal from '$lib/components/crew/ScoutUserModal.svelte' + import BulkPhantomModal from '$lib/components/crew/BulkPhantomModal.svelte' import { DropdownMenu as DropdownMenuBase } from 'bits-ui' import type { MemberFilter, CrewMembership, PhantomPlayer, CrewInvitation } from '$lib/types/api/crew' import type { PageData } from './$types' @@ -36,7 +35,7 @@ // Get filter from URL const filter = $derived( - ($page.url.searchParams.get('filter') as MemberFilter) || 'active' + ($page.url.searchParams.get('filter') as MemberFilter) || 'all' ) // Query for members based on filter @@ -76,21 +75,20 @@ // Mutations const removeMemberMutation = useRemoveMember() const updateMembershipMutation = useUpdateMembership() - const createPhantomMutation = useCreatePhantom() const deletePhantomMutation = useDeletePhantom() // Filter options const filterOptions: { value: MemberFilter; label: string }[] = [ + { value: 'all', label: 'All' }, { value: 'active', label: 'Active' }, { value: 'phantom', label: 'Phantoms' }, - { value: 'retired', label: 'Retired' }, - { value: 'all', label: 'All' } + { value: 'retired', label: 'Retired' } ] // Change filter function handleFilterChange(newFilter: MemberFilter) { const url = new URL($page.url) - if (newFilter === 'active') { + if (newFilter === 'all') { url.searchParams.delete('filter') } else { url.searchParams.set('filter', newFilter) @@ -116,11 +114,11 @@ let invitationsSectionOpen = $state(true) // Dialog state for phantom creation - let phantomDialogOpen = $state(false) - let phantomName = $state('') - let phantomGranblueId = $state('') - let phantomNotes = $state('') - let phantomJoinedAt = $state('') + let bulkPhantomDialogOpen = $state(false) + + // Dialog state for phantom deletion confirmation + let deletePhantomDialogOpen = $state(false) + let phantomToDelete = $state(null) // Role display helpers function getRoleLabel(role: string): string { @@ -241,45 +239,25 @@ editJoinDate = '' } - // Phantom actions - function openPhantomDialog() { - phantomName = '' - phantomGranblueId = '' - phantomNotes = '' - phantomJoinedAt = '' - phantomDialogOpen = true + function openDeletePhantomDialog(phantom: PhantomPlayer) { + phantomToDelete = phantom + deletePhantomDialogOpen = true } - async function handleCreatePhantom() { - if (!phantomName.trim() || !crewStore.crew) return - - try { - await createPhantomMutation.mutateAsync({ - crewId: crewStore.crew.id, - input: { - name: phantomName.trim(), - granblueId: phantomGranblueId.trim() || undefined, - notes: phantomNotes.trim() || undefined, - joinedAt: phantomJoinedAt || undefined - } - }) - phantomDialogOpen = false - } catch (error) { - console.error('Failed to create phantom:', error) - } - } - - async function handleDeletePhantom(phantom: PhantomPlayer) { - if (!crewStore.crew) return + async function handleConfirmDeletePhantom() { + if (!crewStore.crew || !phantomToDelete) return try { await deletePhantomMutation.mutateAsync({ crewId: crewStore.crew.id, - phantomId: phantom.id + phantomId: phantomToDelete.id }) } catch (error) { console.error('Failed to delete phantom:', error) } + + deletePhantomDialogOpen = false + phantomToDelete = null } // Format date @@ -337,8 +315,11 @@ {/if} @@ -563,69 +544,6 @@ {/snippet} - - - {#snippet children()} - - - - - - - (phantomDialogOpen = false)} - cancelDisabled={createPhantomMutation.isPending} - primaryAction={{ - label: createPhantomMutation.isPending ? 'Creating...' : 'Create', - onclick: handleCreatePhantom, - disabled: !phantomName.trim() || createPhantomMutation.isPending - }} - /> - {/snippet} - - {#snippet children()} @@ -657,9 +575,33 @@ {/snippet} + + + {#snippet children()} + + + +

+ Are you sure you want to delete {phantomToDelete?.name ?? 'this phantom player'}? This + action cannot be undone. +

+
+ + (deletePhantomDialogOpen = false)} + primaryAction={{ + label: 'Delete', + onclick: handleConfirmDeletePhantom, + destructive: true + }} + /> + {/snippet} +
+ {#if crewStore.crew?.id} + {/if}