fix(capacity): Fix three defects - caching, save, filters
1. Slow Team Member Dropdown - Fixed - Added cached team members store with 5-minute TTL - Dropdown now loads instantly on subsequent visits 2. Error Preventing Capacity Save - Fixed - Added saveAvailability API endpoint - Added backend service method to persist availability overrides - Added proper error handling and success feedback - Cache invalidation on save 3. Filters Not Working - Fixed - Fixed PTOManager to use shared selectedMemberId - Filters now react to team member selection Test Results: - Backend: 76 passed ✅ - Frontend Unit: 10 passed ✅ - E2E: 130 passed, 24 skipped ✅ Refs: openspec/changes/headroom-foundation
This commit is contained in:
@@ -5,7 +5,8 @@ import type {
|
||||
Holiday,
|
||||
PTO,
|
||||
Revenue,
|
||||
TeamCapacity
|
||||
TeamCapacity,
|
||||
TeamMemberAvailability
|
||||
} from '$lib/types/capacity';
|
||||
|
||||
export interface CreateHolidayData {
|
||||
@@ -139,3 +140,15 @@ export async function createPTO(data: CreatePTOData): Promise<PTO> {
|
||||
export async function approvePTO(id: string): Promise<PTO> {
|
||||
return api.put<PTO>(`/ptos/${id}/approve`);
|
||||
}
|
||||
|
||||
export interface SaveAvailabilityPayload {
|
||||
team_member_id: string;
|
||||
date: string;
|
||||
availability: 0 | 0.5 | 1;
|
||||
}
|
||||
|
||||
export async function saveAvailability(
|
||||
data: SaveAvailabilityPayload
|
||||
): Promise<TeamMemberAvailability> {
|
||||
return api.post<TeamMemberAvailability>('/capacity/availability', data);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
export let teamMembers: TeamMember[] = [];
|
||||
export let month: string;
|
||||
|
||||
let selectedMemberId = '';
|
||||
export let selectedMemberId = '';
|
||||
let submitting = false;
|
||||
let actionLoadingId: string | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
34
frontend/src/lib/stores/teamMembers.ts
Normal file
34
frontend/src/lib/stores/teamMembers.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { TeamMember } from '$lib/services/teamMemberService';
|
||||
import { teamMemberService } from '$lib/services/teamMemberService';
|
||||
|
||||
interface CachedTeamMembers {
|
||||
data: TeamMember[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
function createTeamMembersStore() {
|
||||
const { subscribe, set } = writable<TeamMember[]>([]);
|
||||
let cache: CachedTeamMembers | null = null;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
async load() {
|
||||
if (cache && Date.now() - cache.timestamp < CACHE_TTL) {
|
||||
return cache.data;
|
||||
}
|
||||
|
||||
const data = await teamMemberService.getAll();
|
||||
cache = { data, timestamp: Date.now() };
|
||||
set(data);
|
||||
return data;
|
||||
},
|
||||
invalidate() {
|
||||
cache = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const teamMembersStore = createTeamMembersStore();
|
||||
@@ -6,7 +6,6 @@
|
||||
import CapacitySummary from '$lib/components/capacity/CapacitySummary.svelte';
|
||||
import HolidayManager from '$lib/components/capacity/HolidayManager.svelte';
|
||||
import PTOManager from '$lib/components/capacity/PTOManager.svelte';
|
||||
import { teamMemberService } from '$lib/services/teamMemberService';
|
||||
import { selectedPeriod } from '$lib/stores/period';
|
||||
import {
|
||||
holidaysStore,
|
||||
@@ -18,9 +17,9 @@
|
||||
revenueStore,
|
||||
teamCapacityStore
|
||||
} from '$lib/stores/capacity';
|
||||
import { getIndividualCapacity } from '$lib/api/capacity';
|
||||
import { teamMembersStore } from '$lib/stores/teamMembers';
|
||||
import { getIndividualCapacity, saveAvailability } from '$lib/api/capacity';
|
||||
import type { Capacity } from '$lib/types/capacity';
|
||||
import type { TeamMember } from '$lib/services/teamMemberService';
|
||||
|
||||
type TabKey = 'calendar' | 'summary' | 'holidays' | 'pto';
|
||||
|
||||
@@ -32,26 +31,20 @@
|
||||
];
|
||||
|
||||
let activeTab: TabKey = 'calendar';
|
||||
let teamMembers: TeamMember[] = [];
|
||||
let selectedMemberId = '';
|
||||
let individualCapacity: Capacity | null = null;
|
||||
let loadingIndividual = false;
|
||||
let calendarError: string | null = null;
|
||||
let availabilitySaving = false;
|
||||
let availabilityError: string | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
await loadTeamMembers();
|
||||
});
|
||||
|
||||
async function loadTeamMembers() {
|
||||
try {
|
||||
teamMembers = await teamMemberService.getAll();
|
||||
if (!selectedMemberId && teamMembers.length) {
|
||||
selectedMemberId = teamMembers[0].id;
|
||||
}
|
||||
await teamMembersStore.load();
|
||||
} catch (error) {
|
||||
console.error('Unable to load team members', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshIndividualCapacity(memberId: string, period: string) {
|
||||
if (
|
||||
@@ -76,6 +69,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($teamMembersStore.length) {
|
||||
const exists = $teamMembersStore.some((member) => member.id === selectedMemberId);
|
||||
if (!selectedMemberId || !exists) {
|
||||
selectedMemberId = $teamMembersStore[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAvailabilityChange(event: CustomEvent<{ date: string; availability: number }>) {
|
||||
const { date, availability } = event.detail;
|
||||
const period = $selectedPeriod;
|
||||
|
||||
if (!selectedMemberId || !period) {
|
||||
return;
|
||||
}
|
||||
|
||||
availabilitySaving = true;
|
||||
availabilityError = null;
|
||||
|
||||
try {
|
||||
await saveAvailability({
|
||||
team_member_id: selectedMemberId,
|
||||
date,
|
||||
availability
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
refreshIndividualCapacity(selectedMemberId, period),
|
||||
loadTeamCapacity(period),
|
||||
loadRevenue(period)
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to save availability', error);
|
||||
availabilityError = 'Unable to save availability changes.';
|
||||
} finally {
|
||||
availabilitySaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($selectedPeriod) {
|
||||
loadTeamCapacity($selectedPeriod);
|
||||
loadRevenue($selectedPeriod);
|
||||
@@ -121,7 +152,7 @@
|
||||
bind:value={selectedMemberId}
|
||||
aria-label="Select team member"
|
||||
>
|
||||
{#each teamMembers as member}
|
||||
{#each $teamMembersStore as member}
|
||||
<option value={member.id}>{member.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -131,6 +162,17 @@
|
||||
<div class="alert alert-error text-sm">{calendarError}</div>
|
||||
{/if}
|
||||
|
||||
{#if availabilityError}
|
||||
<div class="alert alert-error text-sm">{availabilityError}</div>
|
||||
{/if}
|
||||
|
||||
{#if availabilitySaving}
|
||||
<div class="flex items-center gap-2 text-sm text-base-content/60">
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
Saving availability...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !selectedMemberId}
|
||||
<p class="text-sm text-base-content/60">Select a team member to view the calendar.</p>
|
||||
{:else if loadingIndividual}
|
||||
@@ -144,6 +186,7 @@
|
||||
capacity={individualCapacity}
|
||||
holidays={$holidaysStore}
|
||||
ptos={$ptosStore}
|
||||
on:availabilitychange={handleAvailabilityChange}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -151,12 +194,16 @@
|
||||
<CapacitySummary
|
||||
teamCapacity={$teamCapacityStore}
|
||||
revenue={$revenueStore}
|
||||
{teamMembers}
|
||||
teamMembers={$teamMembersStore}
|
||||
/>
|
||||
{:else if activeTab === 'holidays'}
|
||||
<HolidayManager month={$selectedPeriod} holidays={$holidaysStore} />
|
||||
{:else}
|
||||
<PTOManager {teamMembers} month={$selectedPeriod} />
|
||||
<PTOManager
|
||||
teamMembers={$teamMembersStore}
|
||||
month={$selectedPeriod}
|
||||
bind:selectedMemberId={selectedMemberId}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user