/** * CalendarDetail - right column detail panel for the Calendar plugin. * * Shows entry details (title, type, status, priority, dates, location, * description, calendar info), subtask checklist for tasks, linked entities, * and action buttons (Edit, Delete). * Pattern follows DMS FileDetails. */ import React, { useEffect, useState } from 'react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { createSubtask, getEntry, updateSubtask, type Calendar, type CalendarEntry, type Subtask, } from '@/api/calendar'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Calendar as CalendarIcon, Pencil, Trash2, X } from 'lucide-react'; function formatDate(dateStr: string | null | undefined): string { if (!dateStr) return '-'; try { const d = new Date(dateStr); return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch { return dateStr; } } function priorityClass(p: string): string { switch (p) { case 'high': return 'bg-danger-100 text-danger-800'; case 'low': return 'bg-secondary-100 text-secondary-700'; default: return 'bg-warning-100 text-warning-800'; } } export interface CalendarDetailProps { entry: CalendarEntry | null; calendar: Calendar | null; onEdit: () => void; onDelete: () => void; onClose: () => void; onEntryChanged: (entry: CalendarEntry) => void; } export function CalendarDetail({ entry, calendar, onEdit, onDelete, onClose, onEntryChanged, }: CalendarDetailProps) { const { t } = useTranslation(); const [subtasks, setSubtasks] = useState(entry?.subtasks ?? []); const [loadingSubtasks, setLoadingSubtasks] = useState(false); const [newSubtaskTitle, setNewSubtaskTitle] = useState(''); const [subtaskError, setSubtaskError] = useState(null); // Load subtasks if entry is a task and subtasks are missing useEffect(() => { if (!entry) { setSubtasks([]); return; } if (entry.entry_type !== 'task') { setSubtasks([]); return; } if ((entry.subtasks?.length ?? 0) > 0) { setSubtasks(entry.subtasks ?? []); return; } let cancelled = false; setLoadingSubtasks(true); getEntry(entry.id) .then((full) => { if (cancelled) return; setSubtasks(full.subtasks ?? []); onEntryChanged(full); }) .catch((e: Error) => { if (!cancelled) setSubtaskError(e.message); }) .finally(() => { if (!cancelled) setLoadingSubtasks(false); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [entry?.id, entry?.entry_type, entry?.subtasks]); const handleAddSubtask = async () => { if (!entry) return; const title = newSubtaskTitle.trim(); if (!title) return; setSubtaskError(null); try { const created = await createSubtask(entry.id, { title }); setSubtasks((cur) => [...cur, created]); setNewSubtaskTitle(''); } catch (e) { setSubtaskError((e as Error).message ?? t('calendar.errorSave')); } }; const handleToggleSubtask = async (sub: Subtask) => { if (!entry) return; setSubtaskError(null); const prev = sub.done; setSubtasks((cur) => cur.map((s) => (s.id === sub.id ? { ...s, done: !prev } : s))); try { const updated = await updateSubtask(entry.id, sub.id, { done: !prev }); setSubtasks((cur) => cur.map((s) => (s.id === sub.id ? updated : s))); } catch (e) { setSubtasks((cur) => cur.map((s) => (s.id === sub.id ? { ...s, done: prev } : s))); setSubtaskError((e as Error).message ?? t('calendar.errorSave')); } }; if (!entry) { return (
); } const isTask = entry.entry_type === 'task'; return (
{/* Header with close button */}

{entry.title}

{/* Scrollable content */}
{/* Badges */}
{entry.entry_type} {t(`calendar.status.${entry.status}`)} {entry.priority}
{/* Action buttons */}
{/* Metadata section */}

{t('calendar.detail.title')}

{t('calendar.detail.type')}
{entry.entry_type}
{t('calendar.detail.status')}
{t(`calendar.status.${entry.status}`)}
{t('calendar.detail.priority')}
{entry.priority}
{t('calendar.detail.start')}
{formatDate(entry.start_at)}
{t('calendar.detail.end')}
{formatDate(entry.end_at)}
{entry.location && (
{t('calendar.detail.location')}
{entry.location}
)} {calendar && (
{t('calendar.detail.calendar')}
)}
{/* Description */} {entry.description && (

{t('calendar.detail.description')}

{entry.description}

)} {/* Subtasks (for tasks) */} {isTask && (

{t('calendar.detail.subtasks')}

{loadingSubtasks && (
{t('common.loading')}
)}
    {subtasks.length === 0 && !loadingSubtasks && (
  • {t('calendar.detail.subtasks.empty')}
  • )} {subtasks.map((sub) => (
  • handleToggleSubtask(sub)} className="mt-1 h-4 w-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500" data-testid={`calendar-detail-subtask-toggle-${sub.id}`} /> {sub.title}
  • ))}
{ e.preventDefault(); void handleAddSubtask(); }} className="flex gap-2" data-testid="calendar-detail-subtask-add-form" > setNewSubtaskTitle(e.target.value)} placeholder={t('calendar.detail.subtasks.addPlaceholder')} data-testid="calendar-detail-subtask-add-input" />
{subtaskError && (
{subtaskError}
)}
)} {/* Linked entities */} {entry.links && entry.links.length > 0 && (

{t('calendar.detail.links')}

    {entry.links.map((link) => (
  • {link.entity_type} {link.entity_id}
  • ))}
)} {(!entry.links || entry.links.length === 0) && (

{t('calendar.detail.links')}

{t('calendar.detail.noLinks')}

)}
); } export default CalendarDetail;