Skip to content
Open
241 changes: 241 additions & 0 deletions lib/dotcom_web/live/daily_schedules_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
defmodule DotcomWeb.DailySchedulesLive do
@moduledoc """
A simple basic-auth gated page to explore what kinds of daily schedules are actually available for the routes we serve.
"""

use DotcomWeb, :live_view

alias Phoenix.LiveView

@impl LiveView
def mount(_params, _session, socket) do
{
:ok,
socket
}
end

@impl true
def handle_params(params, _uri, socket) do
{:noreply, socket |> assign(:route_id, params |> Map.get("route_id", "1"))}
end

@impl true
def handle_event("change-route", %{"route_id" => route_id}, socket) do
new_path = socket |> live_path(__MODULE__, %{"route_id" => route_id})
{:noreply, socket |> push_patch(to: new_path)}
end

@impl true
def render(%{route_id: route_id} = assigns) do
services = Services.Repo.by_route_id(route_id)

daily_schedules =
services
|> Enum.flat_map(fn svc ->
svc
|> Services.Service.all_valid_dates_for_service()
|> Enum.map(&{&1, svc})
end)
|> Enum.group_by(fn {date, _svc} -> date end)
|> Enum.map(fn {date, list} ->
{date, list |> Enum.map(fn {_date, svc} -> svc end) |> Enum.sort_by(& &1.id)}
end)
|> Enum.sort_by(fn {date, _svc_list} -> date end, &Date.before?/2)
|> Enum.group_by(fn {_date, svc_list} -> svc_list end)
|> Enum.map(fn {svc_list, date_tuples} ->
%{
services: svc_list,
dates: date_tuples |> Enum.map(fn {date, _svc_list} -> date end)
}
end)
|> Enum.map(&add_holiday_status/1)
|> Enum.map(&add_title/1)
|> Enum.sort_by(& &1.title)

{holiday_schedules, non_holiday_schedules} = daily_schedules |> Enum.split_with(& &1.holiday?)

assigns =
assigns
|> assign(:route_id, route_id)
|> assign(:daily_schedules, daily_schedules)
|> assign(:holiday_schedules, holiday_schedules)
|> assign(:non_holiday_schedules, non_holiday_schedules)

~H"""
<h1>Daily Schedules</h1>

<.route_changer selected_route_id={@route_id} />

<h2>Regular Schedules</h2>
<.daily_schedule :for={schedule <- @non_holiday_schedules} schedule={schedule} />

<h2>Holiday Schedules</h2>
<.daily_schedule :for={schedule <- @holiday_schedules} schedule={schedule} />
"""
end

defp add_holiday_status(schedule) do
schedule
|> Map.put(:holiday?, schedule.services |> Enum.any?(&(&1.typicality == :holiday_service)))
end

defp add_title(schedule) do
schedule
|> Map.put(:title, title(schedule))
end

defp title(schedule) do
days_of_week = schedule.dates |> Enum.map(&Date.day_of_week/1) |> MapSet.new()
date_set = schedule.dates |> MapSet.new()

[
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-27]])) end, "Thanksgiving"},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the holidays, I'm curious why you didn't want to use the added_date_notes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hummm - I don't remember, tbh!

{fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-28]])) end,
"Day After Thanksgiving"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-11]])) end, "Veterans Day"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-25]])) end, "Christmas Day"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-31]])) end, "New Year's Eve"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-01]])) end, "New Year's Day"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-25], ~D[2026-01-01]])) end,
"Christmas and New Year's"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-19]])) end,
"Martin Luther King Jr. Day"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-02-16]])) end, "Presidents Day"},
{fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-19], ~D[2026-02-16]])) end,
"Martin Luther King Jr. and Presidents' Day"},
{fn -> MapSet.subset?(days_of_week, MapSet.new([5])) end, "Friday"},
{fn -> MapSet.subset?(days_of_week, MapSet.new([1, 2, 3, 4])) end, "Monday - Thursday"},
{fn -> MapSet.subset?(days_of_week, MapSet.new([1, 2, 3, 4, 5])) end, "Weekday"},
{fn -> MapSet.subset?(days_of_week, MapSet.new([6])) end, "Saturday"},
{fn -> MapSet.subset?(days_of_week, MapSet.new([7])) end, "Sunday"},
{fn -> true end, "???"}
]
|> Stream.filter(fn {condition_fun, _title} -> condition_fun.() end)
|> Stream.map(fn {_condition_fun, title} -> title end)
|> Enum.take(1)
|> List.first()
end

