diff --git a/src/lib/types/api/crew.ts b/src/lib/types/api/crew.ts index c64411dc..5e798a1e 100644 --- a/src/lib/types/api/crew.ts +++ b/src/lib/types/api/crew.ts @@ -10,7 +10,7 @@ export type CrewRole = 'member' | 'vice_captain' | 'captain' export type InvitationStatus = 'pending' | 'accepted' | 'rejected' | 'expired' // Member filter options for GET /crew/members -export type MemberFilter = 'active' | 'retired' | 'phantom' | 'all' +export type MemberFilter = 'active' | 'retired' | 'phantom' | 'pending' | 'all' // Crew from CrewBlueprint export interface Crew { diff --git a/src/routes/(app)/crew/members/+page.svelte b/src/routes/(app)/crew/members/+page.svelte index 542d40e7..9bb62215 100644 --- a/src/routes/(app)/crew/members/+page.svelte +++ b/src/routes/(app)/crew/members/+page.svelte @@ -21,10 +21,19 @@ import SettingsRow from '$lib/components/ui/SettingsRow.svelte' import Switch from '$lib/components/ui/switch/Switch.svelte' import CrewHeader from '$lib/components/crew/CrewHeader.svelte' + import MemberRow from '$lib/components/crew/MemberRow.svelte' + import PhantomRow from '$lib/components/crew/PhantomRow.svelte' import ScoutUserModal from '$lib/components/crew/ScoutUserModal.svelte' import BulkPhantomModal from '$lib/components/crew/BulkPhantomModal.svelte' + import AssignPhantomModal from '$lib/components/crew/AssignPhantomModal.svelte' + import ConfirmClaimModal from '$lib/components/crew/ConfirmClaimModal.svelte' import { DropdownMenu as DropdownMenuBase } from 'bits-ui' - import type { MemberFilter, CrewMembership, PhantomPlayer, CrewInvitation } from '$lib/types/api/crew' + import type { + MemberFilter, + CrewMembership, + PhantomPlayer, + CrewInvitation + } from '$lib/types/api/crew' import type { PageData } from './$types' interface Props { @@ -59,13 +68,9 @@ const activeRosterSize = $derived.by(() => { // Use active filter data if viewing active, otherwise use dedicated query const activeMembers = - filter === 'active' - ? (membersQuery.data?.members ?? []) - : (activeQuery.data?.members ?? []) + filter === 'active' ? (membersQuery.data?.members ?? []) : (activeQuery.data?.members ?? []) const activePhantoms = - filter === 'active' - ? (membersQuery.data?.phantoms ?? []) - : (activeQuery.data?.phantoms ?? []) + filter === 'active' ? (membersQuery.data?.phantoms ?? []) : (activeQuery.data?.phantoms ?? []) const activeMemberCount = activeMembers.filter((m) => !m.retired).length const activePhantomCount = activePhantoms.filter((p) => !p.retired).length @@ -79,13 +84,19 @@ const updateMembershipMutation = useUpdateMembership() 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' } - ] + // Filter options - Pending only shown to officers + const filterOptions = $derived.by(() => { + const options: { value: MemberFilter; label: string }[] = [ + { value: 'all', label: 'All' }, + { value: 'active', label: 'Active' }, + { value: 'phantom', label: 'Phantoms' } + ] + if (crewStore.isOfficer) { + options.push({ value: 'pending', label: 'Pending' }) + } + options.push({ value: 'retired', label: 'Retired' }) + return options + }) // Change filter function handleFilterChange(newFilter: MemberFilter) { @@ -113,9 +124,6 @@ // Dialog state for scout modal let scoutModalOpen = $state(false) - // Pending invitations section visibility - let invitationsSectionOpen = $state(true) - // Dialog state for phantom creation let bulkPhantomDialogOpen = $state(false) @@ -123,28 +131,13 @@ let deletePhantomDialogOpen = $state(false) let phantomToDelete = $state(null) - // Role display helpers - function getRoleLabel(role: string): string { - switch (role) { - case 'captain': - return 'Captain' - case 'vice_captain': - return 'Vice Captain' - default: - return 'Member' - } - } + // Dialog state for phantom assignment + let assignPhantomDialogOpen = $state(false) + let phantomToAssign = $state(null) - function getRoleClass(role: string): string { - switch (role) { - case 'captain': - return 'captain' - case 'vice_captain': - return 'officer' - default: - return '' - } - } + // Dialog state for confirm claim + let confirmClaimDialogOpen = $state(false) + let phantomToClaim = $state(null) // Member actions function openRemoveDialog(member: CrewMembership) { @@ -201,7 +194,7 @@ editingMember = member editingPhantom = null // Format date for input - editJoinDate = member.joinedAt ? member.joinedAt.split('T')[0] ?? '' : '' + editJoinDate = member.joinedAt ? (member.joinedAt.split('T')[0] ?? '') : '' editRetired = member.retired editDialogOpen = true } @@ -209,7 +202,7 @@ function openEditPhantomDialog(phantom: PhantomPlayer) { editingPhantom = phantom editingMember = null - editJoinDate = phantom.joinedAt ? phantom.joinedAt.split('T')[0] ?? '' : '' + editJoinDate = phantom.joinedAt ? (phantom.joinedAt.split('T')[0] ?? '') : '' editRetired = phantom.retired editDialogOpen = true } @@ -267,6 +260,18 @@ phantomToDelete = null } + // Phantom assignment + function openAssignPhantomDialog(phantom: PhantomPlayer) { + phantomToAssign = phantom + assignPhantomDialogOpen = true + } + + // Confirm claim + function openConfirmClaimDialog(phantom: PhantomPlayer) { + phantomToClaim = phantom + confirmClaimDialogOpen = true + } + // Format date function formatDate(dateString: string): string { return new Date(dateString).toLocaleDateString(undefined, { @@ -285,6 +290,11 @@ const pendingInvitationsCount = $derived( invitationsQuery.data?.filter((inv) => !isInvitationExpired(inv.expiresAt)).length ?? 0 ) + + // Get phantoms with pending claims (assigned but not confirmed) + const pendingClaimPhantoms = $derived( + membersQuery.data?.phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? [] + ) @@ -293,7 +303,7 @@
- + {#snippet belowTitle()}
{#each filterOptions as option} @@ -334,50 +344,41 @@ {/snippet} - - {#if crewStore.isOfficer && invitationsQuery.data && invitationsQuery.data.length > 0} -
- - - {#if invitationsSectionOpen} -
    - {#each invitationsQuery.data as invitation} - {@const expired = isInvitationExpired(invitation.expiresAt)} -
  • -
    - {invitation.user?.username ?? 'Unknown'} - {#if invitation.invitedBy} - - Invited by {invitation.invitedBy.username} - - {/if} -
    -
    - {#if expired} - Expired - {:else} - Expires {formatDate(invitation.expiresAt)} - {/if} -
    -
  • - {/each} -
- {/if} -
- {/if} - - {#if membersQuery.isLoading} + + {#if filter === 'pending'} + {#if invitationsQuery.isLoading} +
+

Loading...

+
+ {:else if invitationsQuery.data && invitationsQuery.data.length > 0} +
    + {#each invitationsQuery.data as invitation} + {@const expired = isInvitationExpired(invitation.expiresAt)} +
  • +
    + {invitation.user?.username ?? 'Unknown'} + {#if invitation.invitedBy} + + Invited by {invitation.invitedBy.username} + + {/if} +
    +
    + {#if expired} + Expired + {:else} + Expires {formatDate(invitation.expiresAt)} + {/if} +
    +
  • + {/each} +
+ {:else} +
+

No pending invitations.

+
+ {/if} + {:else if membersQuery.isLoading}

Loading...

@@ -390,62 +391,38 @@ {#if membersQuery.data?.members && membersQuery.data.members.length > 0}
    {#each membersQuery.data.members as member} -
  • -
    - {member.user?.username ?? 'Unknown'} - - {getRoleLabel(member.role)} - - {#if member.joinedAt} - Joined {formatDate(member.joinedAt)} - {/if} -
    - {#if crewStore.isOfficer} - - {#snippet trigger({ props })} -
  • + openEditMemberDialog(member)} + onPromote={() => openPromoteDialog(member)} + onDemote={() => openDemoteDialog(member)} + onRemove={() => openRemoveDialog(member)} + /> {/each}
{:else if filter !== 'phantom'}

No members found

{/if} + + {#if crewStore.isOfficer && pendingClaimPhantoms.length > 0 && (filter === 'all' || filter === 'phantom')} +
+ Pending Claims ({pendingClaimPhantoms.length}) +
+
    + {#each pendingClaimPhantoms as phantom} + openEditPhantomDialog(phantom)} + onDelete={() => openDeletePhantomDialog(phantom)} + onAssign={() => openAssignPhantomDialog(phantom)} + onConfirmClaim={() => openConfirmClaimDialog(phantom)} + /> + {/each} +
+ {/if} + {#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0} {#if filter === 'all' && membersQuery.data.members.length > 0} @@ -455,51 +432,14 @@ {/if}
    {#each membersQuery.data.phantoms as phantom} -
  • -
    -
    - {phantom.name} - {#if phantom.granblueId} - ID: {phantom.granblueId} - {/if} - {#if phantom.joinedAt} - Joined {formatDate(phantom.joinedAt)} - {/if} -
    - {#if phantom.claimConfirmed && phantom.claimedBy} - - Claimed by {phantom.claimedBy.username} - - {:else if phantom.claimedBy} - - Pending: {phantom.claimedBy.username} - - {:else} - Unclaimed - {/if} -
    - {#if crewStore.isOfficer} - - {#snippet trigger({ props })} -
  • + openEditPhantomDialog(phantom)} + onDelete={() => openDeletePhantomDialog(phantom)} + onAssign={() => openAssignPhantomDialog(phantom)} + onConfirmClaim={() => openConfirmClaimDialog(phantom)} + /> {/each}
{:else if filter === 'phantom'} @@ -543,7 +483,12 @@ (confirmDialogOpen = false)} primaryAction={{ - label: confirmAction === 'remove' ? 'Remove' : confirmAction === 'promote' ? 'Promote' : 'Demote', + label: + confirmAction === 'remove' + ? 'Remove' + : confirmAction === 'promote' + ? 'Promote' + : 'Demote', onclick: handleConfirmAction, destructive: confirmAction === 'remove' }} @@ -567,10 +512,7 @@ This date is used to determine which events a member was active for when adding historical GW scores.

- + {#snippet control()} {/snippet} @@ -617,6 +559,16 @@ {#if crewStore.crew?.id} + + {/if}