Skip to content

Calendar (operations)

Routes and navigation

  • Route: /calendar (same org-only area as tasks; in platform view the route redirects to the dashboard).
  • Access from the left sidebar (Calendar icon) and shortcut in the right bar (calendar button; hidden in platform view).

API

  • List by visible range: GET /operations/events?startDate=&endDate= (ISO). Bounds come from FullCalendar’s datesSet callback (range aligned with Month / Week / Agenda).
  • Create: POST /operations/events with body validated by CreateEventSchema on the backend (optional tagIds and attachmentIds).
  • Detail and edit: clicking an event opens a dialog using GET /operations/events/:id; Edit sends PATCH /operations/events/:id (UpdateEventSchema). In create/edit, name (title), type, and pills sit in a fixed header; the scrollable body holds description and EventTagsAttachments (EventScheduleFields / EventLocationField). EventFormFields composes header + EventFormDescriptionSection. Map eventResponseToFormValues + tests in event-response-to-form-values.test.ts.

Pills (edit) and detail

  • In create and edit, type includes values from EVENT_TYPE_PRESETS plus a circular + button to define a custom type (free text sent as type to the API). Dates, Location, Tags, and Attachments pills show or hide each block (event-dialog-sections.ts). When an event loads, sections that have data usually start visible (eventDialogSectionsFromEvent).
  • In read-only detail there are no pills: schedule, description (HTML via TipTap with RichTextReadOnly), and location only if there is a value; EventDetailFilledExtras adds tags and attachments when present (files with Open). Create/edit use the same RichTextEditor as tasks. In create, when the description is empty, EventFormDescriptionSection shows the shared dashed pill trigger with Pencil + placeholder (dialog-description-collapsed-trigger-classes.ts); after content exists, the section stays in editor form.
  • tagIds / attachmentIds on create/update; catalog listTags, storage (useFilesLibraryDialogStore + useFileOperations). In create and edit, the Etiquetas block uses the same OperationTagsField as tasks/projects (chips, + opens the portal overlay to toggle tags, inline Crear etiqueta nueva via useOperationTagCreate + OperationTagCreateFields, Listo (n)) — not a dropdown.
  • Helpers: eventLinkedFilesFromEvent, resolveEventFormTags; tests event-linked-files-from-event.test.ts, resolve-event-form-tags.test.ts, event-dialog-sections.test.ts.

Implementation

  • UI: FullCalendar v6 (@fullcalendar/react + daygrid, timegrid, list, interaction). Theme in calendar-shell.css, aligned with Paneldaramex (calendar-theme.css): soft cell borders, month rows with height driven by content (month view only: height: auto on scrollgrid-sync-table; not applied to week so the time grid stays intact), and no cap on events per day (dayMaxEvents: false) in month. Month uses height="auto"; week uses height="100%" and a 24 h grid (slotMinTime 00:00 / slotMaxTime 24:00) with slotDuration / snapDuration 15 minutes (minimum resize/move step and slot height), slotLabelInterval 1 h for axis labels, and scrollTime 07:00; .fc-timeGridWeek-view rules to fill the harness. The dashboard Outlet wrapper uses h-full min-h-0 so height: 100% resolves; week mode adds daramex-fc--time-grid-week so FullCalendar’s React root div participates in the flex column. CalendarGrid calls updateSize() after changeView (double requestAnimationFrame) and uses a ResizeObserver so FC remeasures when the flex layout settles; CSS adds min-height: min(70vh, 56rem) on the week shell and fc-scrollgrid so the slot grid stays visible if % heights still resolve to zero inside the dashboard scroll region. Agenda does not use FullCalendar’s listWeek: it is the layout styled like Paneldaramex (AgendaView + AgendaDayHeader + AgendaEventCard), calendar week Monday–Sunday (agenda-week-range.ts), list of days with API events grouped by day (group-events-by-agenda-days). Previous/next/today in agenda moves that weekly window; toolbar title is the day range for that week (formatAgendaToolbarTitle). In the header, month, week, and agenda share the same fixed title width as month so chevrons do not shift when changing period.
  • Data: TanStack Query in useCalendarEventsQuery; DTOs map to FullCalendar entries in mapEventsToFullCalendar (including multi-day and all-day events: exclusive end per FullCalendar’s contract). In week view, allDaySlot is on: all-day and multi-day timed events (mapped to allDay) in the top band; the “all-day” axis label is hidden with allDayText="". Start time for multi-day events remains in extendedProps / CalendarEventContent.
  • Creation: dialog with shadcn (Dialog, ToggleGroup, Checkbox, etc.) and useCreateEventMutation, which invalidates ['operations', 'events']. Default date/time: now → +1 h (buildCreateEventFormDefaults); if the user picked a day in month (local midnight), that day is kept with the current time; in week, a click with a time preserves that time. Drag-select on month or week (selectable, selectMirror, selectOverlap, selectMinDistance) opens the same dialog with buildCreateEventFormDefaultsFromSelection (FC’s exclusive end → inclusive all-day last day or timed end instant). Operations module tags load with useCalendarOperationTags (['operations', 'tags']).
  • Month and week views: events are draggable (editable + eventDrop on FullCalendar). resolveCalendarEventDropRange maps the drop to API dates: in week time grid, timed events use FullCalendar’s new start/end (move time and preserve duration, Google Calendar–style). In month and for all-day rows (including multi-day timed in the all-day band), only the local calendar day delta is applied via shiftEventRangeByCalendarDays; clock times (or multi-day span) stay aligned with the original event. Resize is week-only (eventDurationEditable, eventResizableFromStart, eventResize): drag top/bottom to change start/end. Month does not allow resize (drag/move only). mapFullCalendarResizeToApiRange maps FC’s range to PATCH dates for single-day timed events. All-day lane bars (true all-day and multi-day timed) use durationEditable: false in map-events-to-fullcalendar so they cannot be resized in week view. Theme uses overflow: visible on .fc-timegrid-event so week resize handles are not clipped. Updates use useUpdateEventMutation (PATCH); on API error the drag or resize is reverted.

Tests

  • apps/panel/src/features/calendar/lib/map-events-to-fullcalendar.test.ts covers mapping timed and all-day events; agenda-week-range.test.ts and group-events-by-agenda-days.test.ts cover the agenda view; shift-event-by-calendar-days.test.ts covers calendar-day shifting; resolve-calendar-event-drop-range.test.ts covers week vs month drag mapping; map-fullcalendar-resize-to-api-range.test.ts covers resize → API dates; build-create-event-form-defaults.test.ts includes buildCreateEventFormDefaultsFromSelection.