defp daily_schedule(assigns) do
%{services: services} = assigns.schedule
title = services |> Enum.map(& &1.id) |> Enum.sort() |> Enum.join(", ")
assigns = assigns |> assign(:title, title)

~H"""
<details class="mt-2 mb-4 border border-brand-primary rounded-lg p-2 group/schedule">
<summary class="flex gap-2 items-center cursor-pointer">
<div class="font-bold">{@schedule.title}</div>
<div class="flex gap-2 group-open/schedule:hidden">
<div :for={svc <- @schedule.services} class="text-xs p-1 border-xs rounded">{svc.id}</div>
</div>
<.icon
name="chevron-down"
class="ml-auto h-3 w-3 group-open/schedule:rotate-180 transition-all"
/>
</summary>

<div class="mt-2 mb-4">
<div class="font-bold text-xs uppercase">Services</div>
<div class="mt-1 flex flex-wrap gap-2">
<div :for={svc <- @schedule.services} class="text-xs border-xs rounded p-1 max-w-80">
<div class="font-bold text-sm">{svc.id}</div>
<div class="mt-1 grid grid-cols-2 gap-1 items-center">
<div class="font-bold">Name</div>
<div class="p-1 border-xs rounded">{svc.name}</div>

<div class="font-bold">Start Date</div>
<div class="p-1 border-xs rounded">{svc.start_date}</div>

<div class="font-bold">End Date</div>
<div class="p-1 border-xs rounded">{svc.end_date}</div>

<div class="font-bold">Valid Days</div>
<div class="flex flex-wrap gap-1">
<div :for={day <- svc.valid_days} class="p-1 border-xs rounded">
{pretty_day_of_week(day)}
</div>
</div>

<div class="font-bold">Typicality</div>
<div class="p-1 border-xs rounded">{svc.typicality}</div>
</div>

<div class="font-bold">Added Dates</div>
<div class="flex flex-wrap gap-1">
<div :for={date <- svc.added_dates} class="p-1 border-xs rounded">{date}</div>
</div>

<div class="font-bold">Removed Dates</div>
<div class="flex flex-wrap gap-1">
<div :for={date <- svc.removed_dates} class="p-1 border-xs rounded">{date}</div>
</div>
</div>
</div>
</div>

<div class="mb-4">
<div class="font-bold text-xs uppercase">Active Dates</div>
<div class="mt-1 flex flex-wrap gap-1">
<div :for={date <- @schedule.dates} class="text-xs border-xs rounded p-1">
{date} ({day_of_week(date)})
</div>
</div>
</div>

<div class="mb-4">
<div class="font-bold text-xs uppercase">Unique days of the week</div>
<div class="mt-1 flex gap-2">
<div
:for={
day_of_week <-
@schedule.dates |> Enum.map(&Date.day_of_week/1) |> Enum.uniq() |> Enum.sort()
}
class="p-1 border-xs rounded text-xs"
>
{pretty_day_of_week(day_of_week)}
</div>
</div>
</div>

<details class="group/code">
<summary class="flex items-center w-full cursor-pointer">
<div class="text-xs font-bold uppercase">Full Schedule Data Blob</div>
<.icon
name="chevron-down"
class="ml-auto h-3 w-3 group-open/code:rotate-180 transition-all"
/>
</summary>
<pre>{inspect @schedule, pretty: true}</pre>
</details>
</details>
"""
end

defp day_of_week(date), do: date |> Date.day_of_week() |> pretty_day_of_week()

