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/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/trainsformer.ex b/lib/arrow/trainsformer.ex index 2538bce1b..371183c8a 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 """ @@ -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: id |> then(&Repo.get!(ServiceDate, &1)) |> 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 f2cebd5c6..f0632eef7 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" => [] } ] } 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/lib/arrow/trainsformer/service_date.ex b/lib/arrow/trainsformer/service_date.ex index 150773635..4d476d360 100644 --- a/lib/arrow/trainsformer/service_date.ex +++ b/lib/arrow/trainsformer/service_date.ex @@ -3,17 +3,39 @@ 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_delete: :delete_all, + on_replace: :delete, + foreign_key: :service_date_id 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 + 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/trainsformer/service_date_day_of_week.ex b/lib/arrow/trainsformer/service_date_day_of_week.ex new file mode 100644 index 000000000..251e325e3 --- /dev/null +++ b/lib/arrow/trainsformer/service_date_day_of_week.ex @@ -0,0 +1,21 @@ +defmodule Arrow.Trainsformer.ServiceDateDayOfWeek do + @moduledoc "Describes the days of week for which a service should be active, within some date range" + + use Arrow.Schema + import Ecto.Changeset + + 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 + + timestamps() + end + + @doc false + def changeset(service_date_days_of_week, attrs) do + service_date_days_of_week + |> cast(attrs, [:day_name]) + |> validate_required([:day_name]) + |> assoc_constraint(:service_date) + end +end diff --git a/lib/arrow_web/components/core_components.ex b/lib/arrow_web/components/core_components.ex index fb566e75e..37e9a98a6 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,6 +336,29 @@ defmodule ArrowWeb.CoreComponents do |> input() 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 ce8423279..88ac8677e 100644 --- a/lib/arrow_web/components/disruption_components.ex +++ b/lib/arrow_web/components/disruption_components.ex @@ -397,78 +397,127 @@ defmodule ArrowWeb.DisruptionComponents do def view_trainsformer_service_schedules(assigns) do ~H""" -
+

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 <- export.services do %> +
+
- Service - - Start Date - - End Date - - Days of Week -
- - - - + + + + - <% end %> -
{service.name} -
- {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)} - - + Service ID + + Start Date + + End Date + + Days of Week +
+ <%= for {service, i} <- Enum.with_index(export.services) do %> + + {service.name} + +
+ {Calendar.strftime(date, "%a")}. + {Calendar.strftime(date, "%m/%d/%Y")} +
+ + +
+ {Calendar.strftime(date, "%a")}. + {Calendar.strftime(date, "%m/%d/%Y")} +
+ + +
+ <% active_dows = + date + |> Kernel.get_in([ + Access.key(:service_date_days_of_week), + Access.all(), + Access.key(:day_name) + ]) %> + + {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_trainsformer_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 1ce30caae..5a05cb502 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].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 + } + /> +
-
1} - class="col-auto align-self-center mt-3" - > +
<.button type="button" phx-click="delete_service_date" @@ -197,19 +218,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 +
@@ -298,6 +329,46 @@ defmodule ArrowWeb.EditTrainsformerExportForm do {:ok, socket} end + def update(assigns, socket) do + form = + assigns.export + |> Trainsformer.change_export() + |> to_form() + + socket = + socket + |> assign(assigns) + |> 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, []) + |> 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, + auto_upload: true + ) + + {: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} @@ -305,8 +376,7 @@ 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 @@ -359,7 +429,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 @@ -454,6 +529,30 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end end + defp update_export(export_params, socket) do + imported_services = + for {key, value} <- export_params["services"], + into: %{}, + do: {key, 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 + defp create_export(export_params, socket) do imported_services = for {key, value} <- export_params["services"], 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 0b0ece3d3..56d3c914a 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,6 +5,7 @@ defmodule ArrowWeb.DisruptionV2ViewLive do alias Arrow.Disruptions.{DisruptionV2, Limit, ReplacementService} alias Arrow.Hastus alias Arrow.Hastus.Export, as: HastusExport + alias Arrow.Trainsformer alias Arrow.Trainsformer.Export, as: TrainsformerExport alias ArrowWeb.DisruptionComponents @@ -132,6 +133,28 @@ defmodule ArrowWeb.DisruptionV2ViewLive do end end + def handle_event("delete_trainsformer_export", %{"export" => export_id}, socket) do + {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}, @@ -287,6 +310,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, 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..13a7ea40e --- /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: :delete_all) + + timestamps() + end + + create index(:service_date_days_of_week, [:service_date_id]) + end +end 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 c416fcd88..4bb93928d 100644 --- a/test/integration/disruptionsv2/trainsformer_export_section_test.exs +++ b/test/integration/disruptionsv2/trainsformer_export_section_test.exs @@ -214,4 +214,64 @@ defmodule Arrow.Integration.Disruptionsv2.TrainsformerExportSectionTest do s |> click(text("Cancel")) |> assert_text("Upload Trainsformer export") 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("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") + |> 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 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()