move pending claims to pending tab with badge
This commit is contained in:
parent
2800bf0554
commit
61cf217107
1 changed files with 84 additions and 52 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue