From 04b3e2ba20363d9c1dfdc5097c61d08f09f309cc Mon Sep 17 00:00:00 2001 From: JD Date: Thu, 15 Jan 2026 10:12:32 -0500 Subject: [PATCH 01/27] feat(trainsformer): edit trainsformer export --- .../components/disruption_components.ex | 11 +++++++ .../edit_trainsformer_export_form.ex | 29 +++++++++++++++++++ .../disruption_v2_view_live.ex | 12 ++++++++ lib/arrow_web/router.ex | 6 ++++ 4 files changed, 58 insertions(+) diff --git a/lib/arrow_web/components/disruption_components.ex b/lib/arrow_web/components/disruption_components.ex index 71e141d3a..b1a37345c 100644 --- a/lib/arrow_web/components/disruption_components.ex +++ b/lib/arrow_web/components/disruption_components.ex @@ -484,6 +484,17 @@ defmodule ArrowWeb.DisruptionComponents do + +
+ <.link + :if={!@editing} + id="edit-disruption-button" + class="grow-0 shrink" + patch={~p"/disruptions/#{@disruption.id}/trainsformer_export/#{export.id}/edit"} + > + <.icon name="hero-pencil-solid" class="bg-primary" /> + +
<% end %> diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index 1ce30caae..97c31e013 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -298,6 +298,35 @@ defmodule ArrowWeb.EditTrainsformerExportForm do {:ok, socket} end + def update(assigns, socket) do + IO.inspect(socket.assigns) + + form = + assigns.export + |> Trainsformer.change_export() + |> to_form() + + socket = + socket + |> assign(assigns) + |> assign(:show_upload_form, true) + |> assign(:show_service_import_form, false) + |> assign(:form, form) + |> assign(:invalid_export_stops, nil) + |> assign(:invalid_stop_times, nil) + |> assign(:one_of_north_south_stations, :ok) + |> assign(:missing_routes, nil) + |> assign(:invalid_routes, nil) + |> assign(:trips_missing_transfers, nil) + |> allow_upload(:trainsformer_export, + accept: ~w(.zip), + progress: &handle_progress/3, + auto_upload: true + ) + + {:ok, socket} + end + @impl true def handle_event("validate", _params, socket) do {:noreply, socket} diff --git a/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex b/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex index f0b9ec19e..1f0208289 100644 --- a/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex +++ b/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex @@ -6,6 +6,7 @@ defmodule ArrowWeb.DisruptionV2ViewLive do alias Arrow.Hastus alias Arrow.Hastus.Export, as: HastusExport alias Arrow.Trainsformer.Export, as: TrainsformerExport + alias Arrow.Trainsformer alias ArrowWeb.DisruptionComponents @impl true @@ -251,6 +252,17 @@ defmodule ArrowWeb.DisruptionV2ViewLive do |> assign(:editing, trainsformer_export) end + defp apply_action(socket, :edit_trainsformer_export, %{"id" => id, "export_id" => export_id}) do + disruption = Disruptions.get_disruption_v2!(id) + trainsformer_export = Trainsformer.get_export!(export_id) + + socket + |> assign(:title, "edit disruption") + |> assign(:page_title, "Edit Disruption v2") + |> assign(:disruption, disruption) + |> assign(:editing, trainsformer_export) + end + defp apply_action(socket, :new_replacement_service, %{"id" => id}) do disruption = Disruptions.get_disruption_v2!(id) replacement_service = %ReplacementService{disruption_id: disruption.id} diff --git a/lib/arrow_web/router.ex b/lib/arrow_web/router.ex index b8a03393f..35a496b5d 100644 --- a/lib/arrow_web/router.ex +++ b/lib/arrow_web/router.ex @@ -71,6 +71,12 @@ defmodule ArrowWeb.Router do :new_trainsformer_export ) + live( + "/disruptions/:id/trainsformer_export/:export_id/edit", + DisruptionV2ViewLive, + :edit_trainsformer_export + ) + live( "/disruptions/:id/hastus_export/:export_id/edit", DisruptionV2ViewLive, From 6f3e994d9e8218860875ec041fa615084054e160 Mon Sep 17 00:00:00 2001 From: JD Date: Tue, 20 Jan 2026 09:37:45 -0500 Subject: [PATCH 02/27] fixup: make edit form display in place of row that is being edited, style trainsformer schedule display --- .../components/disruption_components.ex | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/arrow_web/components/disruption_components.ex b/lib/arrow_web/components/disruption_components.ex index b1a37345c..af888d4c1 100644 --- a/lib/arrow_web/components/disruption_components.ex +++ b/lib/arrow_web/components/disruption_components.ex @@ -444,8 +444,8 @@ defmodule ArrowWeb.DisruptionComponents do - <%= for service <- export.services do %> - + <%= for {service, i} <- Enum.with_index(export.services) do %> + {service.name} @@ -471,30 +471,36 @@ defmodule ArrowWeb.DisruptionComponents do {Calendar.strftime(date, "%m/%d/%Y")} - - - {format_day_name_short(dow)} - + +
+ <.link + :if={!@editing} + id={"edit-export-button-#{export.id}"} + class="btn-sm p-0" + patch={ + ~p"/disruptions/#{@disruption.id}/trainsformer_export/#{export.id}/edit" + } + > + <.icon name="hero-pencil-solid" class="bg-primary" /> + + <.button + :if={!@editing} + id={"delete-export-button-#{export.id}"} + class="btn-sm p-0" + type="button" + phx-click="delete_export" + phx-value-export={export.id} + data-confirm="Are you sure you want to delete this export?" + > + <.icon name="hero-trash-solid" class="bg-primary" /> + +
<% end %> - -
- <.link - :if={!@editing} - id="edit-disruption-button" - class="grow-0 shrink" - patch={~p"/disruptions/#{@disruption.id}/trainsformer_export/#{export.id}/edit"} - > - <.icon name="hero-pencil-solid" class="bg-primary" /> - -
<% end %> From 85ba0ce9180ea3aa47d56d94986f314b21dc5815 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 08:57:33 -0500 Subject: [PATCH 03/27] fixup: working on making service date days of week selectable --- lib/arrow/disruptions.ex | 2 +- lib/arrow/trainsformer.ex | 2 +- lib/arrow/trainsformer/export_upload.ex | 7 +- lib/arrow/trainsformer/service_date.ex | 6 + lib/arrow_web/components/core_components.ex | 46 ++++ .../components/disruption_components.ex | 234 +++++++++-------- .../edit_trainsformer_export_form.ex | 235 ++++++++++++++---- 7 files changed, 375 insertions(+), 157 deletions(-) diff --git a/lib/arrow/disruptions.ex b/lib/arrow/disruptions.ex index a227192be..1a48797a2 100644 --- a/lib/arrow/disruptions.ex +++ b/lib/arrow/disruptions.ex @@ -15,7 +15,7 @@ defmodule Arrow.Disruptions do limits: [:route, :start_stop, :end_stop, :limit_day_of_weeks], replacement_services: [shuttle: [routes: [route_stops: [:stop]]]], hastus_exports: [:line, services: [:service_dates, derived_limits: [:start_stop, :end_stop]]], - trainsformer_exports: [:routes, services: [:service_dates]] + trainsformer_exports: [:routes, services: [service_dates: [:service_date_days_of_week]]] ] @doc """ diff --git a/lib/arrow/trainsformer.ex b/lib/arrow/trainsformer.ex index 2538bce1b..ad9a48366 100644 --- a/lib/arrow/trainsformer.ex +++ b/lib/arrow/trainsformer.ex @@ -12,7 +12,7 @@ defmodule Arrow.Trainsformer do @preloads [ :disruption, :routes, - services: [:service_dates] + services: [service_dates: [:service_date_days_of_week]] ] @doc """ diff --git a/lib/arrow/trainsformer/export_upload.ex b/lib/arrow/trainsformer/export_upload.ex index f2cebd5c6..dd1182431 100644 --- a/lib/arrow/trainsformer/export_upload.ex +++ b/lib/arrow/trainsformer/export_upload.ex @@ -94,6 +94,8 @@ defmodule Arrow.Trainsformer.ExportUpload do current_date_string = Date.to_iso8601(Date.utc_today()) + # note from JD: I don't really like how tightly coupled this is to the frontend (edit_trainsformer_export_form.ex) + # but if you don't set defaults here, the form seems to blow up service_maps = Enum.map(services, fn service_id -> %{ @@ -102,7 +104,8 @@ defmodule Arrow.Trainsformer.ExportUpload do %{ "service_id" => service_id, "start_date" => current_date_string, - "end_date" => current_date_string + "end_date" => current_date_string, + "service_date_days_of_week" => [] } ] } @@ -190,7 +193,7 @@ defmodule Arrow.Trainsformer.ExportUpload do Enum.flat_map(trainsformer_trips, &validate_trip(&1)) if Enum.any?(invalid_stop_times) do - {:error, {:invalid_stop_times, invalid_stop_times}} + :ok else :ok end diff --git a/lib/arrow/trainsformer/service_date.ex b/lib/arrow/trainsformer/service_date.ex index 150773635..4bcbc9bbb 100644 --- a/lib/arrow/trainsformer/service_date.ex +++ b/lib/arrow/trainsformer/service_date.ex @@ -3,17 +3,23 @@ defmodule Arrow.Trainsformer.ServiceDate do use Arrow.Schema import Ecto.Changeset + alias Arrow.Trainsformer.ServiceDateDayOfWeek typed_schema "trainsformer_service_dates" do field :start_date, :date field :end_date, :date belongs_to :service, Arrow.Trainsformer.Service, on_replace: :delete + + has_many :service_date_days_of_week, ServiceDateDayOfWeek, + on_replace: :delete, + foreign_key: :service_date_id end @doc false def changeset(service_date, attrs) do service_date |> cast(attrs, [:start_date, :end_date, :service_id]) + |> cast_assoc(:service_date_days_of_week, with: &ServiceDateDayOfWeek.changeset/2) |> validate_required([:start_date, :end_date]) |> assoc_constraint(:service) end diff --git a/lib/arrow_web/components/core_components.ex b/lib/arrow_web/components/core_components.ex index fb566e75e..0d66dcd16 100644 --- a/lib/arrow_web/components/core_components.ex +++ b/lib/arrow_web/components/core_components.ex @@ -313,6 +313,52 @@ defmodule ArrowWeb.CoreComponents do |> input() end + @doc """ + Multi-select checkbox. Adapted from https://fly.io/phoenix-files/making-a-checkboxgroup-input/ + """ + attr :id, :any + attr :name, :any + attr :label, :string, default: nil + attr :field, Phoenix.HTML.FormField, doc: "..." + attr :errors, :list + attr :required, :boolean, default: false + attr :options, :list, doc: "..." + attr :rest, :global, include: ~w(disabled form readonly) + attr :class, :string, default: nil + attr :value, :list + + def checkgroup(assigns) do + new_assigns = + assigns + |> assign(:multiple, true) + |> assign(:type, "checkgroup") + + input(new_assigns) + end + + def input(%{type: "checkgroup"} = assigns) do + ~H""" +
+
+
+ +
+
+
+ """ + end + def input(%{type: "checkbox"} = assigns) do assigns = assign_new(assigns, :checked, fn -> diff --git a/lib/arrow_web/components/disruption_components.ex b/lib/arrow_web/components/disruption_components.ex index af888d4c1..1e3847ddf 100644 --- a/lib/arrow_web/components/disruption_components.ex +++ b/lib/arrow_web/components/disruption_components.ex @@ -400,109 +400,143 @@ defmodule ArrowWeb.DisruptionComponents do

Trainsformer Service Schedules

<%= if Ecto.assoc_loaded?(@disruption.trainsformer_exports) and Enum.any?(@disruption.trainsformer_exports) do %> -
-

Export: {export.name}

-
-
- Routes - <%= for route <- export.routes do %> -
-
- -
-
-

{route.route_id}

-
+ <%= for export <- @disruption.trainsformer_exports do %> + <%= if @editing && @editing.id == export.id do %> + <.live_component + module={EditTrainsformerExportForm} + id="trainsformer-export-edit-form" + disruption={@disruption} + export={@editing} + icon_paths={@icon_paths} + user_id={@user_id} + /> + <% else %> +
+

Export: {export.name}

+
+
+ Routes + <%= for route <- export.routes do %> +
+
+ +
+
+

{route.route_id}

+
+
+ <% end %>
- <% end %> -
-
- - - - - - - - - - - <%= for {service, i} <- Enum.with_index(export.services) do %> - - - - - - - - <% end %> - -
- Service - - - Start Date - - End Date - - Days of Week -
- {service.name} - - - <.icon name="hero-table-cells" class="bg-primary" />Timetable - - -
- {Calendar.strftime(date, "%a")}. - {Calendar.strftime(date, "%m/%d/%Y")} -
-
-
- {Calendar.strftime(date, "%a")}. - {Calendar.strftime(date, "%m/%d/%Y")} -
-
-
- <.link - :if={!@editing} - id={"edit-export-button-#{export.id}"} - class="btn-sm p-0" - patch={ - ~p"/disruptions/#{@disruption.id}/trainsformer_export/#{export.id}/edit" - } - > - <.icon name="hero-pencil-solid" class="bg-primary" /> - - <.button - :if={!@editing} - id={"delete-export-button-#{export.id}"} - class="btn-sm p-0" - type="button" - phx-click="delete_export" - phx-value-export={export.id} - data-confirm="Are you sure you want to delete this export?" - > - <.icon name="hero-trash-solid" class="bg-primary" /> - -
-
+
+ + + + + + + + + + + <%= for {service, i} <- Enum.with_index(export.services) do %> + + + + + + + + + <% end %> + +
+ Service + + + Start Date + + End Date + + Days of Week +
{service.name} + + <.icon name="hero-table-cells" class="bg-primary" />Timetable + + +
+ {Calendar.strftime(date, "%a")}. + {Calendar.strftime(date, "%m/%d/%Y")} +
+
+
+ {Calendar.strftime(date, "%a")}. + {Calendar.strftime(date, "%m/%d/%Y")} +
+
+
+ + {format_day_name_short(dow)} + +
+
+
+ <.link + :if={!@editing} + id={"edit-export-button-#{export.id}"} + class="btn-sm p-0" + patch={ + ~p"/disruptions/#{@disruption.id}/trainsformer_export/#{export.id}/edit" + } + > + <.icon name="hero-pencil-solid" class="bg-primary" /> + + <.button + :if={!@editing} + id={"delete-export-button-#{export.id}"} + class="btn-sm p-0" + type="button" + phx-click="delete_export" + phx-value-export={export.id} + data-confirm="Are you sure you want to delete this export?" + > + <.icon name="hero-trash-solid" class="bg-primary" /> + +
+
+
+
-
-
+ <% end %> + <% end %> <% end %> <.link diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index 97c31e013..7d0d0fa24 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -8,6 +8,7 @@ defmodule ArrowWeb.EditTrainsformerExportForm do alias Arrow.Trainsformer.Export alias Arrow.Trainsformer.ExportUpload alias Arrow.Trainsformer.ServiceDate + alias Arrow.Trainsformer.ServiceDateDayOfWeek alias Phoenix.LiveView.UploadEntry attr :disruption, DisruptionV2, required: true @@ -117,15 +118,17 @@ defmodule ArrowWeb.EditTrainsformerExportForm do <.input field={@form[:name]} type="text" class="hidden" /> -
- - Successfully imported export {@uploaded_file_name}! - -
+ <%= if is_nil(assigns.export.id) do %> +
+ + Successfully imported export {@uploaded_file_name}! + +
+ <% end %>
Routes - <%= for route <- @uploaded_file_routes do %> + <%= for route <- @uploaded_file_routes || assigns.export.routes do %>
-

{route["route_id"]}

+

{Map.get(route, "route_id") || route.route_id}

<% end %>
- Services + Service ID <.inputs_for :let={f_service} field={@form[:services]}>
-
+
<.input field={f_service[:name]} type="text" class="hidden" /> {f_service[:name].value}
-
+
<.inputs_for :let={f_date} field={f_service[:service_dates]}> {f_service[:start_date].value}
-
<.input field={f_date[:start_date]} type="date" - label={if f_date.index == 0, do: "start date", else: ""} + label="Start Date" />
<.input field={f_date[:end_date]} type="date" - label={if f_date.index == 0, do: "end date", else: ""} + label="End Date" />
-
-
-
- - {format_day_name_short(dow)} - + +
+
+ Days of Week +
+
+ <.checkgroup + field={f_date[:service_date_days_of_week]} + options={ + for dow <- Arrow.Util.DayOfWeek.get_all_day_names() do + {dow |> :erlang.atom_to_binary() |> String.capitalize(), + dow |> :erlang.atom_to_binary()} + end + } + value={ + if Ecto.assoc_loaded?(f_date[:service_date_days_of_week]) do + Enum.map(f_date[:service_date_days_of_week].value, fn dow -> + Atom.to_string(dow.day_name) + end) + else + [] + end + } + phx-change="change_days_of_week" + /> +
-
1} - class="col-auto align-self-center mt-3" - > +
<.button type="button" phx-click="delete_service_date" @@ -197,19 +207,29 @@ defmodule ArrowWeb.EditTrainsformerExportForm do
+
+
+
1} + class="col-auto align-self-center mt-3" + > +
+
-
- <.button - type="button" - class="btn btn-primary ml-3 btn-sm" - value={f_service.index} - phx-click="add_service_date" - phx-target={@myself} - > - Add Another Timeframe - -
+
+ +
+
+ <.button + type="button" + class="btn h-15 w-15 btn-primary btn-sm " + value={f_service.index} + phx-click="add_service_date" + phx-target={@myself} + > + Add Another Timeframe +
@@ -299,8 +319,6 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end def update(assigns, socket) do - IO.inspect(socket.assigns) - form = assigns.export |> Trainsformer.change_export() @@ -309,15 +327,18 @@ defmodule ArrowWeb.EditTrainsformerExportForm do socket = socket |> assign(assigns) - |> assign(:show_upload_form, true) - |> assign(:show_service_import_form, false) + |> assign(:show_upload_form, false) + |> assign(:show_service_import_form, true) |> assign(:form, form) |> assign(:invalid_export_stops, nil) |> assign(:invalid_stop_times, nil) |> assign(:one_of_north_south_stations, :ok) - |> assign(:missing_routes, nil) - |> assign(:invalid_routes, nil) - |> assign(:trips_missing_transfers, nil) + |> assign(:missing_routes, []) + |> assign(:invalid_routes, []) + |> assign(:trips_missing_transfers, []) + |> assign(:error, nil) + |> assign(:uploaded_file_name, nil) + |> assign(:uploaded_file_routes, nil) |> allow_upload(:trainsformer_export, accept: ~w(.zip), progress: &handle_progress/3, @@ -334,13 +355,75 @@ defmodule ArrowWeb.EditTrainsformerExportForm do def handle_event("save", %{"export" => export_params}, socket) do if socket.assigns.export.id do - # Update to be implemented - {:noreply, socket} + update_export(export_params, socket) else create_export(export_params, socket) end end + def handle_event( + "change_days_of_week", + %{ + "_target" => [ + _export, + _services, + service_id_index, + _service_dates, + service_date_index | _ + ], + "export" => %{ + "services" => services + } + }, + socket + ) do + target_days_of_week = + get_in(services, [ + service_id_index, + "service_dates", + service_date_index, + "service_date_days_of_week" + ]) + + formatted_days_of_week = + Enum.map(target_days_of_week, fn day -> + %ServiceDateDayOfWeek{day_name: :erlang.binary_to_existing_atom(day)} + end) + + IO.inspect(formatted_days_of_week) + + socket = + update(socket, :form, fn %{source: changeset} -> + updated_services = + changeset + |> Ecto.Changeset.get_assoc(:services) + |> update_in( + [ + service_id_index |> String.to_integer() |> Access.at() + ], + fn service -> + existing_service_sds = Ecto.Changeset.get_assoc(service, :service_dates, :struct) + IO.inspect(existing_service_sds) + service_date_idx_num = String.to_integer(service_date_index) + target_sd = Enum.at(existing_service_sds, service_date_idx_num) + + Ecto.Changeset.put_change( + service, + :service_dates, + existing_service_sds ++ + %ServiceDate{target_sd | service_date_days_of_week: formatted_days_of_week} + ) + end + ) + + changeset + |> Ecto.Changeset.put_assoc(:services, updated_services) + |> to_form() + end) + + {:noreply, socket} + end + def handle_event( "download_invalid_export_stops", _params, @@ -483,11 +566,57 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end end + defp update_export(export_params, socket) do + imported_services = + for {key, value} <- export_params["services"], + into: %{}, + do: {key, handle_service(value)} + + if imported_services == %{} do + {:noreply, assign(socket, error: "You must import at least one service")} + else + export = Trainsformer.get_export!(socket.assigns.export.id) + + case Trainsformer.update_export(export, export_params) do + {:ok, _} -> + {:noreply, + socket + |> push_patch(to: "/disruptions/#{socket.assigns.disruption.id}") + |> put_flash(:info, "Trainsformer service schedules updated successfully!")} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + end + + # formats input from the day of week multi-select + # so that it works with Arrow.Trainsformer.ServiceDateDayOfWeek.changeset/2 + defp handle_service(%{"service_dates" => service_dates_map} = service) do + fixed_service_dates = + Map.new(service_dates_map, fn + {service_date_id, %{"service_date_days_of_week" => sddow} = service_date} -> + {service_date_id, + %{ + service_date + | "service_date_days_of_week" => + Enum.map(sddow, &%{"day_name" => &1, "service_date_id" => service_date_id}) + }} + + {service_date_id, service_date} -> + {service_date_id, Map.put(service_date, "service_date_days_of_week", [])} + end) + + %{service | "service_dates" => fixed_service_dates} + end + defp create_export(export_params, socket) do imported_services = for {key, value} <- export_params["services"], into: %{}, - do: {key, value} + do: {key, handle_service(value)} + + IO.inspect(imported_services) export_params = Map.put(export_params, "routes", socket.assigns.uploaded_file_routes) From 4f33fe9cbfd212aea94d4c36fc589272b5b9def8 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 08:58:59 -0500 Subject: [PATCH 04/27] fixup: add migrations --- .../trainsformer/service_date_day_of_week.ex | 19 +++++++++++++++++++ ...51940_create_service_date_days_of_week.exs | 14 ++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 lib/arrow/trainsformer/service_date_day_of_week.ex create mode 100644 priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs diff --git a/lib/arrow/trainsformer/service_date_day_of_week.ex b/lib/arrow/trainsformer/service_date_day_of_week.ex new file mode 100644 index 000000000..ba908e9af --- /dev/null +++ b/lib/arrow/trainsformer/service_date_day_of_week.ex @@ -0,0 +1,19 @@ +defmodule Arrow.Trainsformer.ServiceDateDayOfWeek do + use Ecto.Schema + import Ecto.Changeset + + schema "service_date_days_of_week" do + field :day_name, Ecto.Enum, values: Arrow.Util.DayOfWeek.day_name_values() + belongs_to :service_date, Arrow.Trainsformer.ServiceDate, on_replace: :delete + + timestamps() + end + + @doc false + def changeset(service_date_days_of_week, attrs) do + service_date_days_of_week + |> cast(attrs, [:day_name, :service_date_id]) + |> validate_required([:day_name, :service_date_id]) + |> assoc_constraint(:service_date) + end +end diff --git a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs new file mode 100644 index 000000000..668c09a9e --- /dev/null +++ b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs @@ -0,0 +1,14 @@ +defmodule Arrow.Repo.Migrations.CreateTrainsformerServiceDateDaysOfWeek do + use Ecto.Migration + + def change do + create table(:service_date_days_of_week) do + add :day_name, :integer, null: false + add :service_date_id, references(:trainsformer_service_dates, on_delete: :nothing) + + timestamps() + end + + create index(:service_date_days_of_week, [:service_date_id]) + end +end From 4b09dd62fe6a301b474bb031f6158c238a424c30 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 11:29:41 -0500 Subject: [PATCH 05/27] fixup: make upload display correctly --- .../components/disruption_components.ex | 260 +++++++++--------- 1 file changed, 129 insertions(+), 131 deletions(-) diff --git a/lib/arrow_web/components/disruption_components.ex b/lib/arrow_web/components/disruption_components.ex index 1e3847ddf..6d8485812 100644 --- a/lib/arrow_web/components/disruption_components.ex +++ b/lib/arrow_web/components/disruption_components.ex @@ -399,143 +399,141 @@ defmodule ArrowWeb.DisruptionComponents do ~H"""

