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:
@@ -1,7 +1,6 @@
|
|||||||
export { useAnalytics } from './useAnalytics'
|
export { useAnalytics } from './useAnalytics'
|
||||||
export { useLeaveWishes } from './useLeaveWishes'
|
export { useLeaveWishes } from './useLeaveWishes'
|
||||||
export { usePlanProposal } from './usePlanProposal'
|
export { usePlanProposal } from './usePlanProposal'
|
||||||
export { usePlanApply } from './usePlanApply'
|
|
||||||
export { usePlanSync } from './usePlanSync'
|
export { usePlanSync } from './usePlanSync'
|
||||||
export { useWorkspaceMembers } from './useWorkspaceMembers'
|
export { useWorkspaceMembers } from './useWorkspaceMembers'
|
||||||
export { useShowWeekNumbers, getLastLeaveType, setLastLeaveType } from './useCalendarPreferences'
|
export { useShowWeekNumbers, getLastLeaveType, setLastLeaveType } from './useCalendarPreferences'
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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 }
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
makeOptimizationTipsResult,
|
makeOptimizationTipsResult,
|
||||||
makeLeaveWishesResult,
|
makeLeaveWishesResult,
|
||||||
makePlanProposalResult,
|
makePlanProposalResult,
|
||||||
makePlanApplyResult,
|
|
||||||
makeTip,
|
makeTip,
|
||||||
} from '../../tests/fixtures'
|
} from '../../tests/fixtures'
|
||||||
|
|
||||||
@@ -124,7 +123,6 @@ describe('Dashboard', () => {
|
|||||||
vi.mocked(hooks.useOptimizationTips).mockReturnValue(makeOptimizationTipsResult())
|
vi.mocked(hooks.useOptimizationTips).mockReturnValue(makeOptimizationTipsResult())
|
||||||
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult())
|
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult())
|
||||||
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult())
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult())
|
||||||
vi.mocked(hooks.usePlanApply).mockReturnValue(makePlanApplyResult())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders loading spinner initially', () => {
|
it('renders loading spinner initially', () => {
|
||||||
|
|||||||
@@ -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 () => {
|
it('syncPlan posts the workspace id and maps the nested proposal/applyResult/changes', async () => {
|
||||||
fetchMock.mockResolvedValue({
|
fetchMock.mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -1524,8 +1452,21 @@ describe('endpoint functions (representative coverage)', () => {
|
|||||||
skipped_identical_count: 0,
|
skipped_identical_count: 0,
|
||||||
skipped_free_count: 0,
|
skipped_free_count: 0,
|
||||||
replaced_count: 1,
|
replaced_count: 1,
|
||||||
collisions: [],
|
collisions: [
|
||||||
rejections: [],
|
{
|
||||||
|
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: [],
|
created_days: [],
|
||||||
},
|
},
|
||||||
changes: [
|
changes: [
|
||||||
@@ -1563,6 +1504,21 @@ describe('endpoint functions (representative coverage)', () => {
|
|||||||
expect(lastRequestBody()).toEqual({ workspace_id: 'ws1' })
|
expect(lastRequestBody()).toEqual({ workspace_id: 'ws1' })
|
||||||
expect(result.applyResult.createdCount).toBe(2)
|
expect(result.applyResult.createdCount).toBe(2)
|
||||||
expect(result.applyResult.replacedCount).toBe(1)
|
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).toHaveLength(2)
|
||||||
expect(result.changes[0]).toEqual({
|
expect(result.changes[0]).toEqual({
|
||||||
kind: 'retyped',
|
kind: 'retyped',
|
||||||
|
|||||||
@@ -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 {
|
interface RawPlanChange {
|
||||||
kind: string
|
kind: string
|
||||||
date: string
|
date: string
|
||||||
|
|||||||
@@ -121,12 +121,6 @@ export interface ProposedDayApplyItem {
|
|||||||
isDubbeldag?: boolean
|
isDubbeldag?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanApplyRequest {
|
|
||||||
workspaceId: string
|
|
||||||
days: ProposedDayApplyItem[]
|
|
||||||
replace: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DayCollision {
|
export interface DayCollision {
|
||||||
date: string
|
date: string
|
||||||
parentPersonalDataUuid: string
|
parentPersonalDataUuid: string
|
||||||
|
|||||||
Vendored
-13
@@ -21,7 +21,6 @@ import type { useOptimizationTips } from '@/hooks/useOptimizationTips'
|
|||||||
import type { useDayOverrides } from '@/hooks/useDayOverrides'
|
import type { useDayOverrides } from '@/hooks/useDayOverrides'
|
||||||
import type { useLeaveWishes } from '@/hooks/useLeaveWishes'
|
import type { useLeaveWishes } from '@/hooks/useLeaveWishes'
|
||||||
import type { usePlanProposal } from '@/hooks/usePlanProposal'
|
import type { usePlanProposal } from '@/hooks/usePlanProposal'
|
||||||
import type { usePlanApply } from '@/hooks/usePlanApply'
|
|
||||||
import type { usePlanSync } from '@/hooks/usePlanSync'
|
import type { usePlanSync } from '@/hooks/usePlanSync'
|
||||||
import type { useVacationDays } from '@/hooks/useVacationDays'
|
import type { useVacationDays } from '@/hooks/useVacationDays'
|
||||||
import type { useChildSettings } from '@/hooks/useChildSettings'
|
import type { useChildSettings } from '@/hooks/useChildSettings'
|
||||||
@@ -198,18 +197,6 @@ export function makePlanProposalResult(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makePlanApplyResult(
|
|
||||||
overrides: Partial<ReturnType<typeof usePlanApply>> = {},
|
|
||||||
): ReturnType<typeof usePlanApply> {
|
|
||||||
return {
|
|
||||||
response: null,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
apply: vi.fn(),
|
|
||||||
...overrides,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makePlanSyncResult(
|
export function makePlanSyncResult(
|
||||||
overrides: Partial<ReturnType<typeof usePlanSync>> = {},
|
overrides: Partial<ReturnType<typeof usePlanSync>> = {},
|
||||||
): ReturnType<typeof usePlanSync> {
|
): ReturnType<typeof usePlanSync> {
|
||||||
|
|||||||
Reference in New Issue
Block a user