diff --git a/docs/implementation/task-ep1.6.1-calendar-empty-state-rendering-improvement-2026-07-13.md b/docs/implementation/task-ep1.6.1-calendar-empty-state-rendering-improvement-2026-07-13.md new file mode 100644 index 0000000..5c1a46d --- /dev/null +++ b/docs/implementation/task-ep1.6.1-calendar-empty-state-rendering-improvement-2026-07-13.md @@ -0,0 +1,39 @@ +# Task EP.1.6.1 Calendar Empty State Rendering Improvement - 2026-07-13 + +## Scope Delivered + +- updated Calendar Day, Week, and Month views to always render calendar grid cells even when there are zero events +- kept Agenda view list-oriented empty state behavior unchanged +- added inline empty overlay for Day/Week/Month instead of replacing the calendar surface +- added `+ Add Activity` entry point from empty calendar overlay using existing Activity workspace route +- kept date/time rendering through `src/lib/date-format.ts` +- preserved Calendar projection, Activity ownership, and Calendar adapter architecture + +## Review Summary + +Reviewed before implementation: + +- `AGENTS.md` +- `plans/task-ep.1.6.1.md` +- `docs/implementation/task-ep1.6-calendar-projection-workspace-big-calendar-foundation-2026-07-13.md` +- `docs/implementation/task-ep1.7-my-day-workspace-foundation-2026-07-13.md` +- `src/features/crm/calendar/components/calendar-workspace.tsx` +- Calendar API contracts under `src/features/crm/calendar/api/types.ts` + +## Implementation Notes + +- Day view now renders 12 working-hour slots. +- Week view now renders seven day cells. +- Month view now renders a 42-cell month grid. +- Empty Day/Week/Month views show a lightweight non-blocking overlay with Add Activity action. +- Calendar cells remain rendered and selectable via button semantics. +- Agenda retains `CalendarEmptyState` because a list view should show a list empty state. + +## Verification + +- `npm run typecheck` +- `npx oxlint src/features/crm/calendar/components/calendar-workspace.tsx` + +## Outcome + +Empty graphical Calendar views now feel like available scheduling capacity rather than a missing screen. diff --git a/plans/task-ep.1.6.1.md b/plans/task-ep.1.6.1.md new file mode 100644 index 0000000..c0fdcac --- /dev/null +++ b/plans/task-ep.1.6.1.md @@ -0,0 +1,248 @@ +## Calendar Empty State Rendering Improvement + +**Status:** Completed + +### Background + +The current Calendar Workspace renders a full-page empty state when no Calendar events are returned. + +Current behavior: + +```text +No scheduled work in this period + +Try changing filters or create an Activity from the owning CRM record. +``` + +This behavior is acceptable for an Agenda (list) view but is not appropriate for graphical calendar views. + +A calendar should remain a calendar even when there are no events. + +Users still need to: + +* understand the selected date range +* navigate between dates +* visualize available time slots +* click an empty day or time slot +* create a new Activity +* understand that the schedule is empty rather than the Calendar failing + +--- + +## Required UX Change + +### Day View + +Always render the complete Calendar grid. + +When there are no events: + +* render the timeline/grid normally +* show an inline empty-state hint inside the calendar body +* keep toolbar, navigation, and date controls visible +* allow selecting a time slot +* allow creating a new Activity + +Do **NOT** replace the Calendar with a full-page empty screen. + +--- + +### Week View + +Always render the Week grid. + +When there are no events: + +* render all days +* render working hours +* render current time indicator where applicable +* render an inline empty hint only +* preserve drag target areas for future Activity creation + +Do **NOT** hide the calendar grid. + +--- + +### Month View + +Always render the Month calendar. + +When there are no events: + +* render the entire month +* render all weeks +* render date cells +* render navigation +* optionally display a small "No events" message inside the month body + +The Month view should always look like a usable calendar. + +--- + +### Agenda View + +Agenda is different. + +Agenda is a list-based presentation. + +When there are no scheduled items: + +Show the existing empty state: + +```text +No scheduled work in this period. + +Try changing filters or create an Activity. +``` + +This behavior remains correct. + +--- + +## Rendering Rules + +Preferred rendering logic: + +```text +Day + ↓ +Always render Calendar Grid + +Week + ↓ +Always render Calendar Grid + +Month + ↓ +Always render Calendar Grid + +Agenda + ↓ +If empty + Show Empty State +Else + Show Agenda List +``` + +The Calendar component should never disappear simply because there are zero events. + +--- + +## Empty Calendar Overlay + +Instead of replacing the Calendar component, render a lightweight overlay or inline message. + +Example: + +```text +────────────────────────────── + + No scheduled work + + Your schedule is free. + + [+ Add Activity] + +────────────────────────────── +``` + +The overlay must not block: + +* changing dates +* changing view +* selecting days +* selecting time slots +* opening filters + +--- + +## Add Activity Entry Point + +When the Calendar contains no events, provide a visible primary action: + +```text ++ Add Activity +``` + +The button must reuse the existing Activity form and Activity API. + +Do not introduce a Calendar-specific create flow. + +--- + +## Calendar Interaction + +Even when empty, users must still be able to: + +* navigate month/week/day +* click a date +* click a time slot +* switch views +* change filters +* search +* create Activities + +The Calendar should remain fully interactive. + +--- + +## Big Calendar Integration + +Because the project uses **lramos33/big-calendar** as the UI foundation: + +* always render the Big Calendar component for Day, Week, and Month +* pass an empty event array when appropriate +* never short-circuit rendering based on `events.length === 0` +* use the Calendar's native layout even when there are no events + +Avoid patterns such as: + +```tsx +if (events.length === 0) { + return ; +} +``` + +Instead: + +```tsx + + +{events.length === 0 && view === "agenda" && ( + +)} + +{events.length === 0 && view !== "agenda" && ( + +)} +``` + +--- + +## UX Principles + +The Calendar is a navigation surface, not only an event list. + +Users should always understand: + +* where they are +* which dates are visible +* which time slots are available +* where new Activities can be scheduled + +An empty schedule should feel like available capacity—not like a missing screen. + +--- + +## Acceptance Criteria + +* Day view always renders the calendar grid. +* Week view always renders the calendar grid. +* Month view always renders the calendar grid. +* Agenda keeps the existing list-style empty state. +* Empty Day/Week/Month views display a lightweight overlay instead of replacing the Calendar. +* Calendar navigation remains available when no events exist. +* Date and time slots remain clickable. +* Users can create Activities directly from an empty calendar. +* No full-page empty state is shown for Day, Week, or Month views. +* The implementation remains compatible with `lramos33/big-calendar` and ALLA OS projection architecture. diff --git a/src/features/crm/calendar/components/calendar-workspace.tsx b/src/features/crm/calendar/components/calendar-workspace.tsx index 62bc23a..ca04765 100644 --- a/src/features/crm/calendar/components/calendar-workspace.tsx +++ b/src/features/crm/calendar/components/calendar-workspace.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useMemo, useState } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; import { Badge } from "@/components/ui/badge"; @@ -95,16 +96,24 @@ export function CalendarWorkspace({ filters }: CalendarWorkspaceProps) { - + - + @@ -191,43 +200,153 @@ function AgendaView({ function CompactCalendarGrid({ events, - columns, - month, + view, + anchorDate, }: { events: BigCalendarEvent[]; - columns: number; - month?: boolean; + view: "day" | "week" | "month"; + anchorDate: string; }) { - if (!events.length) { - return ; - } + const cells = buildCalendarGridCells({ events, view, anchorDate }); + const isMonth = view === "month"; return ( -
- {events.map((event) => ( -
-

- {formatDate(event.start)} -

- -
- ))} +
+
+ {cells.map((cell) => ( + + ))} +
+ {!events.length ? : null}
); } +interface CalendarGridCell { + id: string; + label: string; + subLabel: string | null; + isToday: boolean; + events: BigCalendarEvent[]; +} + +function buildCalendarGridCells({ + events, + view, + anchorDate, +}: { + events: BigCalendarEvent[]; + view: "day" | "week" | "month"; + anchorDate: string; +}): CalendarGridCell[] { + if (view === "day") { + const day = startOfUtcDay(anchorDate); + return Array.from({ length: 12 }, (_, index) => { + const slot = new Date(day); + slot.setUTCHours(8 + index, 0, 0, 0); + return { + id: `day-${slot.toISOString()}`, + label: formatTime(slot.toISOString()), + subLabel: index === 0 ? formatDate(slot.toISOString()) : null, + isToday: isSameUtcDate(slot.toISOString(), new Date().toISOString()), + events: events.filter((event) => isSameHour(event.start, slot.toISOString())), + }; + }); + } + + const start = view === "week" ? startOfUtcDay(anchorDate) : startOfMonthGrid(anchorDate); + const length = view === "week" ? 7 : 42; + + return Array.from({ length }, (_, index) => { + const date = new Date(start); + date.setUTCDate(start.getUTCDate() + index); + const iso = date.toISOString(); + return { + id: `${view}-${iso}`, + label: formatDate(iso), + subLabel: view === "month" ? null : "Working day", + isToday: isSameUtcDate(iso, new Date().toISOString()), + events: events.filter((event) => isSameUtcDate(event.start, iso)), + }; + }); +} + +function CalendarEmptyOverlay({ view }: { view: "day" | "week" | "month" }) { + return ( +
+
+ +

No scheduled work. This {view} is free.

+

+ The calendar grid is still available for selecting dates and planning Activities. +

+ +
+
+ ); +} + +function startOfUtcDay(value: string): Date { + const date = new Date(value); + date.setUTCHours(0, 0, 0, 0); + return date; +} + +function startOfMonthGrid(value: string): Date { + const firstOfMonth = startOfUtcDay(value); + firstOfMonth.setUTCDate(1); + const gridStart = new Date(firstOfMonth); + gridStart.setUTCDate(firstOfMonth.getUTCDate() - firstOfMonth.getUTCDay()); + return gridStart; +} + +function isSameUtcDate(left: string, right: string): boolean { + return left.slice(0, 10) === right.slice(0, 10); +} + +function isSameHour(left: string, right: string): boolean { + const leftDate = new Date(left); + const rightDate = new Date(right); + return ( + isSameUtcDate(leftDate.toISOString(), rightDate.toISOString()) && + leftDate.getUTCHours() === rightDate.getUTCHours() + ); +} + function CalendarEventCard({ event }: { event: BigCalendarEvent }) { return (