feat: add task management features to calendar

This commit is contained in:
Linus Rath
2026-03-21 20:45:22 +01:00
parent 64c3d7e384
commit 30284859c7
20 changed files with 1190 additions and 46 deletions
+95 -1
View File
@@ -23,6 +23,9 @@ import { CalendarMonthView } from "@/components/calendar/calendar-month-view";
import { CalendarWeekView } from "@/components/calendar/calendar-week-view";
import { CalendarDayView } from "@/components/calendar/calendar-day-view";
import { CalendarAgendaView } from "@/components/calendar/calendar-agenda-view";
import { TaskListView } from "@/components/calendar/task-list-view";
import { TaskToolbar } from "@/components/calendar/task-toolbar";
import { TaskModal } from "@/components/calendar/task-modal";
import { MiniCalendar } from "@/components/calendar/mini-calendar";
import { CalendarSidebarPanel } from "@/components/calendar/calendar-sidebar-panel";
import { EventModal, type PendingEventPreview } from "@/components/calendar/event-modal";
@@ -35,6 +38,7 @@ import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
import { InlineAppView } from "@/components/layout/inline-app-view";
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
import { ResizeHandle } from "@/components/layout/resize-handle";
import { useTaskStore } from "@/stores/task-store";
import { cn } from "@/lib/utils";
import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types";
import { getUserParticipantId } from "@/lib/calendar-participants";
@@ -63,7 +67,8 @@ export default function CalendarPage() {
setSelectedDate, setViewMode, toggleCalendarVisibility, updateCalendar,
refreshAllSubscriptions,
} = useCalendarStore();
const { firstDayOfWeek, timeFormat, showWeekNumbers } = useSettingsStore();
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar } = useSettingsStore();
const taskStore = useTaskStore();
const { identities } = useIdentityStore();
const normalizedViewMode = isCalendarViewMode(viewMode) ? viewMode : "month";
@@ -83,6 +88,8 @@ export default function CalendarPage() {
const [detailEvent, setDetailEvent] = useState<CalendarEvent | null>(null);
const [detailAnchorRect, setDetailAnchorRect] = useState<DOMRect | null>(null);
const [pendingPreview, setPendingPreview] = useState<PendingEventPreview | null>(null);
const [showTaskModal, setShowTaskModal] = useState(false);
const [editTask, setEditTask] = useState<import("@/lib/jmap/types").CalendarTask | null>(null);
const hasFetched = useRef(false);
// Sidebar resize state
@@ -166,9 +173,18 @@ export default function CalendarPage() {
end: format(addDays(agendaStart, 30), "yyyy-MM-dd'T'23:59:59"),
};
}
case "tasks":
return null;
}
}, [selectedDate, normalizedViewMode, firstDayOfWeek]);
// Fetch tasks when tasks view is active or when tasks are shown on calendar grid
useEffect(() => {
if (client && enableCalendarTasks && (normalizedViewMode === "tasks" || showTasksOnCalendar)) {
taskStore.fetchTasks(client);
}
}, [client, enableCalendarTasks, normalizedViewMode, showTasksOnCalendar]);
useEffect(() => {
if (client && calendars.length > 0 && dateRange) {
fetchEvents(client, dateRange.start, dateRange.end);
@@ -182,6 +198,7 @@ export default function CalendarPage() {
case "week": next = subWeeks(selectedDate, 1); break;
case "day": next = subDays(selectedDate, 1); break;
case "agenda": next = subMonths(selectedDate, 1); break;
case "tasks": return;
}
setSelectedDate(next);
setMiniMonth(next);
@@ -194,6 +211,7 @@ export default function CalendarPage() {
case "week": next = addWeeks(selectedDate, 1); break;
case "day": next = addDays(selectedDate, 1); break;
case "agenda": next = addMonths(selectedDate, 1); break;
case "tasks": return;
}
setSelectedDate(next);
setMiniMonth(next);
@@ -254,6 +272,34 @@ export default function CalendarPage() {
setShowEventModal(true);
}, []);
const openCreateTaskModal = useCallback(() => {
setEditTask(null);
setShowTaskModal(true);
}, []);
const openEditTaskModal = useCallback((task: import("@/lib/jmap/types").CalendarTask) => {
setEditTask(task);
setShowTaskModal(true);
}, []);
const handleSaveTask = useCallback(async (data: Partial<import("@/lib/jmap/types").CalendarTask>) => {
if (!client) return;
if (editTask) {
await taskStore.updateTask(client, editTask.id, data);
} else {
await taskStore.createTask(client, data);
}
setShowTaskModal(false);
setEditTask(null);
}, [client, editTask, taskStore]);
const handleDeleteTask = useCallback(async (id: string) => {
if (!client) return;
await taskStore.deleteTask(client, id);
setShowTaskModal(false);
setEditTask(null);
}, [client, taskStore]);
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const closeDetail = useCallback(() => {
@@ -608,6 +654,7 @@ export default function CalendarPage() {
case "w": setViewMode("week"); break;
case "d": setViewMode("day"); break;
case "a": setViewMode("agenda"); break;
case "k": if (enableCalendarTasks) setViewMode("tasks"); break;
case "n": openCreateModal(); break;
}
};
@@ -668,6 +715,8 @@ export default function CalendarPage() {
timeFormat={timeFormat}
isMobile={isMobile}
pendingPreview={pendingPreview}
tasks={enableCalendarTasks && showTasksOnCalendar ? taskStore.tasks : undefined}
onToggleTaskComplete={(task) => { if (client) taskStore.toggleTaskComplete(client, task); }}
/>
);
case "day":
@@ -683,6 +732,8 @@ export default function CalendarPage() {
timeFormat={timeFormat}
isMobile={isMobile}
pendingPreview={pendingPreview}
tasks={enableCalendarTasks && showTasksOnCalendar ? taskStore.tasks : undefined}
onToggleTaskComplete={(task) => { if (client) taskStore.toggleTaskComplete(client, task); }}
/>
);
case "agenda":
@@ -697,6 +748,33 @@ export default function CalendarPage() {
timeFormat={timeFormat}
/>
);
case "tasks":
return (
<div className="flex flex-col h-full">
<TaskToolbar
filter={taskStore.filter}
showCompleted={taskStore.showCompleted}
onFilterChange={taskStore.setFilter}
onShowCompletedChange={taskStore.setShowCompleted}
onCreateTask={openCreateTaskModal}
/>
<TaskListView
tasks={taskStore.tasks}
calendars={calendars}
selectedCalendarIds={selectedCalendarIds}
filter={taskStore.filter}
showCompleted={taskStore.showCompleted}
onSelectTask={openEditTaskModal}
onToggleComplete={(task) => { if (client) taskStore.toggleTaskComplete(client, task); }}
selectedTaskId={taskStore.selectedTaskId}
onQuickCreate={(title) => {
if (client) {
taskStore.createTask(client, { "@type": "Task", title, progress: "needs-action", calendarIds: { [calendars[0]?.id ?? ""]: true } });
}
}}
/>
</div>
);
}
})();
@@ -792,6 +870,7 @@ export default function CalendarPage() {
calendars={calendars}
selectedCalendarIds={selectedCalendarIds}
onToggleVisibility={toggleCalendarVisibility}
enableCalendarTasks={enableCalendarTasks}
/>
<div
@@ -823,6 +902,21 @@ export default function CalendarPage() {
</div>
)}
{/* Desktop task panel */}
{!isMobile && showTaskModal && (
<div className="w-[400px] border-l border-border flex-shrink-0 overflow-hidden">
<TaskModal
key={editTask?.id ?? 'new-task'}
task={editTask}
calendars={calendars}
onSave={handleSaveTask}
onDelete={handleDeleteTask}
onClose={() => { setShowTaskModal(false); setEditTask(null); }}
isMobile={false}
/>
</div>
)}
{/* Floating Create Event Button (mobile) */}
{isMobile && (
<Button