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
}))
// 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)
const activeRosterSize = $derived.by(() => {
// Use active filter data if viewing active, otherwise use dedicated query
@ -308,9 +314,14 @@
)
// Get phantoms with pending claims (assigned but not confirmed)
const pendingClaimPhantoms = $derived(
membersQuery.data?.phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? []
)
// Use phantom query when not viewing phantom/all filter to ensure badge always has data
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>
<svelte:head>
@ -329,6 +340,9 @@
onclick={() => handleFilterChange(option.value)}
>
{option.label}
{#if option.value === 'pending' && (pendingInvitationsCount > 0 || pendingClaimPhantoms.length > 0)}
<span class="tab-badge">{pendingInvitationsCount + pendingClaimPhantoms.length}</span>
{/if}
</button>
{/each}
</div>
@ -345,7 +359,7 @@
</Button>
<DropdownMenu>
{#snippet trigger({ props })}
<Button variant="secondary" size="small" iconOnly icon="ellipsis" {...props} />
<Button variant="ghost" size="small" iconOnly icon="ellipsis" {...props} />
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item
@ -360,39 +374,65 @@
{/snippet}
</CrewHeader>
<!-- Pending Invitations (shown when filter is 'pending') -->
<!-- Pending tab (invitations and claims) -->
{#if filter === 'pending'}
{#if invitationsQuery.isLoading}
{#if invitationsQuery.isLoading || phantomsQuery.isLoading}
<div class="loading-state">
<p>Loading...</p>
</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}
<div class="empty-state">
<p>No pending invitations.</p>
</div>
{#if invitationsQuery.data && invitationsQuery.data.length > 0}
<div class="section-divider">
<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}
{:else if membersQuery.isLoading}
<div class="loading-state">
@ -420,26 +460,6 @@
<p class="empty-state">No members found</p>
{/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 -->
{#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0}
{#if filter === 'all' && membersQuery.data.members.length > 0}
@ -619,6 +639,9 @@
}
.filter-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 4px spacing.$unit;
background: 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,
.error-state {
display: flex;