PhoenixKitProjects.CalendarDisplay (PhoenixKitProjects v0.19.0)

Copy Markdown View Source

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 its ScheduleLayout span 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 display field (bounds the form input).

CSS body (no surrounding <style> tag) for an overdue-animation config map of the shape read/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/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.

The pure compile-time defaults of read/0 (no settings read; core's week_start_day falls back to Monday). Mount-time placeholder so a render can never dot-access a nil config — read/0 overwrites it immediately.

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.

The late-task marker class for a read/0 config: "pattern" (the default) — the pk-overdue overlay, i.e. the same configured stripes/solid look the Projects-mode overdue stretch uses, so every calendar marks lateness identically — or "ring", the red inset ring alternative. Anything else (including maps missing the key) follows the default. Consumers using the pattern must also inject animation_style/1 (and ideally wrap the grid in SyncAnimations).

Allowed late-task marker styles (for the settings form).

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 calendar-display field. An invalid field or value is ignored. Booleans go through put_flag/2; week_start_day is core's setting and is NOT writable from here.

Flip one boolean display flag ("show_weekends", "show_week_numbers", "fixed_weeks"). Unknown fields are ignored.

The full calendar-display config (validated, with safe defaults): the grid appearance half (weekends / week numbers / fixed weeks / per-day caps, plus the site-wide week_start_day core setting the grids must honor) and the overdue/late-marker half. One batched, uncached query — this runs on every Overview reload (each {:projects, …} broadcast), not just mount, and direct reads keep the settings page's live demo fresh.

Restore every calendar-display 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

anim_modes()

@spec anim_modes() :: [String.t()]

Allowed overdue-animation modes (for the settings form).

anim_patterns()

@spec anim_patterns() :: [String.t()]

Allowed overdue-marker patterns (for the settings form).

anim_range(binary)

@spec anim_range(String.t()) :: {number(), number()}

Clamp range {lo, hi} for a numeric display field (bounds the form input).

animation_css(cfg)

@spec animation_css(map()) :: String.t()

CSS body (no surrounding <style> tag) for an overdue-animation config map of the shape read/0 returns. Public so tests and a settings preview can render a given config without touching the settings store.

animation_style(cfg)

@spec animation_style(map()) :: String.t()

The <style> block for the overdue animation, from an already-read/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.

color_for(uuid)

@spec color_for(term()) :: {String.t(), String.t()}

defaults()

@spec defaults() :: map()

The pure compile-time defaults of read/0 (no settings read; core's week_start_day falls back to Monday). Mount-time placeholder so a render can never dot-access a nil config — read/0 overwrites it immediately.

events(active_summaries, completed, upcoming, lang, today \\ Date.utc_today(), offset \\ "0", opts \\ [])

Builds the calendar event list from the dashboard's already-loaded data.

  • active_summariesProjects.project_tree_summary/1 maps for running projects (carry :project and :planned_end), each tagged by the caller with :late (boolean) to drive the blink
  • completed / upcoming[Project.t()] (recently completed / scheduled)
  • lang — content language for project titles
  • today — anchor so a running project's bar always reaches at least today
  • offset — 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

Options:

  • :late_marker — how a LATE running project is marked ("pattern" default): "pattern" highlights the overdue stretch (planned_end+1 .. today) with the configured overdue pattern, so the marked length shows HOW late; "ring" rings the whole bar instead — the same marker late task chips wear, so one setting dresses everything late the same way.

events_on(events, date)

Events whose [start, end) span covers date, soonest-starting first — the shared day-popup row source for both calendars.

exclusive_end_date(start_d, e, offset \\ nil)

@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.

late_marker_class(arg1)

@spec late_marker_class(map()) :: String.t() | nil

The late-task marker class for a read/0 config: "pattern" (the default) — the pk-overdue overlay, i.e. the same configured stripes/solid look the Projects-mode overdue stretch uses, so every calendar marks lateness identically — or "ring", the red inset ring alternative. Anything else (including maps missing the key) follows the default. Consumers using the pattern must also inject animation_style/1 (and ideally wrap the grid in SyncAnimations).

late_markers()

@spec late_markers() :: [String.t()]

Allowed late-task marker styles (for the settings form).

loading_class()

@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.

loading_style()

@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.

max_events()

@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.

max_multiday()

@spec max_multiday() :: pos_integer()

put(arg1, v)

@spec put(String.t(), term()) :: term()

Persist one calendar-display field. An invalid field or value is ignored. Booleans go through put_flag/2; week_start_day is core's setting and is NOT writable from here.

put_flag(field, on?)

@spec put_flag(String.t(), boolean()) :: term()

Flip one boolean display flag ("show_weekends", "show_week_numbers", "fixed_weeks"). Unknown fields are ignored.

read()

@spec read() :: %{
  pattern: String.t(),
  mode: String.t(),
  speed: float(),
  brightness_min: float(),
  brightness_max: float(),
  wave_step: float(),
  opacity: float(),
  late_marker: String.t(),
  week_start: 1..7,
  show_weekends: boolean(),
  show_week_numbers: boolean(),
  fixed_weeks: boolean(),
  max_events: pos_integer(),
  max_multiday: pos_integer()
}

The full calendar-display config (validated, with safe defaults): the grid appearance half (weekends / week numbers / fixed weeks / per-day caps, plus the site-wide week_start_day core setting the grids must honor) and the overdue/late-marker half. One batched, uncached query — this runs on every Overview reload (each {:projects, …} broadcast), not just mount, and direct reads keep the settings page's live demo fresh.

reset()

@spec reset() :: :ok

Restore every calendar-display setting to its default.

task_events(items_with_spans, lang, offset \\ "0", opts \\ [])

@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 UTC NaiveDateTime; when given, a not-done task whose span ended before it is late: its chip/bar gets the late marker class and its meta entry late: true. Without :now nothing is flagged (pure date math only).
  • :late_class — the marker class for late chips/bars (defaults to the red inset ring; pass late_marker_class(read()) to honor the configured marker).

task_late?(a, span, now)

@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).