diff --git a/lib/dotcom_web/live/daily_schedules_live.ex b/lib/dotcom_web/live/daily_schedules_live.ex new file mode 100644 index 0000000000..29b68e0c9b --- /dev/null +++ b/lib/dotcom_web/live/daily_schedules_live.ex @@ -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""" +

Daily Schedules

+ + <.route_changer selected_route_id={@route_id} /> + +

Regular Schedules

+ <.daily_schedule :for={schedule <- @non_holiday_schedules} schedule={schedule} /> + +

Holiday Schedules

+ <.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"}, + {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""" +
+ +
{@schedule.title}
+
+
{svc.id}
+
+ <.icon + name="chevron-down" + class="ml-auto h-3 w-3 group-open/schedule:rotate-180 transition-all" + /> +
+ +
+
Services
+
+
+
{svc.id}
+
+
Name
+
{svc.name}
+ +
Start Date
+
{svc.start_date}
+ +
End Date
+
{svc.end_date}
+ +
Valid Days
+
+
+ {pretty_day_of_week(day)} +
+
+ +
Typicality
+
{svc.typicality}
+
+ +
Added Dates
+
+
{date}
+
+ +
Removed Dates
+
+
{date}
+
+
+
+
+ +
+
Active Dates
+
+
+ {date} ({day_of_week(date)}) +
+
+
+ +
+
Unique days of the week
+
+
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)} +
+
+
+ +
+ +
Full Schedule Data Blob
+ <.icon + name="chevron-down" + class="ml-auto h-3 w-3 group-open/code:rotate-180 transition-all" + /> +
+
{inspect @schedule, pretty: true}
+
+
+ """ + 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""" +
+ +
+ """ + end +end diff --git a/lib/dotcom_web/live/preview_live.ex b/lib/dotcom_web/live/preview_live.ex index 8c4ebcdef5..e31156c20a 100644 --- a/lib/dotcom_web/live/preview_live.ex +++ b/lib/dotcom_web/live/preview_live.ex @@ -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", diff --git a/lib/dotcom_web/router.ex b/lib/dotcom_web/router.ex index d589b6b660..1b28b5bf89 100644 --- a/lib/dotcom_web/router.ex +++ b/lib/dotcom_web/router.ex @@ -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 diff --git a/lib/services/service.ex b/lib/services/service.ex index cc93983143..a63ceed19e 100644 --- a/lib/services/service.ex +++ b/lib/services/service.ex @@ -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() diff --git a/livebooks/service_daily_schedules.livemd b/livebooks/service_daily_schedules.livemd new file mode 100644 index 0000000000..755c519d6f --- /dev/null +++ b/livebooks/service_daily_schedules.livemd @@ -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) +```