7350739554
- 8 calendar components (MonthView, KanbanBoard, AppointmentModal, TaskDetailPanel, IcsControls, ResourceBooking, SharingSettings + API client) - 2 pages (/calendar, /calendar/kanban) + zustand store - 17 vitest tests (MonthView 5, KanbanBoard 6, AppointmentModal 6) all passing - i18n: calendar namespace in en/de (104 lines each, +exportSuccess key) - TS strict mode pass, npm run build pass
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
/**
|
|
* Calendar Kanban page — three task columns with drag & drop status updates.
|
|
*/
|
|
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { KanbanBoard, type KanbanStatus } from '@/components/calendar/KanbanBoard';
|
|
import { TaskDetailPanel } from '@/components/calendar/TaskDetailPanel';
|
|
import {
|
|
fetchKanbanBoard,
|
|
updateEntry,
|
|
type CalendarEntry,
|
|
type KanbanBoard as KanbanBoardData,
|
|
} from '@/api/calendar';
|
|
|
|
const EMPTY_BOARD: KanbanBoardData = {
|
|
open: [],
|
|
in_progress: [],
|
|
done: [],
|
|
cancelled: [],
|
|
};
|
|
|
|
export function CalendarKanbanPage() {
|
|
const { t } = useTranslation();
|
|
const [board, setBoard] = useState<KanbanBoardData>(EMPTY_BOARD);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedTask, setSelectedTask] = useState<CalendarEntry | null>(null);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const data = await fetchKanbanBoard();
|
|
setBoard(data);
|
|
} catch (e) {
|
|
setError((e as Error).message ?? t('calendar.errorLoad'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [t]);
|
|
|
|
useEffect(() => {
|
|
void load();
|
|
}, [load]);
|
|
|
|
const handleMove = async (entry: CalendarEntry, target: KanbanStatus) => {
|
|
// Optimistic update
|
|
const previous = board;
|
|
const next: KanbanBoardData = {
|
|
open: board.open.filter((e) => e.id !== entry.id),
|
|
in_progress: board.in_progress.filter((e) => e.id !== entry.id),
|
|
done: board.done.filter((e) => e.id !== entry.id),
|
|
cancelled: board.cancelled.filter((e) => e.id !== entry.id),
|
|
};
|
|
next[target] = [...next[target], { ...entry, status: target }];
|
|
setBoard(next);
|
|
try {
|
|
const updated = await updateEntry(entry.id, { status: target });
|
|
setBoard((cur) => {
|
|
const list = cur[target].map((e) => (e.id === updated.id ? updated : e));
|
|
return { ...cur, [target]: list };
|
|
});
|
|
} catch (e) {
|
|
setError((e as Error).message ?? t('calendar.errorSave'));
|
|
setBoard(previous);
|
|
}
|
|
};
|
|
|
|
const handleTaskChanged = (updated: CalendarEntry) => {
|
|
setSelectedTask(updated);
|
|
setBoard((cur) => {
|
|
const mapStatus = (s: string): KanbanStatus =>
|
|
s === 'open' || s === 'in_progress' || s === 'done' || s === 'cancelled'
|
|
? s
|
|
: 'open';
|
|
const target = mapStatus(updated.status);
|
|
const strip = (col: CalendarEntry[]) => col.filter((e) => e.id !== updated.id);
|
|
return {
|
|
open: target === 'open' ? [...strip(cur.open), updated] : strip(cur.open),
|
|
in_progress:
|
|
target === 'in_progress' ? [...strip(cur.in_progress), updated] : strip(cur.in_progress),
|
|
done: target === 'done' ? [...strip(cur.done), updated] : strip(cur.done),
|
|
cancelled:
|
|
target === 'cancelled' ? [...strip(cur.cancelled), updated] : strip(cur.cancelled),
|
|
};
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 max-w-7xl mx-auto" data-testid="kanban-page">
|
|
<h1 className="text-2xl font-bold text-secondary-900 mb-4">
|
|
{t('calendar.kanbanTitle')}
|
|
</h1>
|
|
|
|
{error && (
|
|
<div
|
|
className="mb-3 text-sm text-danger-700 bg-danger-50 border border-danger-200 rounded-md p-2"
|
|
data-testid="kanban-error"
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<KanbanBoard
|
|
board={board}
|
|
loading={loading}
|
|
onMove={handleMove}
|
|
onSelect={(entry) => setSelectedTask(entry)}
|
|
/>
|
|
|
|
{selectedTask && (
|
|
<TaskDetailPanel
|
|
entry={selectedTask}
|
|
onClose={() => setSelectedTask(null)}
|
|
onChanged={handleTaskChanged}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default CalendarKanbanPage;
|