Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/arrow/disruptions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
1 change: 1 addition & 0 deletions lib/arrow/disruptions/disruption_v2.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 3 additions & 2 deletions lib/arrow/trainsformer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Arrow.Trainsformer do
@preloads [
:disruption,
:routes,
services: [:service_dates]
services: [service_dates: [:service_date_days_of_week]]
]

@doc """
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion lib/arrow/trainsformer/export_upload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
%{
Expand All @@ -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" => []
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion lib/arrow/trainsformer/service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions lib/arrow/trainsformer/service_date.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

@firestack firestack Jan 22, 2026

Choose a reason for hiding this comment

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

question: I'm wondering if we should specify on_delete?
the docs say

Using this option is DISCOURAGED for most relational databases. Instead, in your migration, set references(:parent_id, on_delete: :delete_all).

https://hexdocs.pm/ecto/3.13.5/Ecto.Schema.html#has_many/3

And you already have on_delete specified in the migration.

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
Expand Down
21 changes: 21 additions & 0 deletions lib/arrow/trainsformer/service_date_day_of_week.ex
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions lib/arrow_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -313,6 +336,29 @@ defmodule ArrowWeb.CoreComponents do
|> input()
end

def input(%{type: "checkgroup"} = assigns) do
~H"""
<div class="mt-1">
<div class="grid grid-cols-2 gap-1 text-sm items-baseline">
<div :for={{label, value} <- @options}>
<label for={"#{@name}-#{value}"}>
<input
type="checkbox"
id={"#{@name}-#{value}"}
name={@name}
value={value}
checked={value in @value}
class="mr-2 h-4 w-4 rounded"
{@rest}
/>
{label}
</label>
</div>
</div>
</div>
"""
end

def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Expand Down
179 changes: 114 additions & 65 deletions lib/arrow_web/components/disruption_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -397,78 +397,127 @@ defmodule ArrowWeb.DisruptionComponents do

def view_trainsformer_service_schedules(assigns) do
~H"""
<section id="trainsformer_service_schedules" class="py-4 my-4">
<section id="trainsformer_service_schedules" class="py-4 my-4 text-sm">
<h3>Trainsformer Service Schedules</h3>
<%= if Ecto.assoc_loaded?(@disruption.trainsformer_exports) and Enum.any?(@disruption.trainsformer_exports) do %>
<div
:for={export <- @disruption.trainsformer_exports}
id={"export-table-hastus-#{export.id}"}
class="border-2 border-dashed border-secondary border-mb-3 p-2 mb-3"
>
<p><b>Export:</b> {export.name}</p>
<div class="row">
<div class="col-3">
<b>Routes</b>
<%= for route <- export.routes do %>
<div class="row">
<div class="col-lg-1">
<span
class="m-icon m-icon-sm mr-1"
style={"background-image: url('#{Map.get(@icon_paths, :commuter_rail)}');"}
/>
</div>
<div class="col-lg-10">
<p>{route.route_id}</p>
<%= 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 %>
<div
id={"export-table-hastus-#{export.id}"}
class="border-2 border-dashed border-secondary border-mb-3 p-2 mb-3"
>
<p><b>Export:</b> {export.name}</p>
<div class="row">
<div class="col-lg-3">
<b>Routes</b>
<%= for route <- export.routes do %>
<div class="row">
<div class="col-lg-1">
<span
class="m-icon m-icon-sm mr-1"
style={"background-image: url('#{Map.get(@icon_paths, :commuter_rail)}');"}
/>
</div>
<div class="col-lg-10">
<p>{route.route_id}</p>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>

<div class="col-9">
<table class="sm:w-full">
<tr>
<th>
Service
</th>
<th>
Start Date
</th>
<th>
End Date
</th>
<th>
Days of Week
</th>
</tr>
<%= for service <- export.services do %>
<div class="col-9">
<table class="sm:w-full">
<tr>
<td>{service.name}</td>
<td>
<div :for={date <- Enum.map(service.service_dates, & &1.start_date)}>
<span class="text-danger">{Calendar.strftime(date, "%a")}.</span>
{Calendar.strftime(date, "%m/%d/%Y")}
</div>
</td>
<td>
<div :for={date <- Enum.map(service.service_dates, & &1.end_date)}>
<span class="text-danger">{Calendar.strftime(date, "%a")}.</span>
{Calendar.strftime(date, "%m/%d/%Y")}
</div>
</td>
<td>
<span
:for={dow <- Arrow.Util.DayOfWeek.get_all_day_names()}
class="text-gray-400"
>
{format_day_name_short(dow)}
</span>
</td>
<th>
Service ID
</th>
<th>
Start Date
</th>
<th>
End Date
</th>
<th>
Days of Week
</th>
</tr>
<% end %>
</table>
<%= for {service, i} <- Enum.with_index(export.services) do %>
<tr class="align-top">
<td>{service.name}</td>
<td>
<div :for={date <- Enum.map(service.service_dates, & &1.start_date)}>
<span class="text-danger">{Calendar.strftime(date, "%a")}.</span>
{Calendar.strftime(date, "%m/%d/%Y")}
</div>
</td>
<td>
<div :for={date <- Enum.map(service.service_dates, & &1.end_date)}>
<span class="text-danger">{Calendar.strftime(date, "%a")}.</span>
{Calendar.strftime(date, "%m/%d/%Y")}
</div>
</td>
<td>
<div :for={date <- service.service_dates}>
<% active_dows =
date
|> Kernel.get_in([
Access.key(:service_date_days_of_week),
Access.all(),
Access.key(:day_name)
]) %>
<span
:for={dow <- Arrow.Util.DayOfWeek.get_all_day_names()}
class={
if dow in active_dows,
do: "text-primary",
else: "text-gray-400"
}
id={"service-#{service.id}-date-#{date.id}-#{dow}"}
>
{format_day_name_short(dow)}
</span>
</div>
</td>
<td :if={i == length(export.services) - 1}>
<div class="text-right">
<.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" />
</.link>
<.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" />
</.button>
</div>
</td>
</tr>
<% end %>
</table>
</div>
</div>
</div>
</div>
<% end %>
<% end %>

<.link
Expand Down
Loading
Loading