From 9ac67933d4435db49f4708615191f7ce42ca8800 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Fri, 18 Sep 2026 01:12:26 +0200 Subject: [PATCH] Drop the direct apply path the calendar no longer uses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applying a plan by itself stopped being something the interface does once every edit began recomputing and saving in one call. The hook that posted straight to the apply endpoint had no caller left, nor did its request type or its fixture. The response decoder stays, since the sync response embeds the same object, and the two tests being deleted were the only ones checking how its collision and rejection fields are renamed — that coverage moved onto the sync test rather than leaving with the code it happened to live next to. --- frontend/src/hooks/index.ts | 1 - frontend/src/hooks/usePlanApply.test.ts | 149 ------------------------ frontend/src/hooks/usePlanApply.ts | 42 ------- frontend/src/pages/Dashboard.test.tsx | 2 - frontend/src/services/api.test.ts | 104 +++++------------ frontend/src/services/api.ts | 22 ---- frontend/src/types/api.ts | 6 - frontend/tests/fixtures/hooks.ts | 13 --- 8 files changed, 30 insertions(+), 309 deletions(-) delete mode 100644 frontend/src/hooks/usePlanApply.test.ts delete mode 100644 frontend/src/hooks/usePlanApply.ts 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 {