fix(capacity): Fix availability load/save error handling

- Clone response before reading to prevent body consumption issues
- Add better error logging in API client
- Surface backend error messages in capacity page
- Import ApiError type for proper error handling

Test Results:
- Backend: 15 capacity tests passed 
- Frontend Unit: 10 passed 
- E2E: 130 passed, 24 skipped 

Refs: openspec/changes/headroom-foundation
This commit is contained in:
2026-02-19 19:30:57 -05:00
parent d6b7215f93
commit 2f8ef8f2b3
2 changed files with 25 additions and 6 deletions

View File

@@ -221,13 +221,21 @@ export async function apiRequest<T>(endpoint: string, options: ApiRequestOptions
// Handle API response // Handle API response
async function handleResponse(response: Response): Promise<Response> { async function handleResponse(response: Response): Promise<Response> {
const contentType = response.headers?.get?.('content-type') || response.headers?.get?.('Content-Type'); const contentType =
response.headers?.get?.('content-type') || response.headers?.get?.('Content-Type');
const isJson = contentType && contentType.includes('application/json'); const isJson = contentType && contentType.includes('application/json');
if (!response.ok) { if (!response.ok) {
const data = isJson ? await response.json() : await response.text(); const payloadResponse = response.clone();
const data = isJson ? await payloadResponse.json() : await payloadResponse.text();
const errorData = typeof data === 'object' ? data : { message: data }; const errorData = typeof data === 'object' ? data : { message: data };
const message = (errorData as { message?: string }).message || 'API request failed'; const message = (errorData as { message?: string }).message || 'API request failed';
console.error('API error', {
url: response.url,
status: response.status,
message,
data: errorData
});
throw new ApiError(message, response.status, errorData); throw new ApiError(message, response.status, errorData);
} }

View File

@@ -19,6 +19,7 @@
} from '$lib/stores/capacity'; } from '$lib/stores/capacity';
import { teamMembersStore } from '$lib/stores/teamMembers'; import { teamMembersStore } from '$lib/stores/teamMembers';
import { getIndividualCapacity, saveAvailability } from '$lib/api/capacity'; import { getIndividualCapacity, saveAvailability } from '$lib/api/capacity';
import { ApiError } from '$lib/services/api';
import type { Capacity } from '$lib/types/capacity'; import type { Capacity } from '$lib/types/capacity';
type TabKey = 'calendar' | 'summary' | 'holidays' | 'pto'; type TabKey = 'calendar' | 'summary' | 'holidays' | 'pto';
@@ -46,6 +47,16 @@
} }
}); });
function mapError(error: unknown, fallback: string) {
if (error instanceof ApiError) {
return error.message;
}
if (error instanceof Error && error.message) {
return error.message;
}
return fallback;
}
async function refreshIndividualCapacity(memberId: string, period: string) { async function refreshIndividualCapacity(memberId: string, period: string) {
if ( if (
individualCapacity && individualCapacity &&
@@ -61,8 +72,8 @@
try { try {
individualCapacity = await getIndividualCapacity(period, memberId); individualCapacity = await getIndividualCapacity(period, memberId);
} catch (error) { } catch (error) {
console.error('Failed to load capacity details', error); console.error('Failed to load capacity details', error, { memberId, period });
calendarError = 'Unable to load individual capacity data.'; calendarError = mapError(error, 'Unable to load individual capacity data.');
individualCapacity = null; individualCapacity = null;
} finally { } finally {
loadingIndividual = false; loadingIndividual = false;
@@ -100,8 +111,8 @@
loadRevenue(period) loadRevenue(period)
]); ]);
} catch (error) { } catch (error) {
console.error('Failed to save availability', error); console.error('Failed to save availability', error, { memberId: selectedMemberId, date, availability });
availabilityError = 'Unable to save availability changes.'; availabilityError = mapError(error, 'Unable to save availability changes.');
} finally { } finally {
availabilitySaving = false; availabilitySaving = false;
} }