Drop the direct apply path the calendar no longer uses

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.
This commit is contained in:
2026-09-18 01:12:26 +02:00
parent d245390886
commit 9ac67933d4
8 changed files with 30 additions and 309 deletions
-1
View File
@@ -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'
-149
View File
@@ -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)
})
})
})
-42
View File
@@ -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<PlanApplyResponse>
}
/**
* 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<PlanApplyResponse | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(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 }
}
-2
View File
@@ -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', () => {
+30 -74
View File
@@ -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',
-22
View File
@@ -892,28 +892,6 @@ function toPlanApplyResponse(raw: RawPlanApplyResponse): import('@/types').PlanA
}
}
export async function applyPlan(
workspaceId: string,
days: import('@/types').ProposedDayApplyItem[],
replace: boolean,
): Promise<import('@/types').PlanApplyResponse> {
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
-6
View File
@@ -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