fix(calendar): refresh recorded dates after follow-up changes

This commit is contained in:
cesnimda
2026-11-04 13:34:16 +01:00
parent e70c7098cd
commit c0ef4171d5
4 changed files with 48 additions and 4 deletions
@@ -0,0 +1,41 @@
import React from 'react';
import '@testing-library/jest-dom';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import CalendarPage from './views/CalendarPage';
import { I18nProvider } from './i18n/I18nProvider';
import { ToastProvider } from './toast';
import { api } from './api';
jest.mock('./api', () => ({ api: { get: jest.fn(), patch: jest.fn() }, getApiErrorMessage: (_error: unknown, fallback: string) => fallback }));
const mocked = api as jest.Mocked<typeof api>;
const calendarReads = () => mocked.get.mock.calls.filter(([url]) => url === '/calendar/agenda').length;
beforeEach(() => { localStorage.clear(); jest.clearAllMocks(); jest.useFakeTimers({ now: new Date('2026-11-04T12:00:00Z') }); });
afterEach(() => jest.useRealTimers());
test('successful scheduling and clearing refresh the adjacent calendar without resetting its view; failed writes do not refresh it', async () => {
let followUpAt: string | null = null;
const job = () => ({ id: 42, jobTitle: 'Synthetic nurse', company: { id: 1, name: 'Synthetic employer' }, status: 'Waiting', needsFollowUp: true, followUpAt });
mocked.get.mockImplementation(url => Promise.resolve({ data: url === '/jobapplications/reminders' ? [job()] : url === '/calendar/agenda' && followUpAt ? [{ jobApplicationId: 42, jobTitle: job().jobTitle, companyName: job().company.name, kind: 'follow-up', date: followUpAt, recordedAt: followUpAt }] : [] } as never));
mocked.patch.mockImplementation((_url, body) => { followUpAt = (body as { followUpAt: string | null }).followUpAt; return Promise.resolve({ data: {} } as never); });
render(<I18nProvider><ToastProvider><MemoryRouter><CalendarPage /></MemoryRouter></ToastProvider></I18nProvider>);
const reminder = await screen.findByRole('article', { name: 'Synthetic nurse' });
expect(calendarReads()).toBe(1);
fireEvent.click(within(reminder).getByRole('button', { name: '+3 days: set follow-up for Synthetic nurse' }));
expect(await screen.findByRole('link', { name: /Synthetic nurse/ })).toBeVisible();
expect(mocked.patch).toHaveBeenCalledWith('/jobapplications/42/followup', { followUpAt: '2026-11-07' });
expect(calendarReads()).toBe(2);
fireEvent.click(screen.getByRole('button', { name: 'Month' }));
expect(screen.getByRole('table', { name: 'Month' })).toBeVisible();
fireEvent.click(within(reminder).getByRole('button', { name: 'Clear follow-up: Synthetic nurse' }));
await waitFor(() => expect(calendarReads()).toBe(3));
await waitFor(() => expect(screen.getByRole('table', { name: 'Month' })).toBeVisible());
expect(screen.getByRole('button', { name: 'Month' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('combobox', { name: /^Selected month/ })).toHaveTextContent('November 2026');
mocked.patch.mockRejectedValueOnce({ response: { status: 503 } });
fireEvent.click(within(reminder).getByRole('button', { name: '+3 days: set follow-up for Synthetic nurse' }));
await waitFor(() => expect(mocked.patch).toHaveBeenCalledTimes(3));
await waitFor(() => expect(within(reminder).getByRole('button', { name: '+3 days: set follow-up for Synthetic nurse' })).toBeEnabled());
expect(calendarReads()).toBe(3);
});
@@ -16,7 +16,7 @@ function calendarDate(value: string) {
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value ? date : null;
}
export default function CalendarAgenda() {
export default function CalendarAgenda({ refreshToken = 0 }: { refreshToken?: number }) {
const { language, t } = useI18n();
const preferenceKey = getUserScopedStorageKey('jobtracker.calendarView');
const [view, setView] = useState<'agenda' | 'month'>(() => { try { return localStorage.getItem(preferenceKey) === 'month' ? 'month' : 'agenda'; } catch { return 'agenda'; } });
@@ -32,7 +32,7 @@ export default function CalendarAgenda() {
if (!Array.isArray(response.data) || response.data.some(item => !Number.isSafeInteger(item.jobApplicationId) || item.jobApplicationId <= 0 || typeof item.jobTitle !== 'string' || !['deadline', 'follow-up'].includes(item.kind) || typeof item.recordedAt !== 'string' || !calendarDate(item.date) || item.date < from || item.date > to)) throw new Error('Invalid calendar response');
return response.data;
}, [from, to]);
const resource = useViewResource(load, { initialData: [] as CalendarItem[], errorMessage: '', deps: [from, to] });
const resource = useViewResource(load, { initialData: [] as CalendarItem[], errorMessage: '', deps: [from, to, refreshToken] });
const locale = language === 'nb' ? 'nb-NO' : 'en-GB';
const formatDate = (date: string) => new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone: 'UTC' }).format(calendarDate(date)!);
const byDay = useMemo(() => {
@@ -106,7 +106,7 @@ function ReminderSection({ title, items, disabled, onOpen, onSetFollowUp }: { ti
);
}
export default function RemindersView() {
export default function RemindersView({ onDatesChanged }: { onDatesChanged?: () => void }) {
const navigate = useNavigate();
const { toast } = useToast();
const { t } = useI18n();
@@ -145,6 +145,7 @@ export default function RemindersView() {
setFailure(null);
try {
await api.patch(`/jobapplications/${job.id}/followup`, { followUpAt: date });
onDatesChanged?.();
toast(daysFromNow === null ? t("remindersFollowUpCleared") : t("remindersFollowUpSet"), "success");
await remindersResource.reload();
} catch (error) {
+3 -1
View File
@@ -1,7 +1,9 @@
import { Stack } from '@mui/material';
import { useState } from 'react';
import CalendarAgenda from '../components/CalendarAgenda';
import RemindersView from '../components/RemindersView';
export default function CalendarPage() {
return <Stack spacing={3} sx={{ minWidth: 0 }}><CalendarAgenda /><RemindersView /></Stack>;
const [calendarRefresh, setCalendarRefresh] = useState(0);
return <Stack spacing={3} sx={{ minWidth: 0 }}><CalendarAgenda refreshToken={calendarRefresh} /><RemindersView onDatesChanged={() => setCalendarRefresh(value => value + 1)} /></Stack>;
}