move pending claims to pending tab with badge

This commit is contained in:
Justin Edmund 2025-12-17 23:00:22 -08:00
parent 2800bf0554
commit 61cf217107

View file

@ -65,6 +65,12 @@
enabled: crewStore.isOfficer && !!crewStore.crew?.id enabled: crewStore.isOfficer && !!crewStore.crew?.id
})) }))
// Query for phantoms (needed for pending claims badge when not viewing phantom/all filter)
const phantomsQuery = createQuery(() => ({
...crewQueries.members('phantom'),
enabled: filter !== 'phantom' && filter !== 'all' && crewStore.isOfficer
}))
// Calculate total active roster size (members + phantoms) // Calculate total active roster size (members + phantoms)
const activeRosterSize = $derived.by(() => { const activeRosterSize = $derived.by(() => {
// Use active filter data if viewing active, otherwise use dedicated query // Use active filter data if viewing active, otherwise use dedicated query
@ -308,9 +314,14 @@
) )
// Get phantoms with pending claims (assigned but not confirmed) // Get phantoms with pending claims (assigned but not confirmed)
const pendingClaimPhantoms = $derived( // Use phantom query when not viewing phantom/all filter to ensure badge always has data
membersQuery.data?.phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? [] const pendingClaimPhantoms = $derived.by(() => {
) let phantoms = membersQuery.data?.phantoms
if (filter !== 'phantom' && filter !== 'all') {
phantoms = phantomsQuery.data?.phantoms
}
return phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? []
})
</script> </script>
<svelte:head> <svelte:head>
@ -329,6 +340,9 @@
onclick={() => handleFilterChange(option.value)} onclick={() => handleFilterChange(option.value)}
> >
{option.label} {option.label}
{#if option.value === 'pending' && (pendingInvitationsCount > 0 || pendingClaimPhantoms.length > 0)}
<span class="tab-badge">{pendingInvitationsCount + pendingClaimPhantoms.length}</span>
{/if}
</button> </button>
{/each} {/each}
</div> </div>
@ -345,7 +359,7 @@
</Button> </Button>
<DropdownMenu> <DropdownMenu>
{#snippet trigger({ props })} {#snippet trigger({ props })}
<Button variant="secondary" size="small" iconOnly icon="ellipsis" {...props} /> <Button variant="ghost" size="small" iconOnly icon="ellipsis" {...props} />
{/snippet} {/snippet}
{#snippet menu()} {#snippet menu()}
<DropdownMenuBase.Item <DropdownMenuBase.Item
@ -360,39 +374,65 @@
{/snippet} {/snippet}
</CrewHeader> </CrewHeader>
<!-- Pending Invitations (shown when filter is 'pending') --> <!-- Pending tab (invitations and claims) -->
{#if filter === 'pending'} {#if filter === 'pending'}
{#if invitationsQuery.isLoading} {#if invitationsQuery.isLoading || phantomsQuery.isLoading}
<div class="loading-state"> <div class="loading-state">
<p>Loading...</p> <p>Loading...</p>
</div> </div>
{:else if invitationsQuery.data && invitationsQuery.data.length > 0}
<ul class="member-list">
{#each invitationsQuery.data as invitation}
{@const expired = isInvitationExpired(invitation.expiresAt)}
<li class="invitation-row" class:expired>
<div class="invitation-info">
<span class="invited-user">{invitation.user?.username ?? 'Unknown'}</span>
{#if invitation.invitedBy}
<span class="invited-by">
Invited by {invitation.invitedBy.username}
</span>
{/if}
</div>
<div class="invitation-status">
{#if expired}
<span class="status-badge expired">Expired</span>
{:else}
<span class="expires-text">Expires {formatDate(invitation.expiresAt)}</span>
{/if}
</div>
</li>
{/each}
</ul>
{:else} {:else}
<div class="empty-state"> {#if invitationsQuery.data && invitationsQuery.data.length > 0}
<p>No pending invitations.</p> <div class="section-divider">
</div> <span>Pending Invitations ({invitationsQuery.data.length})</span>
</div>
<ul class="member-list">
{#each invitationsQuery.data as invitation}
{@const expired = isInvitationExpired(invitation.expiresAt)}
<li class="invitation-row" class:expired>
<div class="invitation-info">
<span class="invited-user">{invitation.user?.username ?? 'Unknown'}</span>
{#if invitation.invitedBy}
<span class="invited-by">
Invited by {invitation.invitedBy.username}
</span>
{/if}
</div>
<div class="invitation-status">
{#if expired}
<span class="status-badge expired">Expired</span>
{:else}
<span class="expires-text">Expires {formatDate(invitation.expiresAt)}</span>
{/if}
</div>
</li>
{/each}
</ul>
{/if}
{#if pendingClaimPhantoms.length > 0}
<div class="section-divider pending-claims">
<span>Pending Claims ({pendingClaimPhantoms.length})</span>
</div>
<ul class="member-list">
{#each pendingClaimPhantoms as phantom}
<PhantomRow
{phantom}
currentUserId={crewStore.membership?.user?.id}
onEdit={() => openEditPhantomDialog(phantom)}
onDelete={() => openDeletePhantomDialog(phantom)}
onAssign={() => openAssignPhantomDialog(phantom)}
onAccept={() => openConfirmClaimDialog(phantom)}
onDecline={() => handleDeclineClaim(phantom)}
/>
{/each}
</ul>
{/if}
{#if (!invitationsQuery.data || invitationsQuery.data.length === 0) && pendingClaimPhantoms.length === 0}
<div class="empty-state">
<p>No pending items.</p>
</div>
{/if}
{/if} {/if}
{:else if membersQuery.isLoading} {:else if membersQuery.isLoading}
<div class="loading-state"> <div class="loading-state">
@ -420,26 +460,6 @@
<p class="empty-state">No members found</p> <p class="empty-state">No members found</p>
{/if} {/if}
<!-- Pending Claims Section (officers only) -->
{#if crewStore.isOfficer && pendingClaimPhantoms.length > 0 && (filter === 'all' || filter === 'phantom')}
<div class="section-divider pending-claims">
<span>Pending Claims ({pendingClaimPhantoms.length})</span>
</div>
<ul class="member-list">
{#each pendingClaimPhantoms as phantom}
<PhantomRow
{phantom}
currentUserId={crewStore.membership?.user?.id}
onEdit={() => openEditPhantomDialog(phantom)}
onDelete={() => openDeletePhantomDialog(phantom)}
onAssign={() => openAssignPhantomDialog(phantom)}
onAccept={() => openConfirmClaimDialog(phantom)}
onDecline={() => handleDeclineClaim(phantom)}
/>
{/each}
</ul>
{/if}
<!-- Phantom players --> <!-- Phantom players -->
{#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0} {#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0}
{#if filter === 'all' && membersQuery.data.members.length > 0} {#if filter === 'all' && membersQuery.data.members.length > 0}
@ -619,6 +639,9 @@
} }
.filter-tab { .filter-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 4px spacing.$unit; padding: 4px spacing.$unit;
background: none; background: none;
border: none; border: none;
@ -642,6 +665,15 @@
} }
} }
.tab-badge {
background: var(--color-orange, #f97316);
color: white;
font-size: 11px;
font-weight: typography.$medium;
padding: 1px 6px;
border-radius: 10px;
}
.loading-state, .loading-state,
.error-state { .error-state {
display: flex; display: flex;