refactor members page filters, add granblue id to phantom edit

This commit is contained in:
Justin Edmund 2025-12-18 17:48:53 -08:00
parent b718dcc335
commit 3a236032bd
2 changed files with 71 additions and 101 deletions

View file

@ -43,9 +43,6 @@
<div class="phantom-info"> <div class="phantom-info">
<div class="phantom-details"> <div class="phantom-details">
<span class="name">{phantom.name}</span> <span class="name">{phantom.name}</span>
{#if phantom.granblueId}
<span class="granblue-id">ID: {phantom.granblueId}</span>
{/if}
{#if phantom.joinedAt} {#if phantom.joinedAt}
<span class="joined-date">Joined {formatDate(phantom.joinedAt)}</span> <span class="joined-date">Joined {formatDate(phantom.joinedAt)}</span>
{/if} {/if}
@ -144,11 +141,6 @@
color: var(--text-primary); color: var(--text-primary);
} }
.granblue-id {
font-size: typography.$font-small;
color: var(--text-secondary);
}
.joined-date { .joined-date {
font-size: typography.$font-small; font-size: typography.$font-small;
color: var(--text-tertiary); color: var(--text-tertiary);

View file

@ -16,6 +16,8 @@
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
import Dialog from '$lib/components/ui/Dialog.svelte' import Dialog from '$lib/components/ui/Dialog.svelte'
import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte' import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte'
import Input from '$lib/components/ui/Input.svelte'
import DatePicker from '$lib/components/ui/DatePicker.svelte'
import ModalHeader from '$lib/components/ui/ModalHeader.svelte' import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
import ModalBody from '$lib/components/ui/ModalBody.svelte' import ModalBody from '$lib/components/ui/ModalBody.svelte'
import ModalFooter from '$lib/components/ui/ModalFooter.svelte' import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
@ -66,10 +68,10 @@
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) // Query for phantoms (needed for pending claims badge when viewing pending filter)
const phantomsQuery = createQuery(() => ({ const phantomsQuery = createQuery(() => ({
...crewQueries.members('phantom'), ...crewQueries.members('phantom'),
enabled: filter !== 'phantom' && filter !== 'all' && crewStore.isOfficer enabled: filter === 'pending' && crewStore.isOfficer
})) }))
// Calculate total active roster size (members + phantoms) // Calculate total active roster size (members + phantoms)
@ -97,8 +99,7 @@
const filterOptions = $derived.by(() => { const filterOptions = $derived.by(() => {
const options: { value: MemberFilter; label: string }[] = [ const options: { value: MemberFilter; label: string }[] = [
{ value: 'all', label: 'All' }, { value: 'all', label: 'All' },
{ value: 'active', label: 'Active' }, { value: 'active', label: 'Active' }
{ value: 'phantom', label: 'Phantoms' }
] ]
if (crewStore.isOfficer) { if (crewStore.isOfficer) {
options.push({ value: 'pending', label: 'Pending' }) options.push({ value: 'pending', label: 'Pending' })
@ -130,6 +131,7 @@
let editJoinDate = $state('') let editJoinDate = $state('')
let editRetired = $state(false) let editRetired = $state(false)
let editRetiredAt = $state('') let editRetiredAt = $state('')
let editGranblueId = $state('')
// Dialog state for scout modal // Dialog state for scout modal
let scoutModalOpen = $state(false) let scoutModalOpen = $state(false)
@ -216,6 +218,7 @@
editJoinDate = phantom.joinedAt ? (phantom.joinedAt.split('T')[0] ?? '') : '' editJoinDate = phantom.joinedAt ? (phantom.joinedAt.split('T')[0] ?? '') : ''
editRetired = phantom.retired editRetired = phantom.retired
editRetiredAt = phantom.retiredAt ? (phantom.retiredAt.split('T')[0] ?? '') : '' editRetiredAt = phantom.retiredAt ? (phantom.retiredAt.split('T')[0] ?? '') : ''
editGranblueId = phantom.granblueId ?? ''
editDialogOpen = true editDialogOpen = true
} }
@ -238,7 +241,8 @@
await crewAdapter.updatePhantom(crewStore.crew.id, editingPhantom.id, { await crewAdapter.updatePhantom(crewStore.crew.id, editingPhantom.id, {
joinedAt: editJoinDate, joinedAt: editJoinDate,
retired: editRetired, retired: editRetired,
retiredAt: editRetired ? editRetiredAt || undefined : undefined retiredAt: editRetired ? editRetiredAt || undefined : undefined,
granblueId: editGranblueId || undefined
}) })
// Invalidate members query // Invalidate members query
membersQuery.refetch() membersQuery.refetch()
@ -255,6 +259,7 @@
editJoinDate = '' editJoinDate = ''
editRetired = false editRetired = false
editRetiredAt = '' editRetiredAt = ''
editGranblueId = ''
} }
function openDeletePhantomDialog(phantom: PhantomPlayer) { function openDeletePhantomDialog(phantom: PhantomPlayer) {
@ -315,12 +320,11 @@
) )
// Get phantoms with pending claims (assigned but not confirmed) // Get phantoms with pending claims (assigned but not confirmed)
// Use phantom query when not viewing phantom/all filter to ensure badge always has data // Use phantom query when viewing pending filter since it doesn't include phantoms
const pendingClaimPhantoms = $derived.by(() => { const pendingClaimPhantoms = $derived.by(() => {
let phantoms = membersQuery.data?.phantoms const phantoms = filter === 'pending'
if (filter !== 'phantom' && filter !== 'all') { ? phantomsQuery.data?.phantoms
phantoms = phantomsQuery.data?.phantoms : membersQuery.data?.phantoms
}
return phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? [] return phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? []
}) })
</script> </script>
@ -444,52 +448,56 @@
<p>Failed to load members</p> <p>Failed to load members</p>
</div> </div>
{:else} {:else}
<!-- Regular members --> {@const hasMembers = membersQuery.data?.members && membersQuery.data.members.length > 0}
{#if membersQuery.data?.members && membersQuery.data.members.length > 0} {@const hasPhantoms = membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0}
<ul class="member-list">
{#each membersQuery.data.members as member}
<MemberRow
{member}
onEdit={() => openEditMemberDialog(member)}
onPromote={() => openPromoteDialog(member)}
onDemote={() => openDemoteDialog(member)}
onRemove={() => openRemoveDialog(member)}
/>
{/each}
</ul>
{:else if filter !== 'phantom'}
<p class="empty-state">No members found</p>
{/if}
<!-- Phantom players --> <!-- Empty state for active/retired when no members or phantoms -->
{#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0} {#if (filter === 'active' || filter === 'retired') && !hasMembers && !hasPhantoms}
{#if filter === 'all' && membersQuery.data.members.length > 0}
<div class="section-divider">
<span>Phantom Players</span>
</div>
{/if}
<ul class="member-list">
{#each membersQuery.data.phantoms 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>
{:else if filter === 'phantom'}
<div class="empty-state"> <div class="empty-state">
<p>No phantom players.</p> <p>No {filter} players found.</p>
{#if crewStore.isOfficer}
<Button variant="secondary" size="small" onclick={() => (bulkPhantomDialogOpen = true)}>
Add phantoms...
</Button>
{/if}
</div> </div>
{:else}
<!-- Regular members -->
{#if hasMembers}
{#if (filter === 'active' || filter === 'retired') && hasPhantoms}
<div class="section-divider">
<span>Members ({membersQuery.data?.members.length})</span>
</div>
{/if}
<ul class="member-list">
{#each membersQuery.data?.members ?? [] as member}
<MemberRow
{member}
onEdit={() => openEditMemberDialog(member)}
onPromote={() => openPromoteDialog(member)}
onDemote={() => openDemoteDialog(member)}
onRemove={() => openRemoveDialog(member)}
/>
{/each}
</ul>
{/if}
<!-- Phantom players -->
{#if hasPhantoms}
{#if filter === 'all' || filter === 'active' || filter === 'retired'}
<div class="section-divider">
<span>Phantom Players ({membersQuery.data?.phantoms.length})</span>
</div>
{/if}
<ul class="member-list">
{#each membersQuery.data?.phantoms ?? [] 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} {/if}
{/if} {/if}
</div> </div>
@ -543,10 +551,15 @@
<ModalBody> <ModalBody>
<div class="modal-form"> <div class="modal-form">
<div class="form-fields"> <div class="form-fields">
<div class="form-field"> {#if editingPhantom}
<label for="joinDate">Join date</label> <Input
<input id="joinDate" type="date" bind:value={editJoinDate} class="date-input" /> label="Granblue ID"
</div> bind:value={editGranblueId}
maxLength={20}
variant="contained"
/>
{/if}
<DatePicker label="Join date" bind:value={editJoinDate} contained />
<p class="help-text"> <p class="help-text">
This date is used to determine which events a member was active for when adding This date is used to determine which events a member was active for when adding
historical GW scores. historical GW scores.
@ -557,10 +570,7 @@
{/snippet} {/snippet}
</SettingsRow> </SettingsRow>
{#if editRetired} {#if editRetired}
<div class="form-field"> <DatePicker label="Retired date" bind:value={editRetiredAt} contained />
<label for="retiredAt">Retired date</label>
<input id="retiredAt" type="date" bind:value={editRetiredAt} class="date-input" />
</div>
<p class="help-text"> <p class="help-text">
This date is used to determine which events a retired player was active for. This date is used to determine which events a retired player was active for.
</p> </p>
@ -758,44 +768,12 @@
gap: spacing.$unit-3x; gap: spacing.$unit-3x;
} }
.form-field {
display: flex;
flex-direction: column;
gap: spacing.$unit-half;
label {
font-size: typography.$font-small;
font-weight: typography.$medium;
color: var(--text-primary);
}
}
:global(fieldset) { :global(fieldset) {
border: none; border: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.date-input {
padding: spacing.$unit spacing.$unit-2x;
border: none;
border-radius: layout.$input-corner;
font-size: typography.$font-regular;
font-family: inherit;
background: var(--input-bound-bg);
color: var(--text-primary);
width: 100%;
&:hover {
background: var(--input-bound-bg-hover);
}
&:focus {
outline: none;
background: var(--input-bound-bg-hover);
}
}
.help-text { .help-text {
font-size: typography.$font-small; font-size: typography.$font-small;
color: var(--text-secondary); color: var(--text-secondary);