# `PhoenixLiveCalendar.Event`
[🔗](https://github.com/mdon/phoenix_live_calendar/blob/v0.5.0/lib/phoenix_live_calendar/event.ex#L1)

Represents a calendar event.

Consumers map their database records to this struct before passing them

to calendar components. Only `id` and `start` are required — everything
else has sensible defaults.

## End time semantics

End times are **exclusive** (half-open interval `[start, end)`).

- An all-day event on April 1st: `start: ~D[2026-04-01], end: ~D[2026-04-02]`
- A 1-hour meeting at 10am: `start: ~U[2026-04-01 10:00:00Z], end: ~U[2026-04-01 11:00:00Z]`

If `end` is `nil`, a default duration is applied:
- All-day events default to 1 day
- Timed events default to 30 minutes

## Examples

    # Minimal event
    %PhoenixLiveCalendar.Event{id: "1", start: ~D[2026-04-01]}

    # Timed event with details
    %PhoenixLiveCalendar.Event{
      id: "meeting-1",
      title: "Team Standup",
      start: ~U[2026-04-01 09:00:00Z],
      end: ~U[2026-04-01 09:30:00Z],
      color: "bg-primary"
    }

    # All-day event spanning multiple days
    %PhoenixLiveCalendar.Event{
      id: "vacation-1",
      title: "Spring Break",
      start: ~D[2026-04-06],
      end: ~D[2026-04-11],
      all_day: true
    }

    # Booking with resource and constraints
    %PhoenixLiveCalendar.Event{
      id: "booking-1",
      title: "Dr. Smith - Consultation",
      start: ~U[2026-04-01 14:00:00Z],
      end: ~U[2026-04-01 15:00:00Z],
      resource_id: "room-a",
      editable: false,
      overlap: false,
      extra: %{patient_id: "p-123", type: :consultation}
    }

## Visibility tiers

The `visibility` field controls which views an event appears in.
Higher values mean the event appears in more zoomed-out views.
Uses multiples of 10 for granularity between tiers.

| Visibility | Shows in                          | Example use          |
|-----------|-----------------------------------|----------------------|
| 10        | Day only                          | Lunch breaks, focus time |
| 20        | Day + Week (default)              | Regular meetings     |
| 25        | Day + Week                        | Slightly important   |
| 30        | Day + Week + Month                | Key deadlines        |
| 35        | Day + Week + Month                | High priority        |
| 40        | Day + Week + Month + Year         | Company milestones   |

The CalendarComponent applies view-specific thresholds automatically.
Override with `min_visibility` attribute on the component.

# `display`

```elixir
@type display() :: :auto | :background | :inverse_background | :none
```

# `priority`

```elixir
@type priority() :: :low | :normal | :high | :urgent
```

# `status`

```elixir
@type status() :: :confirmed | :tentative | :cancelled | :pending_approval | :no_show
```

# `t`

```elixir
@type t() :: %PhoenixLiveCalendar.Event{
  all_day: boolean(),
  badge: String.t() | nil,
  border_color: String.t() | nil,
  category: String.t() | atom() | nil,
  class: String.t() | nil,
  color: String.t() | atom() | nil,
  description: String.t() | nil,
  display: display(),
  editable: boolean(),
  end: Date.t() | DateTime.t() | NaiveDateTime.t() | nil,
  extra: map(),
  group_id: term() | nil,
  icon: String.t() | nil,
  id: term(),
  layer_id: term() | nil,
  location: String.t() | nil,
  overlap: boolean(),
  priority: priority(),
  recurrence_id: term() | nil,
  resource_id: term() | nil,
  resource_ids: [term()] | nil,
  rrule: String.t() | nil,
  start: Date.t() | DateTime.t() | NaiveDateTime.t(),
  status: status(),
  text_color: String.t() | nil,
  title: String.t() | nil,
  transparency: transparency(),
  urgency: urgency(),
  url: String.t() | nil,
  visibility: pos_integer()
}
```

# `transparency`

```elixir
@type transparency() :: :opaque | :transparent
```

# `urgency`

```elixir
@type urgency() :: :none | :attention | :warning | :critical
```

# `all_day?`

```elixir
@spec all_day?(t()) :: boolean()
```

Returns whether this event is an all-day event.

An event is considered all-day if `all_day` is `true` or if
`start` is a `Date` (not a `DateTime` or `NaiveDateTime`).

# `dates_overlap?`

```elixir
@spec dates_overlap?(t(), t()) :: boolean()
```

Whether two events occupy any calendar date in common (INCLUSIVE
first/last dates — the occupancy rule `on_date?/2` uses, so a
midnight-crossing event counts on its spill-over day).

# `day_window`

```elixir
@spec day_window(t(), Date.t(), Time.t(), Time.t()) :: {Time.t(), Time.t()} | nil
```

The times this event's block occupies on ONE day, clipped to the visible
`[min_time, max_time]` window — or `nil` when nothing of it is visible
that day. The single source of truth for per-day time-grid segments:

- a midnight-crossing event runs to end-of-day on its first day and from
  00:00 on its last (the raw EXCLUSIVE end date decides the split, so an
  event ending exactly at midnight still renders on its start day)
- all-day events span the whole visible window

# `duration_seconds`

```elixir
@spec duration_seconds(t()) :: integer()
```

Returns the duration of the event in seconds.

For all-day events, returns the number of days multiplied by 86400.

# `effective_end`

```elixir
@spec effective_end(t()) :: Date.t() | DateTime.t() | NaiveDateTime.t()
```

Returns the effective end time/date for this event.

If `end` is nil, applies a default duration:
- All-day events: start + 1 day
- Timed events: start + 30 minutes

# `first_date`

```elixir
@spec first_date(t()) :: Date.t()
```

The FIRST calendar date this event occupies (its start date).

# `in_range?`

```elixir
@spec in_range?(t(), Date.t(), Date.t()) :: boolean()
```

Whether the event occupies any date in `[range_start, range_end)` —
inclusive start, EXCLUSIVE end, the same shape `on_date_range_change`
reports and `DateHelpers.visible_range/3` returns.

Uses the same occupancy rule as `on_date?/2`/`last_date/1`, so a timed
event running past midnight counts on its spill-over day and an event
ending exactly at midnight does not.

# `last_date`

```elixir
@spec last_date(t()) :: Date.t()
```

The LAST calendar date this event occupies on a date grid (inclusive).

This is the single source of truth for "which day is the event's last
day", so bar rendering and occupancy never disagree:

- All-day events: `end` is exclusive, so the last day is `end - 1`.
- Timed events: the event occupies the date it ends ON (an event ending
  10:30 on the 17th is on the 17th) — UNLESS it ends exactly at midnight
  (00:00:00), the boundary, which does not count as occupying that day.

# `multi_day?`

```elixir
@spec multi_day?(t()) :: boolean()
```

Returns whether this event spans multiple days.

# `on_date?`

```elixir
@spec on_date?(t(), Date.t()) :: boolean()
```

Returns whether this event falls on the given date.

# `on_resource?`

```elixir
@spec on_resource?(t(), term()) :: boolean()
```

Whether the event belongs on a resource's row/column — matches the
singular `resource_id` OR membership in the plural `resource_ids`.

# `overlaps_range?`

```elixir
@spec overlaps_range?(t(), Date.t() | DateTime.t(), Date.t() | DateTime.t()) ::
  boolean()
```

Returns whether this event overlaps with the given date range `[range_start, range_end)`.

# `spans_multiple_dates?`

```elixir
@spec spans_multiple_dates?(t()) :: boolean()
```

Returns whether this event occupies more than one calendar DATE — i.e. its
first and last occupied days differ.

This is the right test for "render as one continuous bar across day cells"
on a date grid: it is true for a multi-day all-day event AND for a timed
event that runs past midnight into another date (a 10pm→2am event is on two
dates), but false for a same-day event or one that ends exactly at midnight
(which occupies only the starting day). Unlike `multi_day?/1`, it doesn't
care how many hours the event lasts — only which dates it touches.

# `visible_at?`

```elixir
@spec visible_at?(t(), pos_integer()) :: boolean()
```

Returns whether this event meets a minimum visibility threshold.

Events with `visibility >= min_visibility` are considered visible.
Default event visibility is 20 (shows in day and week views).

## View defaults

| View     | Threshold | Shows events with visibility >= |
|----------|-----------|-------------------------------|
| Day      | 10        | 10+ (almost everything)       |
| Week     | 20        | 20+ (default events)          |
| Month    | 30        | 30+ (important only)          |
| Year     | 40        | 40+ (highest importance)      |

## Examples

    iex> Event.visible_at?(%Event{id: 1, start: ~D[2026-04-01], visibility: 20}, 30)
    false

    iex> Event.visible_at?(%Event{id: 1, start: ~D[2026-04-01], visibility: 30}, 30)
    true

---

*Consult [api-reference.md](api-reference.md) for complete listing*
