From 74df78a9493d70723faa2b9cd25e8614afdaafda Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 18 Dec 2025 19:37:49 -0800 Subject: [PATCH] add membership history editing for boomerang players --- src/lib/api/adapters/crew.adapter.ts | 11 ++ src/routes/(app)/crew/members/+page.svelte | 200 ++++++++++++++++++--- 2 files changed, 188 insertions(+), 23 deletions(-) diff --git a/src/lib/api/adapters/crew.adapter.ts b/src/lib/api/adapters/crew.adapter.ts index dcd81693..055b6579 100644 --- a/src/lib/api/adapters/crew.adapter.ts +++ b/src/lib/api/adapters/crew.adapter.ts @@ -123,6 +123,17 @@ export class CrewAdapter extends BaseAdapter { this.clearCache('/crew/members') } + /** + * Get all membership periods for a user in a crew (for boomerang players) + */ + async getMembershipHistory(crewId: string, userId: string, options?: RequestOptions): Promise { + const response = await this.request<{ memberships: CrewMembership[] }>( + `/crews/${crewId}/memberships/by_user/${userId}`, + options + ) + return response.memberships + } + // ==================== Invitation Operations ==================== /** diff --git a/src/routes/(app)/crew/members/+page.svelte b/src/routes/(app)/crew/members/+page.svelte index 31d71654..c75bd0f9 100644 --- a/src/routes/(app)/crew/members/+page.svelte +++ b/src/routes/(app)/crew/members/+page.svelte @@ -133,6 +133,16 @@ let editRetiredAt = $state('') let editGranblueId = $state('') + // Membership history for boomerang players + interface EditableMembershipPeriod { + id: string + joinedAt: string + retiredAt: string + retired: boolean + } + let membershipHistory = $state([]) + let loadingHistory = $state(false) + // Dialog state for scout modal let scoutModalOpen = $state(false) @@ -202,14 +212,35 @@ } // Member/phantom editing - function openEditMemberDialog(member: CrewMembership) { + async function openEditMemberDialog(member: CrewMembership) { editingMember = member editingPhantom = null // Format date for input editJoinDate = member.joinedAt ? (member.joinedAt.split('T')[0] ?? '') : '' editRetired = member.retired editRetiredAt = member.retiredAt ? (member.retiredAt.split('T')[0] ?? '') : '' + membershipHistory = [] editDialogOpen = true + + // Fetch membership history for boomerang players + if (crewStore.crew && member.user?.id) { + loadingHistory = true + try { + const history = await crewAdapter.getMembershipHistory(crewStore.crew.id, member.user.id) + // Only show multi-period UI if there are multiple memberships + if (history.length > 1) { + membershipHistory = history.map((m) => ({ + id: m.id, + joinedAt: m.joinedAt ? (m.joinedAt.split('T')[0] ?? '') : '', + retiredAt: m.retiredAt ? (m.retiredAt.split('T')[0] ?? '') : '', + retired: m.retired + })) + } + } catch (error) { + console.error('Failed to fetch membership history:', error) + } + loadingHistory = false + } } function openEditPhantomDialog(phantom: PhantomPlayer) { @@ -227,15 +258,32 @@ try { if (editingMember) { - await updateMembershipMutation.mutateAsync({ - crewId: crewStore.crew.id, - membershipId: editingMember.id, - input: { - joinedAt: editJoinDate, - retired: editRetired, - retiredAt: editRetired ? editRetiredAt || undefined : undefined + // Check if we have multiple membership periods (boomerang player) + if (membershipHistory.length > 1) { + // Update each membership period + for (const period of membershipHistory) { + await updateMembershipMutation.mutateAsync({ + crewId: crewStore.crew.id, + membershipId: period.id, + input: { + joinedAt: period.joinedAt, + retired: period.retired, + retiredAt: period.retired ? period.retiredAt || undefined : undefined + } + }) } - }) + } else { + // Single membership period (normal case) + await updateMembershipMutation.mutateAsync({ + crewId: crewStore.crew.id, + membershipId: editingMember.id, + input: { + joinedAt: editJoinDate, + retired: editRetired, + retiredAt: editRetired ? editRetiredAt || undefined : undefined + } + }) + } } else if (editingPhantom) { // Call the phantom update directly through the adapter await crewAdapter.updatePhantom(crewStore.crew.id, editingPhantom.id, { @@ -260,6 +308,7 @@ editRetired = false editRetiredAt = '' editGranblueId = '' + membershipHistory = [] } function openDeletePhantomDialog(phantom: PhantomPlayer) { @@ -559,21 +608,81 @@ variant="contained" /> {/if} - -

- This date is used to determine which events a member was active for when adding - historical GW scores. -

- - {#snippet control()} - - {/snippet} - - {#if editRetired} - + + {#if loadingHistory} +

Loading membership history...

+ {:else if membershipHistory.length > 1} + +
+

Membership Periods

+

+ This player has joined and left the crew multiple times. Edit each period below. +

+ {#each membershipHistory as period, i} +
+ + {#if i === 0} + Current + {:else} + Period {membershipHistory.length - i} + {/if} + +
+ + {#if period.retired || i > 0} + + {/if} +
+
+ {/each} +
+ + + {#if !membershipHistory[0]?.retired} + + {#snippet control()} + { + editRetired = checked + if (membershipHistory[0]) { + membershipHistory[0].retired = checked + } + }} + /> + {/snippet} + + {#if editRetired && membershipHistory[0]} + + {/if} + {/if} + {:else} + +

- This date is used to determine which events a retired player was active for. + This date is used to determine which events a member was active for when adding + historical GW scores.

+ + {#snippet control()} + + {/snippet} + + {#if editRetired} + +

+ This date is used to determine which events a retired player was active for. +

+ {/if} {/if} @@ -584,7 +693,7 @@ primaryAction={{ label: 'Save', onclick: handleSaveEdit, - disabled: !editJoinDate + disabled: membershipHistory.length > 1 ? !membershipHistory[0]?.joinedAt : !editJoinDate }} /> {/snippet} @@ -781,6 +890,51 @@ line-height: 1.4; } + .loading-text { + font-size: typography.$font-small; + color: var(--text-secondary); + margin: 0; + font-style: italic; + } + + .membership-periods { + display: flex; + flex-direction: column; + gap: spacing.$unit-2x; + } + + .periods-title { + font-size: typography.$font-regular; + font-weight: typography.$medium; + margin: 0; + color: var(--text-primary); + } + + .period-row { + display: flex; + flex-direction: column; + gap: spacing.$unit; + padding: spacing.$unit-2x; + background: rgba(0, 0, 0, 0.02); + border-radius: layout.$item-corner; + border: 1px solid rgba(0, 0, 0, 0.06); + } + + .period-label { + font-size: typography.$font-small; + font-weight: typography.$medium; + color: var(--text-secondary); + } + + .period-fields { + display: flex; + gap: spacing.$unit-2x; + + :global(.date-picker) { + flex: 1; + } + } + // Invitation row styles .invitation-row { display: flex;