Trainsformer Service Schedules

- <%= if Ecto.assoc_loaded?(@disruption.trainsformer_exports) and Enum.any?(@disruption.trainsformer_exports) do %> - <%= for export <- @disruption.trainsformer_exports do %> - <%= if @editing && @editing.id == export.id do %> - <.live_component - module={EditTrainsformerExportForm} - id="trainsformer-export-edit-form" - disruption={@disruption} - export={@editing} - icon_paths={@icon_paths} - user_id={@user_id} - /> - <% else %> -
-

Export: {export.name}

-
-
- Routes - <%= for route <- export.routes do %> -
-
- -
-
-

{route.route_id}

-
+ <%= for export <- @disruption.trainsformer_exports do %> + <%= if @editing && @editing.id == export.id do %> + <.live_component + module={EditTrainsformerExportForm} + id="trainsformer-export-edit-form" + disruption={@disruption} + export={@editing} + icon_paths={@icon_paths} + user_id={@user_id} + /> + <% else %> +
+

Export: {export.name}

+
+
+ Routes + <%= for route <- export.routes do %> +
+
+
- <% end %> -
+
+

{route.route_id}

+
+
+ <% end %> +
-
- - - - - - - - - - - <%= for {service, i} <- Enum.with_index(export.services) do %> - - - + + + <% end %> + +
- Service - - - Start Date - - End Date - - Days of Week -
{service.name} - + + + + + + + + + + + <%= for {service, i} <- Enum.with_index(export.services) do %> + + + + + + - - - - - - <% end %> - -
+ Service + + + Start Date + + End Date + + Days of Week +
{service.name} + + <.icon name="hero-table-cells" class="bg-primary" />Timetable + + +
+ {Calendar.strftime(date, "%a")}. + {Calendar.strftime(date, "%m/%d/%Y")} +
+
+
+ {Calendar.strftime(date, "%a")}. + {Calendar.strftime(date, "%m/%d/%Y")} +
+
+
+ - <.icon name="hero-table-cells" class="bg-primary" />Timetable - -
-
- {Calendar.strftime(date, "%a")}. - {Calendar.strftime(date, "%m/%d/%Y")} -
-
-
- {Calendar.strftime(date, "%a")}. - {Calendar.strftime(date, "%m/%d/%Y")} -
-
-
- - {format_day_name_short(dow)} - -
-
-
- <.link - :if={!@editing} - id={"edit-export-button-#{export.id}"} - class="btn-sm p-0" - patch={ - ~p"/disruptions/#{@disruption.id}/trainsformer_export/#{export.id}/edit" - } - > - <.icon name="hero-pencil-solid" class="bg-primary" /> - - <.button - :if={!@editing} - id={"delete-export-button-#{export.id}"} - class="btn-sm p-0" - type="button" - phx-click="delete_export" - phx-value-export={export.id} - data-confirm="Are you sure you want to delete this export?" - > - <.icon name="hero-trash-solid" class="bg-primary" /> - -
-
- + {format_day_name_short(dow)} + + +
+
+ <.link + :if={!@editing} + id={"edit-export-button-#{export.id}"} + class="btn-sm p-0" + patch={ + ~p"/disruptions/#{@disruption.id}/trainsformer_export/#{export.id}/edit" + } + > + <.icon name="hero-pencil-solid" class="bg-primary" /> + + <.button + :if={!@editing} + id={"delete-export-button-#{export.id}"} + class="btn-sm p-0" + type="button" + phx-click="delete_export" + phx-value-export={export.id} + data-confirm="Are you sure you want to delete this export?" + > + <.icon name="hero-trash-solid" class="bg-primary" /> + +
+
- <% end %> +
<% end %> <% end %> From 742851903f31bc11d8744195ca74340c232b9ec5 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 11:30:24 -0500 Subject: [PATCH 06/27] fixup: add multi select checkbox, debug statements --- lib/arrow/trainsformer/service_date.ex | 16 +++ lib/arrow_web/components/core_components.ex | 48 ++++---- .../edit_trainsformer_export_form.ex | 110 ++++++++++-------- 3 files changed, 104 insertions(+), 70 deletions(-) diff --git a/lib/arrow/trainsformer/service_date.ex b/lib/arrow/trainsformer/service_date.ex index 4bcbc9bbb..e03893a44 100644 --- a/lib/arrow/trainsformer/service_date.ex +++ b/lib/arrow/trainsformer/service_date.ex @@ -17,6 +17,22 @@ defmodule Arrow.Trainsformer.ServiceDate do @doc false def changeset(service_date, attrs) do + attrs = + with %{"service_date_days_of_week" => sddow} <- attrs, + [_ | _] = sddow do + formatted_days = + Enum.map(sddow, fn day -> + %{ + "day_name" => day, + "service_date_id" => service_date.id + } + end) + + %{attrs | "service_date_days_of_week" => formatted_days} + else + _ -> attrs + end + service_date |> cast(attrs, [:start_date, :end_date, :service_id]) |> cast_assoc(:service_date_days_of_week, with: &ServiceDateDayOfWeek.changeset/2) diff --git a/lib/arrow_web/components/core_components.ex b/lib/arrow_web/components/core_components.ex index 0d66dcd16..999c530c8 100644 --- a/lib/arrow_web/components/core_components.ex +++ b/lib/arrow_web/components/core_components.ex @@ -250,6 +250,29 @@ defmodule ArrowWeb.CoreComponents do """ end + @doc """ + Multi-select checkbox. Adapted from https://fly.io/phoenix-files/making-a-checkboxgroup-input/ + """ + attr :id, :any + attr :name, :any + attr :label, :string, default: nil + attr :field, Phoenix.HTML.FormField + attr :errors, :list + attr :required, :boolean, default: false + attr :options, :list + attr :rest, :global, include: ~w(disabled form readonly) + attr :class, :string, default: nil + attr :value, :list + + def checkgroup(assigns) do + new_assigns = + assigns + |> assign(:multiple, true) + |> assign(:type, "checkgroup") + + input(new_assigns) + end + @doc """ Renders an input with label and error messages. @@ -313,30 +336,9 @@ defmodule ArrowWeb.CoreComponents do |> input() end - @doc """ - Multi-select checkbox. Adapted from https://fly.io/phoenix-files/making-a-checkboxgroup-input/ - """ - attr :id, :any - attr :name, :any - attr :label, :string, default: nil - attr :field, Phoenix.HTML.FormField, doc: "..." - attr :errors, :list - attr :required, :boolean, default: false - attr :options, :list, doc: "..." - attr :rest, :global, include: ~w(disabled form readonly) - attr :class, :string, default: nil - attr :value, :list - - def checkgroup(assigns) do - new_assigns = - assigns - |> assign(:multiple, true) - |> assign(:type, "checkgroup") - - input(new_assigns) - end - def input(%{type: "checkgroup"} = assigns) do + dbg(assigns.field) + ~H"""
diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index 7d0d0fa24..f5ae3bf88 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -184,14 +184,21 @@ defmodule ArrowWeb.EditTrainsformerExportForm do } value={ if Ecto.assoc_loaded?(f_date[:service_date_days_of_week]) do - Enum.map(f_date[:service_date_days_of_week].value, fn dow -> - Atom.to_string(dow.day_name) + Enum.map(f_date[:service_date_days_of_week].value, fn + %ServiceDateDayOfWeek{day_name: day_name} -> + Atom.to_string(day_name) + + %Ecto.Changeset{data: %ServiceDateDayOfWeek{day_name: day_name}} -> + Atom.to_string(day_name) + + str -> + str end) else [] end + |> dbg() } - phx-change="change_days_of_week" />
@@ -349,6 +356,15 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end @impl true + def handle_event("validate", %{"export" => export_params}, socket) do + # form = + # socket.assigns.export + # |> Trainsformer.change_export(export_params) + # |> to_form(action: :validate) + + {:noreply, socket} + end + def handle_event("validate", _params, socket) do {:noreply, socket} end @@ -377,50 +393,50 @@ defmodule ArrowWeb.EditTrainsformerExportForm do }, socket ) do - target_days_of_week = - get_in(services, [ - service_id_index, - "service_dates", - service_date_index, - "service_date_days_of_week" - ]) - - formatted_days_of_week = - Enum.map(target_days_of_week, fn day -> - %ServiceDateDayOfWeek{day_name: :erlang.binary_to_existing_atom(day)} - end) - - IO.inspect(formatted_days_of_week) - - socket = - update(socket, :form, fn %{source: changeset} -> - updated_services = - changeset - |> Ecto.Changeset.get_assoc(:services) - |> update_in( - [ - service_id_index |> String.to_integer() |> Access.at() - ], - fn service -> - existing_service_sds = Ecto.Changeset.get_assoc(service, :service_dates, :struct) - IO.inspect(existing_service_sds) - service_date_idx_num = String.to_integer(service_date_index) - target_sd = Enum.at(existing_service_sds, service_date_idx_num) - - Ecto.Changeset.put_change( - service, - :service_dates, - existing_service_sds ++ - %ServiceDate{target_sd | service_date_days_of_week: formatted_days_of_week} - ) - end - ) - - changeset - |> Ecto.Changeset.put_assoc(:services, updated_services) - |> to_form() - end) - + # target_days_of_week = + # get_in(services, [ + # service_id_index, + # "service_dates", + # service_date_index, + # "service_date_days_of_week" + # ]) + # + # formatted_days_of_week = + # Enum.map(target_days_of_week, fn day -> + # %ServiceDateDayOfWeek{day_name: :erlang.binary_to_existing_atom(day)} + # end) + # + # IO.inspect(formatted_days_of_week) + # + # socket = + # update(socket, :form, fn %{source: changeset} -> + # updated_services = + # changeset + # |> Ecto.Changeset.get_assoc(:services) + # |> update_in( + # [ + # service_id_index |> String.to_integer() |> Access.at() + # ], + # fn service -> + # existing_service_sds = Ecto.Changeset.get_assoc(service, :service_dates, :struct) + # IO.inspect(existing_service_sds) + # service_date_idx_num = String.to_integer(service_date_index) + # target_sd = Enum.at(existing_service_sds, service_date_idx_num) + # + # Ecto.Changeset.put_change( + # service, + # :service_dates, + # existing_service_sds ++ + # %ServiceDate{target_sd | service_date_days_of_week: formatted_days_of_week} + # ) + # end + # ) + # + # changeset + # |> Ecto.Changeset.put_assoc(:services, updated_services) + # |> to_form() + # end) + # {:noreply, socket} end From df65d58114a85709ade0d8b94a41625cc35f0879 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 11:50:43 -0500 Subject: [PATCH 07/27] fixup: fix adding service timeframe --- lib/arrow/trainsformer/service_date.ex | 2 +- lib/arrow_web/components/edit_trainsformer_export_form.ex | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/arrow/trainsformer/service_date.ex b/lib/arrow/trainsformer/service_date.ex index e03893a44..e4413ba7b 100644 --- a/lib/arrow/trainsformer/service_date.ex +++ b/lib/arrow/trainsformer/service_date.ex @@ -19,7 +19,7 @@ defmodule Arrow.Trainsformer.ServiceDate do def changeset(service_date, attrs) do attrs = with %{"service_date_days_of_week" => sddow} <- attrs, - [_ | _] = sddow do + [_ | _] <- sddow do formatted_days = Enum.map(sddow, fn day -> %{ diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index f5ae3bf88..58f238f9e 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -487,7 +487,12 @@ defmodule ArrowWeb.EditTrainsformerExportForm do |> Ecto.Changeset.get_assoc(:services) |> update_in([Access.at(index)], fn service -> existing_dates = Ecto.Changeset.get_assoc(service, :service_dates) - Ecto.Changeset.put_change(service, :service_dates, existing_dates ++ [%ServiceDate{}]) + + Ecto.Changeset.put_change( + service, + :service_dates, + existing_dates ++ [%ServiceDate{service_date_days_of_week: []}] + ) end) changeset From 25f48fac0f383c1534cead24d01a4be9543c112c Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 14:40:01 -0500 Subject: [PATCH 08/27] fixup: add delete_all to migration --- .../20260120151940_create_service_date_days_of_week.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs index 668c09a9e..13a7ea40e 100644 --- a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs +++ b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs @@ -4,7 +4,7 @@ defmodule Arrow.Repo.Migrations.CreateTrainsformerServiceDateDaysOfWeek do def change do create table(:service_date_days_of_week) do add :day_name, :integer, null: false - add :service_date_id, references(:trainsformer_service_dates, on_delete: :nothing) + add :service_date_id, references(:trainsformer_service_dates, on_delete: :delete_all) timestamps() end From 165517e33e1f82e928c93c1ad7de806e2f1a87a4 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 14:40:26 -0500 Subject: [PATCH 09/27] fixup: also add delete_all to schema --- lib/arrow/trainsformer/service_date.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/arrow/trainsformer/service_date.ex b/lib/arrow/trainsformer/service_date.ex index e4413ba7b..4d476d360 100644 --- a/lib/arrow/trainsformer/service_date.ex +++ b/lib/arrow/trainsformer/service_date.ex @@ -11,6 +11,7 @@ defmodule Arrow.Trainsformer.ServiceDate do belongs_to :service, Arrow.Trainsformer.Service, on_replace: :delete has_many :service_date_days_of_week, ServiceDateDayOfWeek, + on_delete: :delete_all, on_replace: :delete, foreign_key: :service_date_id end @@ -23,8 +24,7 @@ defmodule Arrow.Trainsformer.ServiceDate do formatted_days = Enum.map(sddow, fn day -> %{ - "day_name" => day, - "service_date_id" => service_date.id + "day_name" => day } end) From 65e01f8eec24ea0aea55919e1720dffce620b098 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 14:41:08 -0500 Subject: [PATCH 10/27] fixup: use Phoenix.Form.input_value, no need to match against changeset --- .../edit_trainsformer_export_form.ex | 48 +++---------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index 58f238f9e..c3fd12790 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -183,21 +183,16 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end } value={ - if Ecto.assoc_loaded?(f_date[:service_date_days_of_week]) do - Enum.map(f_date[:service_date_days_of_week].value, fn + Enum.map( + Phoenix.HTML.Form.input_value(f_date, :service_date_days_of_week), + fn %ServiceDateDayOfWeek{day_name: day_name} -> Atom.to_string(day_name) - %Ecto.Changeset{data: %ServiceDateDayOfWeek{day_name: day_name}} -> - Atom.to_string(day_name) - str -> str - end) - else - [] - end - |> dbg() + end + ) } />
@@ -356,15 +351,6 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end @impl true - def handle_event("validate", %{"export" => export_params}, socket) do - # form = - # socket.assigns.export - # |> Trainsformer.change_export(export_params) - # |> to_form(action: :validate) - - {:noreply, socket} - end - def handle_event("validate", _params, socket) do {:noreply, socket} end @@ -591,7 +577,7 @@ defmodule ArrowWeb.EditTrainsformerExportForm do imported_services = for {key, value} <- export_params["services"], into: %{}, - do: {key, handle_service(value)} + do: {key, value} if imported_services == %{} do {:noreply, assign(socket, error: "You must import at least one service")} @@ -611,31 +597,11 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end end - # formats input from the day of week multi-select - # so that it works with Arrow.Trainsformer.ServiceDateDayOfWeek.changeset/2 - defp handle_service(%{"service_dates" => service_dates_map} = service) do - fixed_service_dates = - Map.new(service_dates_map, fn - {service_date_id, %{"service_date_days_of_week" => sddow} = service_date} -> - {service_date_id, - %{ - service_date - | "service_date_days_of_week" => - Enum.map(sddow, &%{"day_name" => &1, "service_date_id" => service_date_id}) - }} - - {service_date_id, service_date} -> - {service_date_id, Map.put(service_date, "service_date_days_of_week", [])} - end) - - %{service | "service_dates" => fixed_service_dates} - end - defp create_export(export_params, socket) do imported_services = for {key, value} <- export_params["services"], into: %{}, - do: {key, handle_service(value)} + do: {key, value} IO.inspect(imported_services) From 3c12d473322cb52b455ca817376c155256edbb2a Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 14:44:20 -0500 Subject: [PATCH 11/27] fixup: no need to cast service_date_id --- .../trainsformer/service_date_day_of_week.ex | 4 +-- .../components/disruption_components.ex | 28 +++++++++---------- .../disruption_v2_view_live.ex | 23 +++++++++++++++ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/lib/arrow/trainsformer/service_date_day_of_week.ex b/lib/arrow/trainsformer/service_date_day_of_week.ex index ba908e9af..50e4545c1 100644 --- a/lib/arrow/trainsformer/service_date_day_of_week.ex +++ b/lib/arrow/trainsformer/service_date_day_of_week.ex @@ -12,8 +12,8 @@ defmodule Arrow.Trainsformer.ServiceDateDayOfWeek do @doc false def changeset(service_date_days_of_week, attrs) do service_date_days_of_week - |> cast(attrs, [:day_name, :service_date_id]) - |> validate_required([:day_name, :service_date_id]) + |> cast(attrs, [:day_name]) + |> validate_required([:day_name]) |> assoc_constraint(:service_date) end end diff --git a/lib/arrow_web/components/disruption_components.ex b/lib/arrow_web/components/disruption_components.ex index 6d8485812..b2b5853b7 100644 --- a/lib/arrow_web/components/disruption_components.ex +++ b/lib/arrow_web/components/disruption_components.ex @@ -436,21 +436,19 @@ defmodule ArrowWeb.DisruptionComponents do
- - - - - - + + + + <%= for {service, i} <- Enum.with_index(export.services) do %> diff --git a/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex b/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex index 1f0208289..15af9f5ce 100644 --- a/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex +++ b/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex @@ -133,6 +133,29 @@ defmodule ArrowWeb.DisruptionV2ViewLive do end end + def handle_event("delete_trainsformer_export", %{"export" => export_id}, socket) do + dbg() + {parsed_id, _} = Integer.parse(export_id) + export = Trainsformer.get_export!(parsed_id) + + case Trainsformer.delete_export(export) do + {:ok, _} -> + disruption = %{ + socket.assigns.disruption + | trainsformer_exports: + Enum.reject(socket.assigns.disruption.trainsformer_exports, &(&1.id == parsed_id)) + } + + {:noreply, + socket + |> assign(:disruption, disruption) + |> put_flash(:info, "Export deleted successfully")} + + {:error, %Ecto.Changeset{} = _changeset} -> + {:noreply, put_flash(socket, :error, "Error when deleting export!")} + end + end + def handle_event( "delete_replacement_service", %{"replacement_service" => replacement_service_id}, From 385589a1a32a1e3b7708373a9e00d93a7e0b714b Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 14:45:19 -0500 Subject: [PATCH 12/27] fixup: remove unused event --- .../edit_trainsformer_export_form.ex | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index c3fd12790..017fd204f 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -363,69 +363,6 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end end - def handle_event( - "change_days_of_week", - %{ - "_target" => [ - _export, - _services, - service_id_index, - _service_dates, - service_date_index | _ - ], - "export" => %{ - "services" => services - } - }, - socket - ) do - # target_days_of_week = - # get_in(services, [ - # service_id_index, - # "service_dates", - # service_date_index, - # "service_date_days_of_week" - # ]) - # - # formatted_days_of_week = - # Enum.map(target_days_of_week, fn day -> - # %ServiceDateDayOfWeek{day_name: :erlang.binary_to_existing_atom(day)} - # end) - # - # IO.inspect(formatted_days_of_week) - # - # socket = - # update(socket, :form, fn %{source: changeset} -> - # updated_services = - # changeset - # |> Ecto.Changeset.get_assoc(:services) - # |> update_in( - # [ - # service_id_index |> String.to_integer() |> Access.at() - # ], - # fn service -> - # existing_service_sds = Ecto.Changeset.get_assoc(service, :service_dates, :struct) - # IO.inspect(existing_service_sds) - # service_date_idx_num = String.to_integer(service_date_index) - # target_sd = Enum.at(existing_service_sds, service_date_idx_num) - # - # Ecto.Changeset.put_change( - # service, - # :service_dates, - # existing_service_sds ++ - # %ServiceDate{target_sd | service_date_days_of_week: formatted_days_of_week} - # ) - # end - # ) - # - # changeset - # |> Ecto.Changeset.put_assoc(:services, updated_services) - # |> to_form() - # end) - # - {:noreply, socket} - end - def handle_event( "download_invalid_export_stops", _params, From 749ea28781eaa8b83f08794af6016c7213caedc3 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 14:48:14 -0500 Subject: [PATCH 13/27] fixup: credo --- lib/arrow/trainsformer/service_date_day_of_week.ex | 2 ++ lib/arrow_web/components/core_components.ex | 2 -- lib/arrow_web/components/edit_trainsformer_export_form.ex | 2 -- .../live/disruption_v2_live/disruption_v2_view_live.ex | 3 +-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/arrow/trainsformer/service_date_day_of_week.ex b/lib/arrow/trainsformer/service_date_day_of_week.ex index 50e4545c1..f3f06aae0 100644 --- a/lib/arrow/trainsformer/service_date_day_of_week.ex +++ b/lib/arrow/trainsformer/service_date_day_of_week.ex @@ -1,4 +1,6 @@ defmodule Arrow.Trainsformer.ServiceDateDayOfWeek do + @moduledoc "Describes the days of week for which a service should be active, within some date range" + use Ecto.Schema import Ecto.Changeset diff --git a/lib/arrow_web/components/core_components.ex b/lib/arrow_web/components/core_components.ex index 999c530c8..37e9a98a6 100644 --- a/lib/arrow_web/components/core_components.ex +++ b/lib/arrow_web/components/core_components.ex @@ -337,8 +337,6 @@ defmodule ArrowWeb.CoreComponents do end def input(%{type: "checkgroup"} = assigns) do - dbg(assigns.field) - ~H"""
diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index 017fd204f..5afbede7c 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -540,8 +540,6 @@ defmodule ArrowWeb.EditTrainsformerExportForm do into: %{}, do: {key, value} - IO.inspect(imported_services) - export_params = Map.put(export_params, "routes", socket.assigns.uploaded_file_routes) with {:ok, s3_path} <- diff --git a/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex b/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex index 15af9f5ce..9f1de0da9 100644 --- a/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex +++ b/lib/arrow_web/live/disruption_v2_live/disruption_v2_view_live.ex @@ -5,8 +5,8 @@ defmodule ArrowWeb.DisruptionV2ViewLive do alias Arrow.Disruptions.{DisruptionV2, Limit, ReplacementService} alias Arrow.Hastus alias Arrow.Hastus.Export, as: HastusExport - alias Arrow.Trainsformer.Export, as: TrainsformerExport alias Arrow.Trainsformer + alias Arrow.Trainsformer.Export, as: TrainsformerExport alias ArrowWeb.DisruptionComponents @impl true @@ -134,7 +134,6 @@ defmodule ArrowWeb.DisruptionV2ViewLive do end def handle_event("delete_trainsformer_export", %{"export" => export_id}, socket) do - dbg() {parsed_id, _} = Integer.parse(export_id) export = Trainsformer.get_export!(parsed_id) From 26f40dc10ae923e7b4c8880c696782015c78638f Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 15:16:00 -0500 Subject: [PATCH 14/27] fixup: fixing tests --- lib/arrow/trainsformer.ex | 3 ++- lib/arrow/trainsformer/export_upload.ex | 2 +- lib/arrow/trainsformer/service.ex | 2 +- test/support/fixtures/trainsformer_fixtures.ex | 10 ++++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/arrow/trainsformer.ex b/lib/arrow/trainsformer.ex index ad9a48366..a93a213a4 100644 --- a/lib/arrow/trainsformer.ex +++ b/lib/arrow/trainsformer.ex @@ -229,7 +229,8 @@ defmodule Arrow.Trainsformer do ** (Ecto.NoResultsError) """ - def get_service_date!(id), do: Repo.get!(ServiceDate, id) + def get_service_date!(id), + do: Repo.get!(ServiceDate, id) |> Repo.preload(:service_date_days_of_week) @doc """ Creates a service_date. diff --git a/lib/arrow/trainsformer/export_upload.ex b/lib/arrow/trainsformer/export_upload.ex index dd1182431..f0632eef7 100644 --- a/lib/arrow/trainsformer/export_upload.ex +++ b/lib/arrow/trainsformer/export_upload.ex @@ -193,7 +193,7 @@ defmodule Arrow.Trainsformer.ExportUpload do Enum.flat_map(trainsformer_trips, &validate_trip(&1)) if Enum.any?(invalid_stop_times) do - :ok + {:error, {:invalid_stop_times, invalid_stop_times}} else :ok end diff --git a/lib/arrow/trainsformer/service.ex b/lib/arrow/trainsformer/service.ex index 35458f4c1..46e08bff9 100644 --- a/lib/arrow/trainsformer/service.ex +++ b/lib/arrow/trainsformer/service.ex @@ -14,7 +14,7 @@ defmodule Arrow.Trainsformer.Service do on_replace: :delete, foreign_key: :service_id - belongs_to :export, Export + belongs_to :export, Export, on_replace: :delete, foreign_key: :export_id timestamps(type: :utc_datetime) end diff --git a/test/support/fixtures/trainsformer_fixtures.ex b/test/support/fixtures/trainsformer_fixtures.ex index 0340c702e..f1ef9890d 100644 --- a/test/support/fixtures/trainsformer_fixtures.ex +++ b/test/support/fixtures/trainsformer_fixtures.ex @@ -23,8 +23,9 @@ defmodule Arrow.TrainsformerFixtures do name: "SPRING2025-SOUTHSS-Weekend-31A", service_dates: [ %{ - start_date: ~D[2026-01-26], - end_date: ~D[2026-01-26] + "service_date_days_of_week" => ["monday"], + "start_date" => "2026-01-26", + "end_date" => "2026-01-26" } ] } @@ -56,8 +57,9 @@ defmodule Arrow.TrainsformerFixtures do {:ok, service_date} = attrs |> Enum.into(%{ - end_date: ~D[2025-03-11], - start_date: ~D[2025-03-11] + "service_date_days_of_week" => ["monday"], + "start_date" => "2026-01-26", + "end_date" => "2026-01-26" }) |> Arrow.Trainsformer.create_service_date() From b5d2636e5764972900dd06af9e6bb300ebde391f Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 21 Jan 2026 15:50:20 -0500 Subject: [PATCH 15/27] fixup: credo'd --- lib/arrow/trainsformer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/arrow/trainsformer.ex b/lib/arrow/trainsformer.ex index a93a213a4..3ca013a36 100644 --- a/lib/arrow/trainsformer.ex +++ b/lib/arrow/trainsformer.ex @@ -230,7 +230,7 @@ defmodule Arrow.Trainsformer do """ def get_service_date!(id), - do: Repo.get!(ServiceDate, id) |> Repo.preload(:service_date_days_of_week) + do: id |> Repo.get!(ServiceDate) |> Repo.preload(:service_date_days_of_week) @doc """ Creates a service_date. From 1f62f9e829ca71035cf5d85012aac01f99d9ebef Mon Sep 17 00:00:00 2001 From: JD Date: Thu, 22 Jan 2026 11:50:00 -0500 Subject: [PATCH 16/27] fixup: make integration tests pass --- lib/arrow/disruptions/disruption_v2.ex | 1 + .../components/disruption_components.ex | 21 +- .../edit_trainsformer_export_form.ex | 39 ++- priv/repo/structure.sql | 258 +++++++++++++++++- .../trainsformer_export_section_test.exs | 60 ++++ 5 files changed, 353 insertions(+), 26 deletions(-) diff --git a/lib/arrow/disruptions/disruption_v2.ex b/lib/arrow/disruptions/disruption_v2.ex index 7a4f7a55c..158cf1ef6 100644 --- a/lib/arrow/disruptions/disruption_v2.ex +++ b/lib/arrow/disruptions/disruption_v2.ex @@ -51,6 +51,7 @@ defmodule Arrow.Disruptions.DisruptionV2 do |> cast(attrs, [:mode], force_changes: true) |> cast_assoc(:limits, with: &Limit.changeset/2) |> cast_assoc(:replacement_services, with: &ReplacementService.changeset/2) + |> cast_assoc(:trainsformer_exports, with: &Arrow.Trainsformer.Export.changeset/2) |> validate_required([:title, :mode, :is_active]) |> validate_required_for(:is_active) |> validate_no_mode_change() diff --git a/lib/arrow_web/components/disruption_components.ex b/lib/arrow_web/components/disruption_components.ex index b2b5853b7..6a0ac5640 100644 --- a/lib/arrow_web/components/disruption_components.ex +++ b/lib/arrow_web/components/disruption_components.ex @@ -454,6 +454,7 @@ defmodule ArrowWeb.DisruptionComponents do <%= for {service, i} <- Enum.with_index(export.services) do %>
+
- Service - - - Start Date - - End Date - - Days of Week -
+ Service ID + + + Start Date + + End Date + + Days of Week +
{service.name} -
+
+ <% active_dows = + date + |> Kernel.get_in([ + Access.key(:service_date_days_of_week), + Access.all(), + Access.key(:day_name) + ]) %> {format_day_name_short(dow)} diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index 5afbede7c..5a05cb502 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -183,16 +183,25 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end } value={ - Enum.map( - Phoenix.HTML.Form.input_value(f_date, :service_date_days_of_week), - fn - %ServiceDateDayOfWeek{day_name: day_name} -> - Atom.to_string(day_name) - - str -> - str - end - ) + if Ecto.assoc_loaded?(f_date[:service_date_days_of_week].value) do + Enum.map( + f_date[:service_date_days_of_week].value, + fn + %ServiceDateDayOfWeek{day_name: day_name} -> + Atom.to_string(day_name) + + %Ecto.Changeset{} = changeset -> + changeset + |> Ecto.Changeset.get_field(:day_name) + |> Atom.to_string() + + str -> + str + end + ) + else + [] + end } />
@@ -350,6 +359,16 @@ defmodule ArrowWeb.EditTrainsformerExportForm do {:ok, socket} end + @impl true + def handle_event("validate", %{"export" => export_params}, socket) do + form = + socket.assigns.export + |> Trainsformer.change_export(export_params) + |> to_form(action: :validate) + + {:noreply, assign(socket, form: form)} + end + @impl true def handle_event("validate", _params, socket) do {:noreply, socket} diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 7ef4a8476..d02fd674f 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2,10 +2,10 @@ -- PostgreSQL database dump -- -\restrict 2U5xctdYZPzR7MiFLT6ex6uTDnp2eF7UGFIpPgaVW1X1NJYXsQ9bDXJi8zrFfC9 +\restrict 162USDnzTVH6AoDpkAVnueTQTvFnNunyreal1sV0AuOxo1ZOXxE0kLpJYW5sHY6 --- Dumped from database version 15.14 (Postgres.app) --- Dumped by pg_dump version 15.14 (Postgres.app) +-- Dumped from database version 15.15 (Debian 15.15-1.pgdg13+1) +-- Dumped by pg_dump version 15.15 (Homebrew) SET statement_timeout = 0; SET lock_timeout = 0; @@ -1084,6 +1084,38 @@ CREATE TABLE public.schema_migrations ( ); +-- +-- Name: service_date_days_of_week; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.service_date_days_of_week ( + id bigint NOT NULL, + day_name integer NOT NULL, + service_date_id bigint, + inserted_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: service_date_days_of_week_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.service_date_days_of_week_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: service_date_days_of_week_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.service_date_days_of_week_id_seq OWNED BY public.service_date_days_of_week.id; + + -- -- Name: shapes; Type: TABLE; Schema: public; Owner: - -- @@ -1268,6 +1300,36 @@ CREATE SEQUENCE public.stops_id_seq ALTER SEQUENCE public.stops_id_seq OWNED BY public.stops.id; +-- +-- Name: trainsformer_export_routes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.trainsformer_export_routes ( + id bigint NOT NULL, + route_id character varying(255), + export_id bigint +); + + +-- +-- Name: trainsformer_export_routes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.trainsformer_export_routes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: trainsformer_export_routes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.trainsformer_export_routes_id_seq OWNED BY public.trainsformer_export_routes.id; + + -- -- Name: trainsformer_exports; Type: TABLE; Schema: public; Owner: - -- @@ -1277,7 +1339,8 @@ CREATE TABLE public.trainsformer_exports ( s3_path text, disruption_id bigint, inserted_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + name character varying(255) ); @@ -1300,6 +1363,69 @@ CREATE SEQUENCE public.trainsformer_exports_id_seq ALTER SEQUENCE public.trainsformer_exports_id_seq OWNED BY public.trainsformer_exports.id; +-- +-- Name: trainsformer_service_dates; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.trainsformer_service_dates ( + id bigint NOT NULL, + start_date date, + end_date date, + service_id bigint +); + + +-- +-- Name: trainsformer_service_dates_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.trainsformer_service_dates_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: trainsformer_service_dates_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.trainsformer_service_dates_id_seq OWNED BY public.trainsformer_service_dates.id; + + +-- +-- Name: trainsformer_services; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.trainsformer_services ( + id bigint NOT NULL, + name character varying(255), + export_id bigint, + inserted_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + + +-- +-- Name: trainsformer_services_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.trainsformer_services_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: trainsformer_services_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.trainsformer_services_id_seq OWNED BY public.trainsformer_services.id; + + -- -- Name: adjustments id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1433,6 +1559,13 @@ ALTER TABLE ONLY public.oban_jobs ALTER COLUMN id SET DEFAULT nextval('public.ob ALTER TABLE ONLY public.replacement_services ALTER COLUMN id SET DEFAULT nextval('public.replacement_services_id_seq'::regclass); +-- +-- Name: service_date_days_of_week id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.service_date_days_of_week ALTER COLUMN id SET DEFAULT nextval('public.service_date_days_of_week_id_seq'::regclass); + + -- -- Name: shapes id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1468,6 +1601,13 @@ ALTER TABLE ONLY public.shuttles ALTER COLUMN id SET DEFAULT nextval('public.shu ALTER TABLE ONLY public.stops ALTER COLUMN id SET DEFAULT nextval('public.stops_id_seq'::regclass); +-- +-- Name: trainsformer_export_routes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_export_routes ALTER COLUMN id SET DEFAULT nextval('public.trainsformer_export_routes_id_seq'::regclass); + + -- -- Name: trainsformer_exports id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1475,6 +1615,20 @@ ALTER TABLE ONLY public.stops ALTER COLUMN id SET DEFAULT nextval('public.stops_ ALTER TABLE ONLY public.trainsformer_exports ALTER COLUMN id SET DEFAULT nextval('public.trainsformer_exports_id_seq'::regclass); +-- +-- Name: trainsformer_service_dates id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_service_dates ALTER COLUMN id SET DEFAULT nextval('public.trainsformer_service_dates_id_seq'::regclass); + + +-- +-- Name: trainsformer_services id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_services ALTER COLUMN id SET DEFAULT nextval('public.trainsformer_services_id_seq'::regclass); + + -- -- Name: adjustments adjustments_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1787,6 +1941,14 @@ ALTER TABLE ONLY public.schema_migrations ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); +-- +-- Name: service_date_days_of_week service_date_days_of_week_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.service_date_days_of_week + ADD CONSTRAINT service_date_days_of_week_pkey PRIMARY KEY (id); + + -- -- Name: shapes shapes_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1827,6 +1989,14 @@ ALTER TABLE ONLY public.stops ADD CONSTRAINT stops_pkey PRIMARY KEY (id); +-- +-- Name: trainsformer_export_routes trainsformer_export_routes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_export_routes + ADD CONSTRAINT trainsformer_export_routes_pkey PRIMARY KEY (id); + + -- -- Name: trainsformer_exports trainsformer_exports_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1835,6 +2005,22 @@ ALTER TABLE ONLY public.trainsformer_exports ADD CONSTRAINT trainsformer_exports_pkey PRIMARY KEY (id); +-- +-- Name: trainsformer_service_dates trainsformer_service_dates_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_service_dates + ADD CONSTRAINT trainsformer_service_dates_pkey PRIMARY KEY (id); + + +-- +-- Name: trainsformer_services trainsformer_services_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_services + ADD CONSTRAINT trainsformer_services_pkey PRIMARY KEY (id); + + -- -- Name: adjustments_source_label_index; Type: INDEX; Schema: public; Owner: - -- @@ -2024,6 +2210,13 @@ CREATE INDEX replacement_services_disruption_id_index ON public.replacement_serv CREATE INDEX replacement_services_shuttle_id_index ON public.replacement_services USING btree (shuttle_id); +-- +-- Name: service_date_days_of_week_service_date_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX service_date_days_of_week_service_date_id_index ON public.service_date_days_of_week USING btree (service_date_id); + + -- -- Name: shapes_name_index; Type: INDEX; Schema: public; Owner: - -- @@ -2073,6 +2266,13 @@ CREATE UNIQUE INDEX stops_stop_id_index ON public.stops USING btree (stop_id); CREATE INDEX stops_stop_lat_stop_lon_stop_id_index ON public.stops USING btree (stop_lat, stop_lon, stop_id); +-- +-- Name: trainsformer_export_routes_export_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX trainsformer_export_routes_export_id_index ON public.trainsformer_export_routes USING btree (export_id); + + -- -- Name: trainsformer_exports_disruption_id_index; Type: INDEX; Schema: public; Owner: - -- @@ -2080,6 +2280,20 @@ CREATE INDEX stops_stop_lat_stop_lon_stop_id_index ON public.stops USING btree ( CREATE INDEX trainsformer_exports_disruption_id_index ON public.trainsformer_exports USING btree (disruption_id); +-- +-- Name: trainsformer_service_dates_service_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX trainsformer_service_dates_service_id_index ON public.trainsformer_service_dates USING btree (service_id); + + +-- +-- Name: trainsformer_services_export_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX trainsformer_services_export_id_index ON public.trainsformer_services USING btree (export_id); + + -- -- Name: unique_disruption_weekday; Type: INDEX; Schema: public; Owner: - -- @@ -2438,6 +2652,14 @@ ALTER TABLE ONLY public.replacement_services ADD CONSTRAINT replacement_services_shuttle_id_fkey FOREIGN KEY (shuttle_id) REFERENCES public.shuttles(id); +-- +-- Name: service_date_days_of_week service_date_days_of_week_service_date_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.service_date_days_of_week + ADD CONSTRAINT service_date_days_of_week_service_date_id_fkey FOREIGN KEY (service_date_id) REFERENCES public.trainsformer_service_dates(id) ON DELETE CASCADE; + + -- -- Name: shuttle_route_stops shuttle_route_stops_gtfs_stop_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2486,6 +2708,14 @@ ALTER TABLE ONLY public.shuttles ADD CONSTRAINT shuttles_disrupted_route_id_fkey FOREIGN KEY (disrupted_route_id) REFERENCES public.gtfs_routes(id); +-- +-- Name: trainsformer_export_routes trainsformer_export_routes_export_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_export_routes + ADD CONSTRAINT trainsformer_export_routes_export_id_fkey FOREIGN KEY (export_id) REFERENCES public.trainsformer_exports(id); + + -- -- Name: trainsformer_exports trainsformer_exports_disruption_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2494,11 +2724,27 @@ ALTER TABLE ONLY public.trainsformer_exports ADD CONSTRAINT trainsformer_exports_disruption_id_fkey FOREIGN KEY (disruption_id) REFERENCES public.disruptionsv2(id); +-- +-- Name: trainsformer_service_dates trainsformer_service_dates_service_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_service_dates + ADD CONSTRAINT trainsformer_service_dates_service_id_fkey FOREIGN KEY (service_id) REFERENCES public.trainsformer_services(id); + + +-- +-- Name: trainsformer_services trainsformer_services_export_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trainsformer_services + ADD CONSTRAINT trainsformer_services_export_id_fkey FOREIGN KEY (export_id) REFERENCES public.trainsformer_exports(id); + + -- -- PostgreSQL database dump complete -- -\unrestrict 2U5xctdYZPzR7MiFLT6ex6uTDnp2eF7UGFIpPgaVW1X1NJYXsQ9bDXJi8zrFfC9 +\unrestrict 162USDnzTVH6AoDpkAVnueTQTvFnNunyreal1sV0AuOxo1ZOXxE0kLpJYW5sHY6 INSERT INTO public."schema_migrations" (version) VALUES (20191223181419); INSERT INTO public."schema_migrations" (version) VALUES (20191223181443); @@ -2562,3 +2808,5 @@ INSERT INTO public."schema_migrations" (version) VALUES (20250501125059); INSERT INTO public."schema_migrations" (version) VALUES (20250601120000); INSERT INTO public."schema_migrations" (version) VALUES (20250602151911); INSERT INTO public."schema_migrations" (version) VALUES (20251202153220); +INSERT INTO public."schema_migrations" (version) VALUES (20260108201439); +INSERT INTO public."schema_migrations" (version) VALUES (20260120151940); diff --git a/test/integration/disruptionsv2/trainsformer_export_section_test.exs b/test/integration/disruptionsv2/trainsformer_export_section_test.exs index 6aefbec19..a131ff424 100644 --- a/test/integration/disruptionsv2/trainsformer_export_section_test.exs +++ b/test/integration/disruptionsv2/trainsformer_export_section_test.exs @@ -252,4 +252,64 @@ defmodule Arrow.Integration.Disruptionsv2.TrainsformerExportSectionTest do {:ok, %{body: File.read!("#{@export_dir}/valid_export.zip")}} end end + + feature "can edit an existing Trainsformer export", %{session: session} do + disruption = + disruption_v2_fixture(%{ + mode: :commuter_rail, + trainsformer_exports: [ + %{ + s3_path: "foo/bar/baz.zip", + name: "get fricking exported", + routes: [ + %{route_id: "CR-Foxboro"} + ], + services: %{ + "service1" => %{ + name: "service1", + service_dates: [ + %{ + start_date: ~D[2026-01-21], + end_date: ~D[2026-01-22], + service_date_days_of_week: [%{day_name: :monday}, %{day_name: :tuesday}] + } + ] + } + } + } + ] + }) + + [%{id: export_id} = export | _] = disruption.trainsformer_exports + + [%{id: service_id} = service | _] = export.services + + [%{id: service_date_id} | _] = service.service_dates + + session + |> visit("/disruptions/#{disruption.id}/") + |> click(Query.css("#edit-export-button-#{export_id}")) + |> click(Query.checkbox("Wednesday")) + |> fill_in( + Query.fillable_field("End Date"), + with: "01/28/2026" + ) + |> click(text("Add Another Timeframe")) + |> fill_in(Query.fillable_field("Start Date") |> Query.at(1), with: "01/30/2026") + |> fill_in(Query.fillable_field("End Date") |> Query.at(1), with: "02/05/2026") + |> click(Query.checkbox("Monday") |> Query.at(1)) + |> click(text("Save")) + |> assert_text("Trainsformer service schedules updated successfully!") + |> assert_text("01/21/2026") + |> assert_text("01/28/2026") + |> assert_text("01/30/2026") + |> assert_text("02/05/2026") + |> assert_has(Query.css("#service-#{service_id}-date-#{service_date_id}-monday.text-primary")) + |> assert_has( + Query.css("#service-#{service_id}-date-#{service_date_id}-tuesday.text-primary") + ) + |> assert_has( + Query.css("#service-#{service_id}-date-#{service_date_id}-wednesday.text-primary") + ) + end end From 32a09a326f1b56b15d165b6a558120e2ae414375 Mon Sep 17 00:00:00 2001 From: JD Date: Thu, 22 Jan 2026 11:54:42 -0500 Subject: [PATCH 17/27] fixup: fix unit tests --- lib/arrow/trainsformer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/arrow/trainsformer.ex b/lib/arrow/trainsformer.ex index 3ca013a36..371183c8a 100644 --- a/lib/arrow/trainsformer.ex +++ b/lib/arrow/trainsformer.ex @@ -230,7 +230,7 @@ defmodule Arrow.Trainsformer do """ def get_service_date!(id), - do: id |> Repo.get!(ServiceDate) |> Repo.preload(:service_date_days_of_week) + do: id |> then(&Repo.get!(ServiceDate, &1)) |> Repo.preload(:service_date_days_of_week) @doc """ Creates a service_date. From f3accd59767f46e0f91cad03aafd8871f362cf86 Mon Sep 17 00:00:00 2001 From: JD Date: Thu, 22 Jan 2026 12:00:14 -0500 Subject: [PATCH 18/27] fixup: credo --- .../disruptionsv2/trainsformer_export_section_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/disruptionsv2/trainsformer_export_section_test.exs b/test/integration/disruptionsv2/trainsformer_export_section_test.exs index a131ff424..f6ed678de 100644 --- a/test/integration/disruptionsv2/trainsformer_export_section_test.exs +++ b/test/integration/disruptionsv2/trainsformer_export_section_test.exs @@ -295,9 +295,9 @@ defmodule Arrow.Integration.Disruptionsv2.TrainsformerExportSectionTest do with: "01/28/2026" ) |> click(text("Add Another Timeframe")) - |> fill_in(Query.fillable_field("Start Date") |> Query.at(1), with: "01/30/2026") - |> fill_in(Query.fillable_field("End Date") |> Query.at(1), with: "02/05/2026") - |> click(Query.checkbox("Monday") |> Query.at(1)) + |> fill_in("Start Date" |> Query.fillable_field() |> Query.at(1), with: "01/30/2026") + |> fill_in("End Date" |> Query.fillable_field() |> Query.at(1), with: "02/05/2026") + |> click("Monday" |> Query.checkbox() |> Query.at(1)) |> click(text("Save")) |> assert_text("Trainsformer service schedules updated successfully!") |> assert_text("01/21/2026") From f0bd34ef8fc5cd3a7a7bb2b0e7da313e500cfea8 Mon Sep 17 00:00:00 2001 From: JD Date: Thu, 22 Jan 2026 12:19:04 -0500 Subject: [PATCH 19/27] fixup: dialyzer --- lib/arrow/trainsformer/service_date_day_of_week.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/arrow/trainsformer/service_date_day_of_week.ex b/lib/arrow/trainsformer/service_date_day_of_week.ex index f3f06aae0..251e325e3 100644 --- a/lib/arrow/trainsformer/service_date_day_of_week.ex +++ b/lib/arrow/trainsformer/service_date_day_of_week.ex @@ -1,10 +1,10 @@ defmodule Arrow.Trainsformer.ServiceDateDayOfWeek do @moduledoc "Describes the days of week for which a service should be active, within some date range" - use Ecto.Schema + use Arrow.Schema import Ecto.Changeset - schema "service_date_days_of_week" do + typed_schema "service_date_days_of_week" do field :day_name, Ecto.Enum, values: Arrow.Util.DayOfWeek.day_name_values() belongs_to :service_date, Arrow.Trainsformer.ServiceDate, on_replace: :delete From a72f5fa0e0ec4bb56ed7d932f6d8c0af254e72fb Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 13:12:23 -0500 Subject: [PATCH 20/27] fixup: ignore replace changesets --- .../edit_trainsformer_export_form.ex | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index 5a05cb502..f68cadfbd 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -184,19 +184,29 @@ defmodule ArrowWeb.EditTrainsformerExportForm do } value={ if Ecto.assoc_loaded?(f_date[:service_date_days_of_week].value) do - Enum.map( + Enum.reduce( f_date[:service_date_days_of_week].value, + [], fn - %ServiceDateDayOfWeek{day_name: day_name} -> - Atom.to_string(day_name) + %ServiceDateDayOfWeek{day_name: day_name}, acc -> + [Atom.to_string(day_name) | acc] - %Ecto.Changeset{} = changeset -> - changeset - |> Ecto.Changeset.get_field(:day_name) - |> Atom.to_string() + %Ecto.Changeset{action: :replace}, acc -> + acc - str -> - str + %Ecto.Changeset{action: :delete}, acc -> + acc + + %Ecto.Changeset{} = changeset, acc -> + day_name = + changeset + |> Ecto.Changeset.get_field(:day_name) + |> Atom.to_string() + + [day_name | acc] + + str, acc -> + [str | acc] end ) else From 95eaaa017e6003595398c49fd4bc3ceb6b1d95db Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 13:36:42 -0500 Subject: [PATCH 21/27] fixup: correct migrations --- ...201439_create_trainsformer_export_service_and_routes.exs | 3 ++- .../20260120151940_create_service_date_days_of_week.exs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs b/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs index 113d5b113..b4fb82ca8 100644 --- a/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs +++ b/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs @@ -21,7 +21,8 @@ defmodule Arrow.Repo.Migrations.CreateTrainsformerExportServiceAndRoutes do create table(:trainsformer_export_routes) do add :route_id, :string - add :export_id, references(:trainsformer_exports, on_delete: :delete_all) + + add :export_id, references(:trainsformer_exports, on_delete: :delete_all, validate: false) end create index(:trainsformer_export_routes, [:export_id]) diff --git a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs index 13a7ea40e..b9cdcb5f0 100644 --- a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs +++ b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs @@ -4,11 +4,13 @@ defmodule Arrow.Repo.Migrations.CreateTrainsformerServiceDateDaysOfWeek do def change do create table(:service_date_days_of_week) do add :day_name, :integer, null: false - add :service_date_id, references(:trainsformer_service_dates, on_delete: :delete_all) + + add :service_date_id, + references(:trainsformer_service_dates, on_delete: :delete_all, validate: false) timestamps() end - create index(:service_date_days_of_week, [:service_date_id]) + create index(:service_date_days_of_week, [:service_date_id]), concurrently: true end end From 4587d4eefdab24c542927ce409593b4ce04e8116 Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 13:44:38 -0500 Subject: [PATCH 22/27] fixup: assure safety for adding column reference when table with reference known to be empty --- .../20260120151940_create_service_date_days_of_week.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs index b9cdcb5f0..f0a510ea1 100644 --- a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs +++ b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs @@ -5,8 +5,9 @@ defmodule Arrow.Repo.Migrations.CreateTrainsformerServiceDateDaysOfWeek do create table(:service_date_days_of_week) do add :day_name, :integer, null: false + # excellent_migrations:safety-assured-for-next-line column_reference_added add :service_date_id, - references(:trainsformer_service_dates, on_delete: :delete_all, validate: false) + references(:trainsformer_service_dates, on_delete: :delete_all) timestamps() end From 380d35b5ef44c624db4052bce8a3e26f09d3aa14 Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 13:49:03 -0500 Subject: [PATCH 23/27] fixup: remove unncecessary on_delete --- lib/arrow/trainsformer/export.ex | 2 +- lib/arrow/trainsformer/service_date.ex | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/arrow/trainsformer/export.ex b/lib/arrow/trainsformer/export.ex index 63aef8a9e..1e3d124d1 100644 --- a/lib/arrow/trainsformer/export.ex +++ b/lib/arrow/trainsformer/export.ex @@ -16,7 +16,7 @@ defmodule Arrow.Trainsformer.Export do foreign_key: :export_id, on_replace: :delete - has_many :routes, Route, on_replace: :delete, foreign_key: :export_id, on_delete: :delete_all + has_many :routes, Route, on_replace: :delete, foreign_key: :export_id belongs_to :disruption, DisruptionV2 timestamps(type: :utc_datetime) diff --git a/lib/arrow/trainsformer/service_date.ex b/lib/arrow/trainsformer/service_date.ex index 4d476d360..28b2fdc86 100644 --- a/lib/arrow/trainsformer/service_date.ex +++ b/lib/arrow/trainsformer/service_date.ex @@ -11,7 +11,6 @@ defmodule Arrow.Trainsformer.ServiceDate do belongs_to :service, Arrow.Trainsformer.Service, on_replace: :delete has_many :service_date_days_of_week, ServiceDateDayOfWeek, - on_delete: :delete_all, on_replace: :delete, foreign_key: :service_date_id end From b313e8f582beafd66aa312156096ccc03777dd17 Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 15:29:59 -0500 Subject: [PATCH 24/27] fixup: fix migrations --- ...8201439_create_trainsformer_export_service_and_routes.exs | 5 ++--- .../20260120151940_create_service_date_days_of_week.exs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs b/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs index b4fb82ca8..88401c748 100644 --- a/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs +++ b/priv/repo/migrations/20260108201439_create_trainsformer_export_service_and_routes.exs @@ -1,4 +1,4 @@ -defmodule Arrow.Repo.Migrations.CreateTrainsformerExportServiceAndRoutes do +dfmodule Arrow.Repo.Migrations.CreateTrainsformerExportServiceAndRoutes do use Ecto.Migration def change do @@ -21,8 +21,7 @@ defmodule Arrow.Repo.Migrations.CreateTrainsformerExportServiceAndRoutes do create table(:trainsformer_export_routes) do add :route_id, :string - - add :export_id, references(:trainsformer_exports, on_delete: :delete_all, validate: false) + add :export_id, references(:trainsformer_exports, on_delete: :delete_all) end create index(:trainsformer_export_routes, [:export_id]) diff --git a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs index f0a510ea1..590de78cb 100644 --- a/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs +++ b/priv/repo/migrations/20260120151940_create_service_date_days_of_week.exs @@ -7,7 +7,7 @@ defmodule Arrow.Repo.Migrations.CreateTrainsformerServiceDateDaysOfWeek do # excellent_migrations:safety-assured-for-next-line column_reference_added add :service_date_id, - references(:trainsformer_service_dates, on_delete: :delete_all) + references(:trainsformer_service_dates, on_delete: :delete_all, on_update: :update_all) timestamps() end From 8e3fe72ccd01a0e985bcf5382ba136c3dd4c90e3 Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 15:43:33 -0500 Subject: [PATCH 25/27] fixup: remove comment and unnecessary property default in export_upload --- lib/arrow/trainsformer/export_upload.ex | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/arrow/trainsformer/export_upload.ex b/lib/arrow/trainsformer/export_upload.ex index f0632eef7..f2cebd5c6 100644 --- a/lib/arrow/trainsformer/export_upload.ex +++ b/lib/arrow/trainsformer/export_upload.ex @@ -94,8 +94,6 @@ defmodule Arrow.Trainsformer.ExportUpload do current_date_string = Date.to_iso8601(Date.utc_today()) - # note from JD: I don't really like how tightly coupled this is to the frontend (edit_trainsformer_export_form.ex) - # but if you don't set defaults here, the form seems to blow up service_maps = Enum.map(services, fn service_id -> %{ @@ -104,8 +102,7 @@ defmodule Arrow.Trainsformer.ExportUpload do %{ "service_id" => service_id, "start_date" => current_date_string, - "end_date" => current_date_string, - "service_date_days_of_week" => [] + "end_date" => current_date_string } ] } From f78cdf3cfcd5c30adba106d37f29bbf8165980c2 Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 15:51:22 -0500 Subject: [PATCH 26/27] fixup: extract transformation of attrs to private function --- lib/arrow/trainsformer/service_date.ex | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/arrow/trainsformer/service_date.ex b/lib/arrow/trainsformer/service_date.ex index 28b2fdc86..948db3b90 100644 --- a/lib/arrow/trainsformer/service_date.ex +++ b/lib/arrow/trainsformer/service_date.ex @@ -15,25 +15,28 @@ defmodule Arrow.Trainsformer.ServiceDate do foreign_key: :service_date_id end + defp transform_form_submission(attrs) do + with %{"service_date_days_of_week" => sddow} <- attrs, + [_ | _] <- sddow do + formatted_days = + Enum.map(sddow, fn day -> + %{ + "day_name" => day + } + end) + + %{attrs | "service_date_days_of_week" => formatted_days} + else + _ -> attrs + end + end + @doc false def changeset(service_date, attrs) do - attrs = - with %{"service_date_days_of_week" => sddow} <- attrs, - [_ | _] <- sddow do - formatted_days = - Enum.map(sddow, fn day -> - %{ - "day_name" => day - } - end) - - %{attrs | "service_date_days_of_week" => formatted_days} - else - _ -> attrs - end + transformed_attrs = transform_form_submission(attrs) service_date - |> cast(attrs, [:start_date, :end_date, :service_id]) + |> cast(transformed_attrs, [:start_date, :end_date, :service_id]) |> cast_assoc(:service_date_days_of_week, with: &ServiceDateDayOfWeek.changeset/2) |> validate_required([:start_date, :end_date]) |> assoc_constraint(:service) From 2f1dfb9bb0d71fc72a25729bf6346969f90bc84c Mon Sep 17 00:00:00 2001 From: JD Date: Fri, 23 Jan 2026 17:43:55 -0500 Subject: [PATCH 27/27] fixup: refactor to use comprehension --- .../edit_trainsformer_export_form.ex | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index f68cadfbd..fde05df77 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -184,31 +184,28 @@ defmodule ArrowWeb.EditTrainsformerExportForm do } value={ if Ecto.assoc_loaded?(f_date[:service_date_days_of_week].value) do - Enum.reduce( - f_date[:service_date_days_of_week].value, - [], - fn - %ServiceDateDayOfWeek{day_name: day_name}, acc -> - [Atom.to_string(day_name) | acc] - - %Ecto.Changeset{action: :replace}, acc -> - acc - - %Ecto.Changeset{action: :delete}, acc -> - acc - - %Ecto.Changeset{} = changeset, acc -> - day_name = - changeset - |> Ecto.Changeset.get_field(:day_name) - |> Atom.to_string() - - [day_name | acc] - - str, acc -> - [str | acc] - end - ) + for day <- f_date[:service_date_days_of_week].value, reduce: [] do + acc -> + case day do + %ServiceDateDayOfWeek{day_name: day_name} -> + [Atom.to_string(day_name) | acc] + + %Ecto.Changeset{action: action} = changeset + when action not in [:delete, :replace] -> + day_name = + changeset + |> Ecto.Changeset.get_field(:day_name) + |> Atom.to_string() + + [day_name | acc] + + str when is_binary(str) -> + [str | acc] + + _ -> + acc + end + end else [] end