diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 56544af..e17fec2 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,7 +1,6 @@ export { useAnalytics } from './useAnalytics' export { useLeaveWishes } from './useLeaveWishes' export { usePlanProposal } from './usePlanProposal' -export { usePlanApply } from './usePlanApply' export { usePlanSync } from './usePlanSync' export { useWorkspaceMembers } from './useWorkspaceMembers' export { useShowWeekNumbers, getLastLeaveType, setLastLeaveType } from './useCalendarPreferences' diff --git a/frontend/src/hooks/usePlanApply.test.ts b/frontend/src/hooks/usePlanApply.test.ts deleted file mode 100644 index d431d64..0000000 --- a/frontend/src/hooks/usePlanApply.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { renderHook, waitFor } from '@testing-library/react' -import { usePlanApply } from './usePlanApply' -import * as api from '@/services/api' - -vi.mock('@/services/api') - -describe('usePlanApply', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('initializes with null response and no loading/error', () => { - const { result } = renderHook(() => usePlanApply()) - expect(result.current.response).toBeNull() - expect(result.current.isLoading).toBe(false) - expect(result.current.error).toBeNull() - }) - - it('calls applyPlan and returns the response', async () => { - const mockResponse = { - accepted: true, - createdCount: 2, - skippedIdenticalCount: 0, - skippedFreeCount: 0, - replacedCount: 0, - collisions: [], - rejections: [], - createdDays: [], - } - - vi.mocked(api.applyPlan).mockResolvedValue(mockResponse) - - const { result } = renderHook(() => usePlanApply()) - const days = [ - { - date: '2026-06-15', - parentPersonalDataUuid: 'pd-1', - childId: 'c-1', - dayType: 'sjukpenning' as const, - isDubbeldag: false, - }, - ] - - const applyPromise = result.current.apply('ws-1', days, false) - - await waitFor(() => { - expect(result.current.isLoading).toBe(false) - }) - - const response = await applyPromise - expect(response).toEqual(mockResponse) - expect(result.current.response).toEqual(mockResponse) - expect(api.applyPlan).toHaveBeenCalledWith('ws-1', days, false) - }) - - it('sets error when applyPlan throws', async () => { - vi.mocked(api.applyPlan).mockRejectedValue(new Error('Network error')) - - const { result } = renderHook(() => usePlanApply()) - const days = [ - { - date: '2026-06-15', - parentPersonalDataUuid: 'pd-1', - childId: 'c-1', - dayType: 'sjukpenning' as const, - }, - ] - - try { - await result.current.apply('ws-1', days, false) - } catch { - // Expected to throw - } - - await waitFor(() => { - expect(result.current.isLoading).toBe(false) - }) - - expect(result.current.error).toBe('Network error') - expect(result.current.response).toBeNull() - }) - - it('falls back to a generic message when a non-Error value is thrown', async () => { - vi.mocked(api.applyPlan).mockRejectedValue('boom') - - const { result } = renderHook(() => usePlanApply()) - const days = [ - { - date: '2026-06-15', - parentPersonalDataUuid: 'pd-1', - childId: 'c-1', - dayType: 'sjukpenning' as const, - }, - ] - - try { - await result.current.apply('ws-1', days, false) - } catch { - // Expected to throw - } - - await waitFor(() => { - expect(result.current.isLoading).toBe(false) - }) - - expect(result.current.error).toBe('Kunde inte använda plan') - expect(result.current.response).toBeNull() - }) - - it('handles replace parameter correctly', async () => { - const mockResponse = { - accepted: true, - createdCount: 1, - skippedIdenticalCount: 0, - skippedFreeCount: 0, - replacedCount: 1, - collisions: [], - rejections: [], - createdDays: [], - } - - vi.mocked(api.applyPlan).mockResolvedValue(mockResponse) - - const { result } = renderHook(() => usePlanApply()) - const days = [ - { - date: '2026-06-15', - parentPersonalDataUuid: 'pd-1', - childId: 'c-1', - dayType: 'sjukpenning' as const, - }, - ] - - const applyPromise = result.current.apply('ws-1', days, true) - - await waitFor(() => { - expect(result.current.isLoading).toBe(false) - }) - - const response = await applyPromise - expect(api.applyPlan).toHaveBeenCalledWith('ws-1', days, true) - expect(response).toEqual(mockResponse) - - await waitFor(() => { - expect(result.current.response).toEqual(mockResponse) - }) - }) -}) diff --git a/frontend/src/hooks/usePlanApply.ts b/frontend/src/hooks/usePlanApply.ts deleted file mode 100644 index dd8b61f..0000000 --- a/frontend/src/hooks/usePlanApply.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useState, useCallback } from 'react' -import * as api from '@/services/api' -import type { ProposedDayApplyItem, PlanApplyResponse } from '@/types' - -interface UsePlanApplyResult { - response: PlanApplyResponse | null - isLoading: boolean - error: string | null - apply: (workspaceId: string, days: ProposedDayApplyItem[], replace: boolean) => Promise -} - -/** - * Applies a plan (persists proposed days as real planned days). - * Returns the full response including collision info, rejections, and created counts. - */ -export function usePlanApply(): UsePlanApplyResult { - const [response, setResponse] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - - const apply = useCallback( - async (workspaceId: string, days: ProposedDayApplyItem[], replace: boolean) => { - setIsLoading(true) - setError(null) - try { - const result = await api.applyPlan(workspaceId, days, replace) - setResponse(result) - return result - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Kunde inte använda plan' - setError(message) - setResponse(null) - throw err - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { response, isLoading, error, apply } -} diff --git a/frontend/src/pages/Dashboard.test.tsx b/frontend/src/pages/Dashboard.test.tsx index 2c08f58..5bc8611 100644 --- a/frontend/src/pages/Dashboard.test.tsx +++ b/frontend/src/pages/Dashboard.test.tsx @@ -17,7 +17,6 @@ import { makeOptimizationTipsResult, makeLeaveWishesResult, makePlanProposalResult, - makePlanApplyResult, makeTip, } from '../../tests/fixtures' @@ -124,7 +123,6 @@ describe('Dashboard', () => { vi.mocked(hooks.useOptimizationTips).mockReturnValue(makeOptimizationTipsResult()) vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult()) vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult()) - vi.mocked(hooks.usePlanApply).mockReturnValue(makePlanApplyResult()) }) it('renders loading spinner initially', () => { diff --git a/frontend/src/services/api.test.ts b/frontend/src/services/api.test.ts index f6d7bbc..552ed1d 100644 --- a/frontend/src/services/api.test.ts +++ b/frontend/src/services/api.test.ts @@ -1432,78 +1432,6 @@ describe('endpoint functions (representative coverage)', () => { }) }) - it('applyPlan maps rejections to messageKey/data (§3.5)', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - accepted: false, - created_count: 0, - skipped_identical_count: 0, - skipped_free_count: 0, - replaced_count: 0, - collisions: [], - rejections: [ - { - message_key: 'planApply.rejection.insufficientBalance', - data: { tier: 'sjukpenning', needed: 2, remaining: 1 }, - }, - ], - created_days: [], - }), - }) - - const result = await api.applyPlan('ws1', [], false) - - expect(result.accepted).toBe(false) - expect(result.rejections).toEqual([ - { - messageKey: 'planApply.rejection.insufficientBalance', - data: { tier: 'sjukpenning', needed: 2, remaining: 1 }, - }, - ]) - }) - - it('applyPlan maps a populated collisions array to camelCase (§3.5)', async () => { - // The only other applyPlan mapping test passes collisions: [], so - // existing_day_type/proposed_day_type were never checked against real - // data — this exercises the actual field renames. - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - accepted: false, - created_count: 0, - skipped_identical_count: 0, - skipped_free_count: 0, - replaced_count: 0, - collisions: [ - { - date: '2026-06-15', - parent_personal_data_uuid: 'pd1', - child_id: 'child1', - existing_day_type: 'vacation', - proposed_day_type: 'sjukpenning', - }, - ], - rejections: [], - created_days: [], - }), - }) - - const result = await api.applyPlan('ws1', [], false) - - expect(result.collisions).toEqual([ - { - date: '2026-06-15', - parentPersonalDataUuid: 'pd1', - childId: 'child1', - existingDayType: 'vacation', - proposedDayType: 'sjukpenning', - }, - ]) - }) - it('syncPlan posts the workspace id and maps the nested proposal/applyResult/changes', async () => { fetchMock.mockResolvedValue({ ok: true, @@ -1524,8 +1452,21 @@ describe('endpoint functions (representative coverage)', () => { skipped_identical_count: 0, skipped_free_count: 0, replaced_count: 1, - collisions: [], - rejections: [], + collisions: [ + { + date: '2026-06-15', + parent_personal_data_uuid: 'pd1', + child_id: 'child1', + existing_day_type: 'vacation', + proposed_day_type: 'sjukpenning', + }, + ], + rejections: [ + { + message_key: 'planApply.rejection.insufficientBalance', + data: { tier: 'sjukpenning', needed: 2, remaining: 1 }, + }, + ], created_days: [], }, changes: [ @@ -1563,6 +1504,21 @@ describe('endpoint functions (representative coverage)', () => { expect(lastRequestBody()).toEqual({ workspace_id: 'ws1' }) expect(result.applyResult.createdCount).toBe(2) expect(result.applyResult.replacedCount).toBe(1) + expect(result.applyResult.collisions).toEqual([ + { + date: '2026-06-15', + parentPersonalDataUuid: 'pd1', + childId: 'child1', + existingDayType: 'vacation', + proposedDayType: 'sjukpenning', + }, + ]) + expect(result.applyResult.rejections).toEqual([ + { + messageKey: 'planApply.rejection.insufficientBalance', + data: { tier: 'sjukpenning', needed: 2, remaining: 1 }, + }, + ]) expect(result.changes).toHaveLength(2) expect(result.changes[0]).toEqual({ kind: 'retyped', diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index fd7fce5..36ece1a 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -892,28 +892,6 @@ function toPlanApplyResponse(raw: RawPlanApplyResponse): import('@/types').PlanA } } -export async function applyPlan( - workspaceId: string, - days: import('@/types').ProposedDayApplyItem[], - replace: boolean, -): Promise { - const raw = (await apiFetch('/plan/apply', { - method: 'POST', - body: JSON.stringify({ - workspace_id: workspaceId, - days: days.map((d) => ({ - date: d.date, - parent_personal_data_uuid: d.parentPersonalDataUuid, - child_id: d.childId, - day_type: d.dayType, - is_dubbeldag: d.isDubbeldag ?? false, - })), - replace, - }), - })) as RawPlanApplyResponse - return toPlanApplyResponse(raw) -} - interface RawPlanChange { kind: string date: string diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 2d93f2d..95c8605 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -121,12 +121,6 @@ export interface ProposedDayApplyItem { isDubbeldag?: boolean } -export interface PlanApplyRequest { - workspaceId: string - days: ProposedDayApplyItem[] - replace: boolean -} - export interface DayCollision { date: string parentPersonalDataUuid: string diff --git a/frontend/tests/fixtures/hooks.ts b/frontend/tests/fixtures/hooks.ts index ad99e60..0f9e6f5 100644 --- a/frontend/tests/fixtures/hooks.ts +++ b/frontend/tests/fixtures/hooks.ts @@ -21,7 +21,6 @@ import type { useOptimizationTips } from '@/hooks/useOptimizationTips' import type { useDayOverrides } from '@/hooks/useDayOverrides' import type { useLeaveWishes } from '@/hooks/useLeaveWishes' import type { usePlanProposal } from '@/hooks/usePlanProposal' -import type { usePlanApply } from '@/hooks/usePlanApply' import type { usePlanSync } from '@/hooks/usePlanSync' import type { useVacationDays } from '@/hooks/useVacationDays' import type { useChildSettings } from '@/hooks/useChildSettings' @@ -198,18 +197,6 @@ export function makePlanProposalResult( } } -export function makePlanApplyResult( - overrides: Partial> = {}, -): ReturnType { - return { - response: null, - isLoading: false, - error: null, - apply: vi.fn(), - ...overrides, - } -} - export function makePlanSyncResult( overrides: Partial> = {}, ): ReturnType {