Maps projects — and, for the default Tasks mode, their scheduled tasks — to
PhoenixLiveCalendar.Event structs for the Overview dashboard calendar.
Two mapping modes back the Overview calendar's mode toggle:
task_events/3(the Tasks default) — every leaf task across every given project, placed on the days itsScheduleLayoutspan covers, colored by the OWNING project's identity color so a day reads as "what happens today, across projects". Same-day tasks stack as chips, multi-day tasks as bars; the LiveView caps the per-day count and the grid shows a Google-style "+N more" for the rest.events/6(the Projects mode — the original view) — each project as one all-day "ongoing line": running projects span start → planned-end (reaching at least today, so they read as ongoing), completed projects span start → completion, and scheduled projects get a one-day marker on their planned start.
Bars are colored by project identity — a stable color per project, picked
by hashing the project id over a fixed palette of hues spread around the color
wheel — so adjacent and stacked bars stay tellable apart. Lifecycle status is
conveyed by the dashboard's project cards, with one exception: for a running
summary the caller tags late: true, only the stretch of the bar PAST its
planned end (planned_end+1 .. today) is highlighted — via extra.highlight
with the pk-overdue class (it marks that stretch in the INVERSE of the bar's
own colour — diagonal stripes over the identity colour, or a solid fill —
animated per animation_style/0, all configured on /admin/settings/projects)
— so the length of the marked tail shows how late the project is. The rest
keeps its identity color.
end is exclusive in PhoenixLiveCalendar ([start, end)), so the inclusive
last day gets +1. Projects without a placeable start date are dropped.
Summary
Functions
Allowed overdue-animation modes (for the settings form).
Allowed overdue-marker patterns (for the settings form).
Clamp range {lo, hi} for a numeric overdue field (bounds the form input).
CSS body (no surrounding <style> tag) for an overdue-animation config map of
the shape read_animation/0 returns. Public so tests and a settings preview
can render a given config without touching the settings store.
The <style> block for the overdue animation, from an already-read_animation/0
config map (callers thread the map so one read serves style + legend). Safe to
inject with Phoenix.HTML.raw/1: every interpolated value is a validated enum
or clamped number, never free text.
Builds the calendar event list from the dashboard's already-loaded data.
Events whose [start, end) span covers date, soonest-starting first —
the shared day-popup row source for both calendars.
The exclusive end DATE for a span ending at e (UTC naive). With a viewer
offset ("+3") the end is evaluated in the local frame; nil keeps UTC —
the project Calendar tab's Timeline-parity frame. An end falling exactly on
(local) midnight doesn't occupy that day, any later instant does. Floored
one day past the start so a zero-length span still shows as a one-day chip.
Tasks-mode late marker: a red inset ring on the chip/bar. Returned from a function (not duplicated as a literal per call site) so the Overview calendar and the project Calendar tab can't drift apart.
Tailwind classes giving any of OUR clickable controls in-flight feedback:
LiveView tags the clicked element with phx-click-loading for the round
trip; these arbitrary variants pulse + dim it and block double-clicks.
Returned from a function (not inlined per call site) so every control
shares one literal for the Tailwind scan.
A <style> block giving the calendar LIBRARY's clickable nodes (event
chips, multi-day bars, day cells, "+N more") the same in-flight pulse —
their classes are lib-rendered, so the Tailwind-variant approach above
can't reach them. Static CSS, safe for Phoenix.HTML.raw/1.
Per-day caps for the month grids, shared by BOTH calendars — the "+N more"
link / whole-day popup contract depends on the two grids agreeing on when
a day overflows. max_events/0 caps single-day chips, max_multiday/0
caps multi-day bar slots.
Persist one overdue-animation field ("pattern", "mode", "speed",
"brightness_min", "brightness_max", "wave_step"). An invalid field or
value is ignored.
Current overdue-animation settings (validated, with safe defaults).
Restore every overdue-animation setting to its default.
Builds the Tasks-mode event list (+ a per-task metadata map) from prepared
{item, span} pairs — each a PhoenixKitProjects.ScheduleLayout item with
its computed %{start: NaiveDateTime, end: NaiveDateTime} span. Pure: the
caller runs the schedule walk (ScheduleLayout.tree/1 per project) and
passes only the LEAF items (sub-project container assignments excluded —
their child tasks stand for themselves here).
A task is late when it isn't done and its scheduled span ended before
now — the schedule says it should already have finished. The shared rule
behind both calendars' red rings, late badges, and Overdue-only filters
(a nil now flags nothing — pure date math only).
Functions
@spec anim_modes() :: [String.t()]
Allowed overdue-animation modes (for the settings form).
@spec anim_patterns() :: [String.t()]
Allowed overdue-marker patterns (for the settings form).
Clamp range {lo, hi} for a numeric overdue field (bounds the form input).
CSS body (no surrounding <style> tag) for an overdue-animation config map of
the shape read_animation/0 returns. Public so tests and a settings preview
can render a given config without touching the settings store.
The <style> block for the overdue animation, from an already-read_animation/0
config map (callers thread the map so one read serves style + legend). Safe to
inject with Phoenix.HTML.raw/1: every interpolated value is a validated enum
or clamped number, never free text.
@spec events( [map()], [PhoenixKitProjects.Schemas.Project.t()], [PhoenixKitProjects.Schemas.Project.t()], String.t() | nil, Date.t(), String.t() ) :: [PhoenixLiveCalendar.Event.t()]
Builds the calendar event list from the dashboard's already-loaded data.
active_summaries—Projects.project_tree_summary/1maps for running projects (carry:projectand:planned_end), each tagged by the caller with:late(boolean) to drive the blinkcompleted/upcoming—[Project.t()](recently completed / scheduled)lang— content language for project titlestoday— anchor so a running project's bar always reaches at least todayoffset— viewer timezone offset (e.g. "+3"); UTC datetimes are converted to local calendar dates with it, so bars land on the same day the viewer sees
@spec events_on([PhoenixLiveCalendar.Event.t()], Date.t()) :: [ PhoenixLiveCalendar.Event.t() ]
Events whose [start, end) span covers date, soonest-starting first —
the shared day-popup row source for both calendars.
@spec exclusive_end_date(Date.t(), NaiveDateTime.t(), String.t() | nil) :: Date.t()
The exclusive end DATE for a span ending at e (UTC naive). With a viewer
offset ("+3") the end is evaluated in the local frame; nil keeps UTC —
the project Calendar tab's Timeline-parity frame. An end falling exactly on
(local) midnight doesn't occupy that day, any later instant does. Floored
one day past the start so a zero-length span still shows as a one-day chip.
@spec late_class() :: String.t()
Tasks-mode late marker: a red inset ring on the chip/bar. Returned from a function (not duplicated as a literal per call site) so the Overview calendar and the project Calendar tab can't drift apart.
@spec loading_class() :: String.t()
Tailwind classes giving any of OUR clickable controls in-flight feedback:
LiveView tags the clicked element with phx-click-loading for the round
trip; these arbitrary variants pulse + dim it and block double-clicks.
Returned from a function (not inlined per call site) so every control
shares one literal for the Tailwind scan.
@spec loading_style() :: String.t()
A <style> block giving the calendar LIBRARY's clickable nodes (event
chips, multi-day bars, day cells, "+N more") the same in-flight pulse —
their classes are lib-rendered, so the Tailwind-variant approach above
can't reach them. Static CSS, safe for Phoenix.HTML.raw/1.
@spec max_events() :: pos_integer()
Per-day caps for the month grids, shared by BOTH calendars — the "+N more"
link / whole-day popup contract depends on the two grids agreeing on when
a day overflows. max_events/0 caps single-day chips, max_multiday/0
caps multi-day bar slots.
@spec max_multiday() :: pos_integer()
Persist one overdue-animation field ("pattern", "mode", "speed",
"brightness_min", "brightness_max", "wave_step"). An invalid field or
value is ignored.
@spec read_animation() :: %{ pattern: String.t(), mode: String.t(), speed: float(), brightness_min: float(), brightness_max: float(), wave_step: float() }
Current overdue-animation settings (validated, with safe defaults).
@spec reset_animation() :: :ok
Restore every overdue-animation setting to its default.
@spec task_events( [{map(), %{start: NaiveDateTime.t(), end: NaiveDateTime.t()}}], String.t() | nil, String.t(), keyword() ) :: {[PhoenixLiveCalendar.Event.t()], %{optional(String.t()) => map()}}
Builds the Tasks-mode event list (+ a per-task metadata map) from prepared
{item, span} pairs — each a PhoenixKitProjects.ScheduleLayout item with
its computed %{start: NaiveDateTime, end: NaiveDateTime} span. Pure: the
caller runs the schedule walk (ScheduleLayout.tree/1 per project) and
passes only the LEAF items (sub-project container assignments excluded —
their child tasks stand for themselves here).
Each task becomes an all-day event spanning its scheduled days in the
viewer's timezone (offset, e.g. "+3" — same local basis as the rest of
the Overview), colored by the OWNING project's identity color
(color_for/1), so tasks group visually by project across the month.
Exclusive ends ([start, end)): a span ending exactly at local midnight
doesn't occupy that day, and a zero-length span still shows as a one-day
chip.
Returns {events, meta} — meta keyed by assignment uuid with
:project_uuid / :project_name / :status / :late for the day popup's
rows, the overdue filter, and click navigation.
Options:
:now— a UTCNaiveDateTime; when given, a not-done task whose span ended before it is late: its chip/bar gets a red inset ring (@late_class) and its meta entrylate: true. Without:nownothing is flagged (pure date math only).
@spec task_late?( map(), %{:end => NaiveDateTime.t(), optional(atom()) => term()}, NaiveDateTime.t() | nil ) :: boolean()
A task is late when it isn't done and its scheduled span ended before
now — the schedule says it should already have finished. The shared rule
behind both calendars' red rings, late badges, and Overdue-only filters
(a nil now flags nothing — pure date math only).