defp pretty_day_of_week(1), do: "Mon"
defp pretty_day_of_week(2), do: "Tue"
defp pretty_day_of_week(3), do: "Wed"
defp pretty_day_of_week(4), do: "Thu"
defp pretty_day_of_week(5), do: "Fri"
defp pretty_day_of_week(6), do: "Sat"
defp pretty_day_of_week(7), do: "Sun"
defp pretty_day_of_week(_), do: "?"

defp route_changer(assigns) do
assigns =
assigns
|> assign(:routes, Routes.Repo.all())

~H"""
<form phx-change="change-route">
<select class="border border-brand-primary rounded-lg p-2 bg-white" name="route_id">
<option :for={route <- @routes} selected={route.id == @selected_route_id} value={route.id}>
{route.name}
</option>
</select>
</form>
"""
end
end
8 changes: 8 additions & 0 deletions lib/dotcom_web/live/preview_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ defmodule DotcomWeb.PreviewLive do

use DotcomWeb, :live_view

alias DotcomWeb.DailySchedulesLive
alias DotcomWeb.Router.Helpers
alias DotcomWeb.ScheduleFinderLive
alias Phoenix.LiveView

@pages [
%{
arguments: [],
icon_name: "calendar-days",
icon_type: "solid",
module: DailySchedulesLive,
title: "Daily Schedules Experiment"
},
%{
arguments: ["Red", "0"],
icon_name: "icon-realtime-tracking",
Expand Down
1 change: 1 addition & 0 deletions lib/dotcom_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ defmodule DotcomWeb.Router do
live_session :default, layout: {DotcomWeb.LayoutView, :preview} do
live "/", PreviewLive
live "/schedules/:route_id/:direction_id", ScheduleFinderLive
live "/daily-schedules", DailySchedulesLive
end
end

Expand Down
14 changes: 7 additions & 7 deletions lib/services/service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,13 @@ defmodule Services.Service do
end

@spec all_valid_dates_for_service(t()) :: [Date.t()]
defp all_valid_dates_for_service(%__MODULE__{
start_date: from,
end_date: until,
added_dates: added_dates,
removed_dates: removed_dates,
valid_days: valid_days
}) do
def all_valid_dates_for_service(%__MODULE__{
start_date: from,
end_date: until,
added_dates: added_dates,
removed_dates: removed_dates,
valid_days: valid_days
}) do
# fallback to today if either start or end date are nil
from = from || Timex.today()
until = until || Timex.today()
Expand Down
34 changes: 34 additions & 0 deletions livebooks/service_daily_schedules.livemd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Service --> Daily Schedules

## Services to Daily Schedules

```elixir
services = Services.Repo.by_route_id("39")

services
|> Enum.flat_map(fn svc ->
svc
|> Services.Service.all_valid_dates_for_service()
|> Enum.map(&{&1, svc})
end)
|> Enum.group_by(fn {date, _svc} -> date end)
|> Enum.map(fn {date, list} ->
{date, list |> Enum.map(fn {_date, svc} -> svc end) |> Enum.sort_by(& &1.id)}
end)
|> Enum.sort_by(fn {date, _svc_list} -> date end, &Date.before?/2)
|> Enum.group_by(fn {_date, svc_list} -> svc_list end)
|> Enum.each(fn {svc_list, date_tuples} ->
dates =
date_tuples
|> Enum.map(fn {date, _svc_list} -> date end)
|> Enum.map(&"#{&1}")
|> Enum.join(", ")

IO.puts(svc_list |> Enum.map(& &1.id) |> Enum.join(", "))
IO.puts(" #{svc_list |> Enum.map(& &1.name) |> Enum.join(", ")}")
IO.puts(" #{svc_list |> Enum.map(& &1.type) |> Enum.join(", ")}")
IO.puts(" #{svc_list |> Enum.map(& &1.typicality) |> Enum.join(", ")}")
IO.puts(" #{dates}")
IO.puts("")
end)
```