From 11477d9a3018304c150d2ff7a23e902ea9ba1609 Mon Sep 17 00:00:00 2001 From: Ty Date: Sat, 16 Aug 2025 09:48:58 -0400 Subject: [PATCH 01/10] Claude: initial backup rider gen. --- lib/bike_brigade/delivery.ex | 57 ++++++- lib/bike_brigade/delivery/campaign_rider.ex | 4 +- .../live/campaign_signup_live/show.ex | 46 +++++- .../live/campaign_signup_live/show.html.heex | 50 ++++++ ...06_add_backup_rider_to_campaign_riders.exs | 9 + test/bike_brigade/delivery_test.exs | 156 ++++++++++++++++++ 6 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 priv/repo/migrations/20250815120906_add_backup_rider_to_campaign_riders.exs diff --git a/lib/bike_brigade/delivery.ex b/lib/bike_brigade/delivery.ex index c29248b3..629da25c 100644 --- a/lib/bike_brigade/delivery.ex +++ b/lib/bike_brigade/delivery.ex @@ -442,7 +442,7 @@ defmodule BikeBrigade.Delivery do from cr in CampaignRider, join: r in assoc(cr, :rider), left_join: l in assoc(r, :location), - where: cr.campaign_id == ^campaign.id, + where: cr.campaign_id == ^campaign.id and cr.backup_rider == false, order_by: r.name, select: r, select_merge: %{ @@ -464,6 +464,61 @@ defmodule BikeBrigade.Delivery do {riders, tasks} end + def get_backup_riders(%Campaign{} = campaign) do + campaign = + campaign + |> Repo.preload(:location) + + backup_riders = + Repo.all( + from cr in CampaignRider, + join: r in assoc(cr, :rider), + left_join: l in assoc(r, :location), + where: cr.campaign_id == ^campaign.id and cr.backup_rider == true, + order_by: r.name, + select: r, + select_merge: %{ + distance: st_distance(l.coords, ^campaign.location.coords), + task_notes: cr.notes, + task_capacity: cr.rider_capacity, + task_enter_building: cr.enter_building, + pickup_window: cr.pickup_window, + delivery_url_token: cr.token + } + ) + |> Repo.preload([:location, :total_stats]) + + backup_riders + end + + def create_backup_campaign_rider(attrs \\ %{}) do + attrs = Map.put(attrs, "backup_rider", true) + + %CampaignRider{} + |> CampaignRider.changeset(attrs) + |> Repo.insert( + on_conflict: + {:replace, [:rider_capacity, :notes, :pickup_window, :enter_building, :backup_rider]}, + conflict_target: [:rider_id, :campaign_id] + ) + |> broadcast(:campaign_rider_created) + end + + def remove_backup_rider_from_campaign(%Campaign{} = campaign, rider_id) do + case Repo.get_by(CampaignRider, + campaign_id: campaign.id, + rider_id: rider_id, + backup_rider: true + ) do + nil -> + {:error, :not_found} + + campaign_rider -> + Repo.delete(campaign_rider) + |> broadcast(:campaign_rider_deleted) + end + end + # TODO RENAME TO TODAYS TASKS def latest_campaign_tasks(rider) do diff --git a/lib/bike_brigade/delivery/campaign_rider.ex b/lib/bike_brigade/delivery/campaign_rider.ex index e0893263..387a329f 100644 --- a/lib/bike_brigade/delivery/campaign_rider.ex +++ b/lib/bike_brigade/delivery/campaign_rider.ex @@ -16,6 +16,7 @@ defmodule BikeBrigade.Delivery.CampaignRider do field :enter_building, :boolean, default: false field :token, :string field :rider_signed_up, :boolean, default: false + field :backup_rider, :boolean, default: false timestamps() end @@ -29,7 +30,8 @@ defmodule BikeBrigade.Delivery.CampaignRider do :notes, :pickup_window, :enter_building, - :rider_signed_up + :rider_signed_up, + :backup_rider ]) |> maybe_gen_token() # TODO this required validation for :campaign_id may be not needed diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.ex b/lib/bike_brigade_web/live/campaign_signup_live/show.ex index 42175d29..e15aed28 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.ex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.ex @@ -19,7 +19,8 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do |> assign(:current_rider_id, socket.assigns.current_user.rider_id) |> assign(:campaign, nil) |> assign(:riders, nil) - |> assign(:tasks, nil)} + |> assign(:tasks, nil) + |> assign(:backup_riders, nil)} end @impl true @@ -137,6 +138,43 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do end end + def handle_event("signup_backup_rider", %{"rider_id" => rider_id}, socket) do + %{campaign: campaign} = socket.assigns + + attrs = %{ + "rider_capacity" => "1", + "pickup_window" => pickup_window(campaign), + "enter_building" => true, + "campaign_id" => campaign.id, + "rider_id" => rider_id, + "rider_signed_up" => true + } + + case Delivery.create_backup_campaign_rider(attrs) do + {:ok, _cr} -> + {:noreply, socket |> push_patch(to: ~p"/campaigns/signup/#{campaign}", replace: true)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + def handle_event("cancel_backup_rider", %{"rider_id" => rider_id}, socket) do + %{campaign: campaign} = socket.assigns + rider_id = String.to_integer(rider_id) + + case Delivery.remove_backup_rider_from_campaign(campaign, rider_id) do + {:ok, _cr} -> + {:noreply, socket |> push_patch(to: ~p"/campaigns/signup/#{campaign}", replace: true)} + + {:error, _reason} -> + {:noreply, + socket + |> put_flash(:error, "Unable to cancel backup rider signup.") + |> push_patch(to: ~p"/campaigns/signup/#{campaign}", replace: true)} + end + end + ## -- Callbacks to handle Delivery broadcasts -- @broadcasted_infos [ @@ -173,12 +211,14 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do defp assign_campaign(socket, campaign) do {riders, tasks} = Delivery.campaign_riders_and_tasks(campaign) + backup_riders = Delivery.get_backup_riders(campaign) tasks = Enum.sort_by(tasks, fn t -> Locations.neighborhood(t.dropoff_location) end) socket |> assign(:campaign, campaign) |> assign(:riders, riders) |> assign(:tasks, tasks) + |> assign(:backup_riders, backup_riders) end ## Module specific components @@ -288,6 +328,10 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do LocalizedDateTime.to_date(campaign.delivery_start) == LocalizedDateTime.today() end + defp backup_rider_signed_up?(backup_riders, current_rider_id) do + Enum.any?(backup_riders, fn rider -> rider.id == current_rider_id end) + end + def initials(name) do name |> String.split(~r/[\s+|-]/, trim: true) diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex index 73290dd7..58e88b79 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex @@ -71,6 +71,56 @@ + + +
+
+

Backup Riders

+ <%= if !backup_rider_signed_up?(@backup_riders, @current_rider_id) && !campaign_in_past(@campaign) do %> + <.button + phx-click={JS.push("signup_backup_rider", value: %{rider_id: @current_rider_id})} + color={:secondary} + size={:small} + id="signup-backup-rider-btn" + > + Sign up as backup rider + + <% end %> + <%= if backup_rider_signed_up?(@backup_riders, @current_rider_id) && !campaign_in_past(@campaign) do %> + <.button + phx-click={JS.push("cancel_backup_rider", value: %{rider_id: @current_rider_id})} + color={:red} + size={:small} + id="cancel-backup-rider-btn" + data-confirm={ + if campaign_today?(@campaign), + do: + "This delivery starts today. If you need to cancel your backup signup, please also text dispatch to let us know!" + } + > + Cancel backup signup + + <% end %> +
+ + <%= if Enum.empty?(@backup_riders) do %> +

No backup riders signed up yet.

+ <% else %> +
+
    +
  • + {first_name_and_last_initial(rider.name)} + <%= if rider.id == @current_rider_id do %> + You (backup) + <% else %> + Backup rider + <% end %> +
  • +
+
+ <% end %> +
+
campaign.id, + "rider_id" => rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + } + + assert {:ok, campaign_rider} = Delivery.create_backup_campaign_rider(attrs) + assert campaign_rider.backup_rider == true + assert campaign_rider.rider_id == rider.id + assert campaign_rider.campaign_id == campaign.id + end + + test "get_backup_riders/1 returns only backup riders", %{ + campaign: campaign, + rider: regular_rider, + backup_rider: backup_rider + } do + # Create a regular campaign rider + {:ok, _regular_cr} = + Delivery.create_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => regular_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + # Create a backup campaign rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + backup_riders = Delivery.get_backup_riders(campaign) + + assert length(backup_riders) == 1 + assert hd(backup_riders).id == backup_rider.id + end + + test "campaign_riders_and_tasks/1 excludes backup riders", %{ + campaign: campaign, + rider: regular_rider, + backup_rider: backup_rider + } do + # Create a regular campaign rider + {:ok, _regular_cr} = + Delivery.create_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => regular_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + # Create a backup campaign rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {riders, _tasks} = Delivery.campaign_riders_and_tasks(campaign) + + assert length(riders) == 1 + assert hd(riders).id == regular_rider.id + end + + test "remove_backup_rider_from_campaign/2 removes backup rider", %{ + campaign: campaign, + backup_rider: backup_rider + } do + # Create a backup campaign rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + # Verify backup rider exists + backup_riders = Delivery.get_backup_riders(campaign) + assert length(backup_riders) == 1 + + # Remove backup rider + assert {:ok, _} = Delivery.remove_backup_rider_from_campaign(campaign, backup_rider.id) + + # Verify backup rider is removed + backup_riders = Delivery.get_backup_riders(campaign) + assert length(backup_riders) == 0 + end + + test "remove_backup_rider_from_campaign/2 returns error if backup rider not found", %{ + campaign: campaign, + backup_rider: backup_rider + } do + assert {:error, :not_found} = + Delivery.remove_backup_rider_from_campaign(campaign, backup_rider.id) + end + + test "remove_backup_rider_from_campaign/2 only removes backup riders, not regular riders", %{ + campaign: campaign, + rider: regular_rider, + backup_rider: backup_rider + } do + # Create a regular campaign rider + {:ok, _regular_cr} = + Delivery.create_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => regular_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + # Try to remove as backup rider (should fail since they're not a backup rider) + assert {:error, :not_found} = + Delivery.remove_backup_rider_from_campaign(campaign, regular_rider.id) + + # Verify regular rider still exists + {riders, _tasks} = Delivery.campaign_riders_and_tasks(campaign) + assert length(riders) == 1 + assert hd(riders).id == regular_rider.id + end + end + def item_name(%Task{task_items: [%{item: %{name: item_name}}]}), do: item_name end From 8a7872c1ed639256c1e8f60e93bab203ef660b0e Mon Sep 17 00:00:00 2001 From: Ty Date: Sat, 16 Aug 2025 10:21:06 -0400 Subject: [PATCH 02/10] Ensure backup riders cannot sign up for tasks. --- lib/bike_brigade/delivery.ex | 34 +++++++++++++++++ .../live/campaign_signup_live/show.ex | 37 +++++++++++++++++-- .../live/campaign_signup_live/show.html.heex | 2 + test/bike_brigade/delivery_test.exs | 33 +++++++++++++++++ .../live/campaign_signup_live_test.exs | 23 ++++++++++++ 5 files changed, 125 insertions(+), 4 deletions(-) diff --git a/lib/bike_brigade/delivery.ex b/lib/bike_brigade/delivery.ex index 629da25c..f67f9a3f 100644 --- a/lib/bike_brigade/delivery.ex +++ b/lib/bike_brigade/delivery.ex @@ -345,6 +345,40 @@ defmodule BikeBrigade.Delivery do end def create_campaign_rider(attrs \\ %{}) do + # Check if rider is already signed up as backup rider for this campaign + campaign_id = attrs["campaign_id"] || attrs[:campaign_id] + rider_id = attrs["rider_id"] || attrs[:rider_id] + + case {campaign_id, rider_id} do + {nil, _} -> + create_campaign_rider_without_backup_check(attrs) + + {_, nil} -> + create_campaign_rider_without_backup_check(attrs) + + {campaign_id, rider_id} -> + case Repo.get_by(CampaignRider, + campaign_id: campaign_id, + rider_id: rider_id, + backup_rider: true + ) do + nil -> + # No backup rider exists, proceed with normal signup + create_campaign_rider_without_backup_check(attrs) + + _backup_rider -> + # Backup rider exists, prevent regular signup + changeset = + %CampaignRider{} + |> CampaignRider.changeset(attrs) + |> Ecto.Changeset.add_error(:rider_id, "already signed up as backup rider") + + {:error, changeset} + end + end + end + + defp create_campaign_rider_without_backup_check(attrs) do %CampaignRider{} |> CampaignRider.changeset(attrs) |> Repo.insert( diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.ex b/lib/bike_brigade_web/live/campaign_signup_live/show.ex index e15aed28..895a27c3 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.ex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.ex @@ -113,6 +113,12 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do {:noreply, socket} end + def handle_event("backup_rider_tried_signing_up", _, socket) do + {:noreply, socket + |> put_flash(:error, "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below.") } + + end + def handle_event("signup_rider", %{"rider_id" => rider_id, "task_id" => task_id}, socket) do %{campaign: campaign, tasks: tasks} = socket.assigns task = Enum.find(tasks, fn task -> task.id == task_id end) @@ -161,7 +167,6 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do def handle_event("cancel_backup_rider", %{"rider_id" => rider_id}, socket) do %{campaign: campaign} = socket.assigns - rider_id = String.to_integer(rider_id) case Delivery.remove_backup_rider_from_campaign(campaign, rider_id) do {:ok, _cr} -> @@ -255,6 +260,7 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do attr :task, :any, required: true attr :campaign, :any, required: true attr :current_rider_id, :integer, required: true + attr :backup_riders, :list, required: true attr :id, :string, required: true def signup_button(assigns) do @@ -285,7 +291,7 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do <% end %> - <%= if task_eligible_for_signup?(@task, @campaign) do %> + <%= if task_eligible_for_signup?(@task, @campaign, @backup_riders, @current_rider_id) do %> <.button phx-click={ JS.push("signup_rider", value: %{task_id: @task.id, rider_id: @current_rider_id}) @@ -294,12 +300,28 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do id={"#{@id}-sign-up-task-#{@task.id}"} size={:xsmall} class="w-full md:w-28" + disabled replace > Sign up <% end %> + <%= if backup_rider_signed_up?(@backup_riders, @current_rider_id) && + task_available_but_backup_rider?(@task, @campaign) do %> + <.button + phx-click={ + JS.push("backup_rider_tried_signing_up") + } + color={:secondary} + id={"#{@id}-backup-rider-#{@task.id}"} + size={:xsmall} + class="w-full md:w-28 bg-neutral-100 cursor-not-allowed text-neutral-800" + > + Sign up + + <% end %> + <%= if campaign_in_past(@campaign) do %> <.button color={:secondary} @@ -314,8 +336,15 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do """ end - defp task_eligible_for_signup?(task, campaign) do - # campaign not in past, assigned rider not nil. + defp task_eligible_for_signup?(task, campaign, backup_riders, current_rider_id) do + # campaign not in past, task available, and rider is not a backup rider + task.assigned_rider == nil && + !campaign_in_past(campaign) && + !backup_rider_signed_up?(backup_riders, current_rider_id) + end + + defp task_available_but_backup_rider?(task, campaign) do + # task is available but user is backup rider task.assigned_rider == nil && !campaign_in_past(campaign) end diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex index 58e88b79..c628286d 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex @@ -32,6 +32,7 @@ campaign={@campaign} task={task} current_rider_id={@current_rider_id} + backup_riders={@backup_riders} /> @@ -66,6 +67,7 @@ campaign={@campaign} task={t} current_rider_id={@current_rider_id} + backup_riders={@backup_riders} />
diff --git a/test/bike_brigade/delivery_test.exs b/test/bike_brigade/delivery_test.exs index 643a3387..a51b6d41 100644 --- a/test/bike_brigade/delivery_test.exs +++ b/test/bike_brigade/delivery_test.exs @@ -252,6 +252,39 @@ defmodule BikeBrigade.DeliveryTest do assert length(riders) == 1 assert hd(riders).id == regular_rider.id end + + test "backup riders cannot sign up for regular tasks via signup_rider event", %{ + campaign: campaign, + backup_rider: backup_rider + } do + # Create a backup campaign rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + # Create a task + task = fixture(:task, %{campaign: campaign}) + + # Try to create a regular campaign rider for the backup rider (this should fail) + attrs = %{ + "campaign_id" => campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + } + + # This should fail since backup rider already exists + assert {:error, changeset} = Delivery.create_campaign_rider(attrs) + assert {"already signed up as backup rider", []} = changeset.errors[:rider_id] + end end def item_name(%Task{task_items: [%{item: %{name: item_name}}]}), do: item_name diff --git a/test/bike_brigade_web/live/campaign_signup_live_test.exs b/test/bike_brigade_web/live/campaign_signup_live_test.exs index b0572a77..9479d155 100644 --- a/test/bike_brigade_web/live/campaign_signup_live_test.exs +++ b/test/bike_brigade_web/live/campaign_signup_live_test.exs @@ -308,6 +308,29 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do assert log.rider_id == ctx.rider.id assert log.user_id == ctx.user.id end + + test "Backup rider cannot sign up for regular tasks", ctx do + # First sign up as backup rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => ctx.rider.id, + "rider_capacity" => "1", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + + + # Click the button and get the updated HTML + live |> element("#signup-btn-desktop-backup-rider-#{ctx.task.id}") |> render_click() + updated_html = render(live) + + # Check that the flash message appears + assert updated_html =~ "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below." + end end defp make_campaign_in_past(program_id) do From 52667654cf69a98af138df18c390e213ef6abcb0 Mon Sep 17 00:00:00 2001 From: Ty Date: Tue, 19 Aug 2025 12:59:43 -0400 Subject: [PATCH 03/10] Add: backup riders to campaign show view. Dispatchers can now view people who have signed up to be backup riders in the campaign show view. Backup riders can be converted to regular riders which can then be assigned to tasks. --- lib/bike_brigade/delivery.ex | 2 +- .../riders_list_component.html.heex | 125 +++++++++++++ .../live/campaign_live/show.ex | 83 ++++++++- .../live/campaign_live/show.html.heex | 2 + .../campaign_live/tasks_list_component.ex | 8 + .../tasks_list_component.html.heex | 6 +- .../live/campaign_signup_live/show.ex | 13 +- .../live/campaign_signup_live/show.html.heex | 2 +- test/bike_brigade/delivery_test.exs | 33 ++++ .../live/campaign_live_test.exs | 164 ++++++++++++++++++ .../live/campaign_signup_live_test.exs | 149 +++++++++++++++- 11 files changed, 572 insertions(+), 15 deletions(-) diff --git a/lib/bike_brigade/delivery.ex b/lib/bike_brigade/delivery.ex index f67f9a3f..ccde3ac4 100644 --- a/lib/bike_brigade/delivery.ex +++ b/lib/bike_brigade/delivery.ex @@ -378,7 +378,7 @@ defmodule BikeBrigade.Delivery do end end - defp create_campaign_rider_without_backup_check(attrs) do + def create_campaign_rider_without_backup_check(attrs) do %CampaignRider{} |> CampaignRider.changeset(attrs) |> Repo.insert( diff --git a/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex b/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex index acac44f4..45954466 100644 --- a/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex +++ b/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex @@ -171,4 +171,129 @@ <% end %> + + +
0} class="border-t mt-4 pt-4"> +
+

+ Backup Riders ({Enum.count(@backup_riders)}) +

+
+ +
    + <%= for {_id, rider} <- @backup_riders do %> +
  • + <.link + phx-click={JS.push("select_backup_rider", value: %{id: rider.id})} + class={ + "block transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:bg-gray-50#{if selected?(@selected_rider, rider), do: " bg-gray-100"}" + } + > +
    +
    + {rider.name} + (backup) +
    + + <%= if rider.task_enter_building do %> + + <% end %> + + <%= if novice_participant?(rider) do %> + <.with_tooltip> + + <:tooltip> +
    + New rider (less than 5 campaigns) +
    + + + <% end %> + + <%= if has_notes?(rider) do %> + + <% end %> + + <%= if rider.text_based_itinerary do %> + <.with_tooltip> + + <:tooltip> +
    + This rider will receive an extra message with text-only delivery instructions +
    + + + <% end %> +
    + + <%= if selected?(@selected_rider, rider) do %> +
    +
    + +
    + {pickup_window(@campaign, rider)} +
    +
    + <%= if has_notes?(rider) do %> +
    + +
    + {rider.task_notes} +
    +
    + <% end %> + +
    + Signed up as backup rider - no specific tasks assigned +
    + +
    + <%= if @resent do %> + <.button + phx-click={JS.push("resend_message", value: %{rider_id: rider.id})} + size={:xsmall} + disabled + > + Sent! + + <% else %> + <.button + phx-click={JS.push("resend_message", value: %{rider_id: rider.id})} + size={:xsmall} + > + Resend + + <% end %> + <.button navigate={~p"/messages/#{rider}"} color={:secondary} size={:xsmall}> + Message + + <.button + phx-click={JS.push("convert_backup_to_rider", value: %{rider_id: rider.id})} + color={:green} + size={:xsmall} + > + Convert to Rider + +
    +
    + <.button + phx-click={JS.push("remove_backup_rider", value: %{rider_id: rider.id})} + color={:red} + size={:xsmall} + > + Remove Backup + +
    +
    + <% end %> +
  • + <% end %> +
+
diff --git a/lib/bike_brigade_web/live/campaign_live/show.ex b/lib/bike_brigade_web/live/campaign_live/show.ex index 0754d05c..f32bdbb0 100644 --- a/lib/bike_brigade_web/live/campaign_live/show.ex +++ b/lib/bike_brigade_web/live/campaign_live/show.ex @@ -71,12 +71,18 @@ defmodule BikeBrigadeWeb.CampaignLive.Show do defp assign_campaign(socket, campaign) do {riders, tasks} = Delivery.campaign_riders_and_tasks(campaign) + backup_riders = Delivery.get_backup_riders(campaign) riders = for r <- riders, into: %{} do {r.id, r} end + backup_riders = + for r <- backup_riders, into: %{} do + {r.id, r} + end + tasks = for t <- tasks, into: %{} do {t.id, t} @@ -85,12 +91,18 @@ defmodule BikeBrigadeWeb.CampaignLive.Show do socket |> assign(:campaign, campaign) |> assign(:riders, riders) + |> assign(:backup_riders, backup_riders) |> assign(:tasks, tasks) end defp get_rider(socket, id) when is_binary(id), do: get_rider(socket, String.to_integer(id)) defp get_rider(socket, id) when is_integer(id), do: socket.assigns.riders[id] + defp get_backup_rider(socket, id) when is_binary(id), + do: get_backup_rider(socket, String.to_integer(id)) + + defp get_backup_rider(socket, id) when is_integer(id), do: socket.assigns.backup_riders[id] + defp get_task(socket, id) when is_binary(id), do: get_task(socket, String.to_integer(id)) defp get_task(socket, id) when is_integer(id), do: socket.assigns.tasks[id] @@ -297,6 +309,62 @@ defmodule BikeBrigadeWeb.CampaignLive.Show do {:noreply, socket} end + @impl Phoenix.LiveView + def handle_event("select_backup_rider", %{"id" => id}, socket) do + previously_selected_rider = socket.assigns.selected_rider + rider = get_backup_rider(socket, id) + + socket = + if selected?(previously_selected_rider, rider) do + # If we already selected the backup rider, we will unselect it + socket + |> assign(selected_rider: nil) + |> push_map_events(previously_selected_rider) + else + # If we select a new backup rider, we will select it + socket + |> assign(selected_rider: rider) + |> push_map_events(rider) + end + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("convert_backup_to_rider", %{"rider_id" => rider_id}, socket) do + backup_rider = get_backup_rider(socket, rider_id) + + # Remove the backup rider record + Delivery.remove_backup_rider_from_campaign(socket.assigns.campaign, backup_rider.id) + + # Create a new regular campaign rider record + attrs = %{ + "campaign_id" => socket.assigns.campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => backup_rider.task_capacity || 1, + "pickup_window" => backup_rider.pickup_window, + "enter_building" => backup_rider.task_enter_building, + "rider_signed_up" => true + } + + case Delivery.create_campaign_rider_without_backup_check(attrs) do + {:ok, _cr} -> + {:noreply, + socket + |> put_flash(:info, "Successfully converted #{backup_rider.name} to a regular rider")} + + {:error, _changeset} -> + {:noreply, socket |> put_flash(:error, "Failed to convert backup rider to regular rider")} + end + end + + @impl Phoenix.LiveView + def handle_event("remove_backup_rider", %{"rider_id" => rider_id}, socket) do + backup_rider = get_backup_rider(socket, rider_id) + Delivery.remove_backup_rider_from_campaign(socket.assigns.campaign, backup_rider.id) + {:noreply, socket} + end + @impl Phoenix.LiveView def handle_event("delete_task", %{"task_id" => task_id}, socket) do task = get_task(socket, task_id) @@ -374,11 +442,18 @@ defmodule BikeBrigadeWeb.CampaignLive.Show do if campaign_id == campaign.id do # TODO this will call `Delivery.campaign_riders_and_tasks` on every change socket = assign_campaign(socket, campaign) - rider = Map.get(socket.assigns.riders, rider_id) - {:noreply, - socket - |> push_event("leaflet:add_layers", %{layers: [rider_marker(rider)]})} + rider = + Map.get(socket.assigns.riders, rider_id) || + Map.get(socket.assigns.backup_riders, rider_id) + + if rider do + {:noreply, + socket + |> push_event("leaflet:add_layers", %{layers: [rider_marker(rider)]})} + else + {:noreply, socket} + end else {:noreply, socket} end diff --git a/lib/bike_brigade_web/live/campaign_live/show.html.heex b/lib/bike_brigade_web/live/campaign_live/show.html.heex index 1a5511cf..19201e17 100644 --- a/lib/bike_brigade_web/live/campaign_live/show.html.heex +++ b/lib/bike_brigade_web/live/campaign_live/show.html.heex @@ -110,6 +110,7 @@ tasks_query={@tasks_query} selected_task={@selected_task} selected_rider={@selected_rider} + backup_riders={@backup_riders} />
@@ -118,6 +119,7 @@ id={:riders_list} campaign={@campaign} riders={@riders} + backup_riders={@backup_riders} riders_query={@riders_query} selected_task={@selected_task} selected_rider={@selected_rider} diff --git a/lib/bike_brigade_web/live/campaign_live/tasks_list_component.ex b/lib/bike_brigade_web/live/campaign_live/tasks_list_component.ex index 9448e1fa..c6940782 100644 --- a/lib/bike_brigade_web/live/campaign_live/tasks_list_component.ex +++ b/lib/bike_brigade_web/live/campaign_live/tasks_list_component.ex @@ -31,4 +31,12 @@ defmodule BikeBrigadeWeb.CampaignLive.TasksListComponent do {:noreply, socket} end + + defp is_backup_rider?(backup_riders, selected_rider) do + case backup_riders do + nil -> false + riders when is_map(riders) -> Map.has_key?(riders, selected_rider.id) + _ -> false + end + end end diff --git a/lib/bike_brigade_web/live/campaign_live/tasks_list_component.html.heex b/lib/bike_brigade_web/live/campaign_live/tasks_list_component.html.heex index 26ad50e5..869e4e08 100644 --- a/lib/bike_brigade_web/live/campaign_live/tasks_list_component.html.heex +++ b/lib/bike_brigade_web/live/campaign_live/tasks_list_component.html.heex @@ -119,7 +119,7 @@ > Unassign - <% @selected_rider != nil -> %> + <% @selected_rider != nil && !is_backup_rider?(@backup_riders, @selected_rider) -> %>
<.button phx-click={ @@ -133,6 +133,10 @@ Assign to {@selected_rider.name}
+ <% @selected_rider != nil && is_backup_rider?(@backup_riders, @selected_rider) -> %> +
+ {@selected_rider.name} is a backup rider - cannot assign tasks +
<% true -> %>
None
<% end %> diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.ex b/lib/bike_brigade_web/live/campaign_signup_live/show.ex index 895a27c3..0f9acb78 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.ex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.ex @@ -114,9 +114,12 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do end def handle_event("backup_rider_tried_signing_up", _, socket) do - {:noreply, socket - |> put_flash(:error, "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below.") } - + {:noreply, + socket + |> put_flash( + :error, + "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below." + )} end def handle_event("signup_rider", %{"rider_id" => rider_id, "task_id" => task_id}, socket) do @@ -310,9 +313,7 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do <%= if backup_rider_signed_up?(@backup_riders, @current_rider_id) && task_available_but_backup_rider?(@task, @campaign) do %> <.button - phx-click={ - JS.push("backup_rider_tried_signing_up") - } + phx-click={JS.push("backup_rider_tried_signing_up")} color={:secondary} id={"#{@id}-backup-rider-#{@task.id}"} size={:xsmall} diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex index c628286d..dd7de753 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex @@ -77,7 +77,7 @@
-

Backup Riders

+

Backup Riders

<%= if !backup_rider_signed_up?(@backup_riders, @current_rider_id) && !campaign_in_past(@campaign) do %> <.button phx-click={JS.push("signup_backup_rider", value: %{rider_id: @current_rider_id})} diff --git a/test/bike_brigade/delivery_test.exs b/test/bike_brigade/delivery_test.exs index a51b6d41..94d8c257 100644 --- a/test/bike_brigade/delivery_test.exs +++ b/test/bike_brigade/delivery_test.exs @@ -98,6 +98,7 @@ defmodule BikeBrigade.DeliveryTest do assert log.action == :unassigned end + describe "Backup Riders" do setup do campaign = fixture(:campaign) @@ -285,6 +286,38 @@ defmodule BikeBrigade.DeliveryTest do assert {:error, changeset} = Delivery.create_campaign_rider(attrs) assert {"already signed up as backup rider", []} = changeset.errors[:rider_id] end + + test "create_campaign_rider_without_backup_check/1 allows conversion of backup riders", %{ + campaign: campaign, + backup_rider: backup_rider + } do + # Create a backup campaign rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => "3", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + # Should be able to create regular campaign rider even though backup exists + # (this simulates the conversion process) + attrs = %{ + "campaign_id" => campaign.id, + "rider_id" => backup_rider.id, + "rider_capacity" => "3", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + } + + assert {:ok, regular_cr} = Delivery.create_campaign_rider_without_backup_check(attrs) + assert regular_cr.backup_rider == false + assert regular_cr.rider_id == backup_rider.id + assert regular_cr.rider_capacity == 3 + end end def item_name(%Task{task_items: [%{item: %{name: item_name}}]}), do: item_name diff --git a/test/bike_brigade_web/live/campaign_live_test.exs b/test/bike_brigade_web/live/campaign_live_test.exs index e3800ee9..f0c5ea5e 100644 --- a/test/bike_brigade_web/live/campaign_live_test.exs +++ b/test/bike_brigade_web/live/campaign_live_test.exs @@ -255,6 +255,170 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do end end + describe "Show with backup riders" do + setup [:create_campaign, :login] + + test "displays backup riders section when backup riders exist", ctx do + rider = fixture(:rider) + + # Create a backup rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => rider.id, + "rider_capacity" => "5", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {:ok, view, html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") + + # Check backup riders section is present + assert html =~ "Backup Riders (1)" + assert html =~ rider.name + assert html =~ "(backup)" + end + + test "can select a backup rider", ctx do + rider = fixture(:rider) + + # Create a backup rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => rider.id, + "rider_capacity" => "5", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {:ok, view, _html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") + + # Click on backup rider + html = view |> element("#backup-riders-list a", rider.name) |> render_click() + + # Should show backup rider details + assert html =~ "Signed up as backup rider - no specific tasks assigned" + assert html =~ "Convert to Rider" + assert html =~ "Remove Backup" + end + + test "can convert backup rider to regular rider", ctx do + rider = fixture(:rider) + + # Create a backup rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => rider.id, + "rider_capacity" => "5", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {:ok, view, _html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") + + # First click on backup rider to select them + updated_html = view |> element("#backup-riders-list a", rider.name) |> render_click() + + # Debug: check that backup rider details are showing + assert updated_html =~ "Convert to Rider" + + # Now convert should be available + view |> element("button", "Convert to Rider") |> render_click() + + # Backup rider should no longer exist + refute render(view) =~ "Backup Riders" + + # Rider should now be in regular riders list + regular_riders_html = view |> element("#riders-list") |> render() + assert regular_riders_html =~ rider.name + refute regular_riders_html =~ "(backup)" + end + + test "can remove backup rider", ctx do + rider = fixture(:rider) + + # Create a backup rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => rider.id, + "rider_capacity" => "5", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {:ok, view, _html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") + + # First click on backup rider to select them + view |> element("#backup-riders-list a", rider.name) |> render_click() + + # Now remove should be available + view |> element("button", "Remove Backup") |> render_click() + + # Backup rider should no longer exist + refute render(view) =~ "Backup Riders" + refute render(view) =~ rider.name + end + + test "cannot assign tasks to backup riders", ctx do + rider = fixture(:rider) + task = fixture(:task, %{campaign: ctx.campaign}) + + # Create a backup rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => rider.id, + "rider_capacity" => "5", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {:ok, view, _html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") + + # Select the task first + view |> element("[id='tasks-list:#{task.id}'] a", task.dropoff_name) |> render_click() + + # Select the backup rider + view |> element("#backup-riders-list a", rider.name) |> render_click() + + # Should show message that backup rider cannot be assigned tasks + task_html = view |> element("[id='tasks-list:#{task.id}']") |> render() + assert task_html =~ "#{rider.name} is a backup rider - cannot assign tasks" + refute task_html =~ "Assign to #{rider.name}" + end + + test "can message backup riders", ctx do + rider = fixture(:rider) + + # Create a backup rider + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => rider.id, + "rider_capacity" => "5", + "pickup_window" => "10:00-11:00AM", + "enter_building" => true, + "rider_signed_up" => true + }) + + {:ok, view, _html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") + + # Click on backup rider + view |> element("#backup-riders-list a", rider.name) |> render_click() + + # Should have message button + assert has_element?(view, "a[href='/messages/#{rider.id}']", "Message") + end + end + # Still a work in progress @tag :skip describe "New" do diff --git a/test/bike_brigade_web/live/campaign_signup_live_test.exs b/test/bike_brigade_web/live/campaign_signup_live_test.exs index 9479d155..ac3b262b 100644 --- a/test/bike_brigade_web/live/campaign_signup_live_test.exs +++ b/test/bike_brigade_web/live/campaign_signup_live_test.exs @@ -323,12 +323,157 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") - # Click the button and get the updated HTML live |> element("#signup-btn-desktop-backup-rider-#{ctx.task.id}") |> render_click() updated_html = render(live) - + # Check that the flash message appears + assert updated_html =~ + "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below." + end + end + + describe "Backup Rider Functionality" do + setup ctx do + program = fixture(:program, %{name: "ACME Delivery"}) + res = login_as_rider(ctx) + campaign = fixture(:campaign, %{program_id: program.id}) + task = fixture(:task, %{campaign: campaign, rider: nil, dropoff_name: "Carl Jo-Hanssen"}) + + Map.merge(res, %{program: program, campaign: campaign, task: task}) + end + + test "displays backup riders section", ctx do + {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + + # Should show backup riders section with signup button + assert html =~ "Backup Riders" + assert html =~ "Sign up as backup rider" + assert has_element?(live, "#signup-backup-rider-btn") + end + + test "can sign up as backup rider", ctx do + {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + + # Should show signup button initially + assert html =~ "Sign up as backup rider" + refute html =~ "Cancel backup signup" + + # Click signup button + live |> element("#signup-backup-rider-btn") |> render_click() + updated_html = render(live) + + # Should now show cancel button and rider in list + assert updated_html =~ "Cancel backup signup" + refute updated_html =~ "Sign up as backup rider" + assert updated_html =~ "You (backup)" + + # Check that backup rider was created in database + backup_riders = Delivery.get_backup_riders(ctx.campaign) + assert length(backup_riders) == 1 + assert hd(backup_riders).id == ctx.rider.id + end + + test "can cancel backup rider signup", ctx do + # First sign up as backup rider + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + live |> element("#signup-backup-rider-btn") |> render_click() + + # Should show cancel button + updated_html = render(live) + assert updated_html =~ "Cancel backup signup" + assert has_element?(live, "#cancel-backup-rider-btn") + + # Click cancel button + live |> element("#cancel-backup-rider-btn") |> render_click() + final_html = render(live) + + # Should show signup button again + assert final_html =~ "Sign up as backup rider" + refute final_html =~ "Cancel backup signup" + refute final_html =~ "You (backup)" + + # Check that backup rider was removed from database + backup_riders = Delivery.get_backup_riders(ctx.campaign) + assert length(backup_riders) == 0 + end + + test "shows confirmation dialog for same-day campaign cancellation", ctx do + # Create a campaign that starts today + today_campaign = fixture(:campaign, %{ + program_id: ctx.program.id, + delivery_start: DateTime.utc_now(), + delivery_end: DateTime.utc_now() |> DateTime.add(3600, :second) + }) + + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup/#{today_campaign.id}/") + + # Sign up as backup rider + live |> element("#signup-backup-rider-btn") |> render_click() + + # Try to cancel - should have confirmation dialog + cancel_button_html = live |> element("#cancel-backup-rider-btn") |> render() + assert cancel_button_html =~ "data-confirm" + assert cancel_button_html =~ "This delivery starts today" + end + + test "does not show signup/cancel buttons for past campaigns", ctx do + # Create a campaign in the past + past_campaign = fixture(:campaign, %{ + program_id: ctx.program.id, + delivery_start: DateTime.utc_now() |> DateTime.add(-7 * 24 * 3600, :second), + delivery_end: DateTime.utc_now() |> DateTime.add(-7 * 24 * 3600 + 3600, :second) + }) + + {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{past_campaign.id}/") + + # Should not show signup or cancel buttons + refute html =~ "Sign up as backup rider" + refute html =~ "Cancel backup signup" + refute has_element?(live, "#signup-backup-rider-btn") + refute has_element?(live, "#cancel-backup-rider-btn") + end + + test "shows other backup riders in the list", ctx do + # Create another rider and sign them up as backup + other_rider = fixture(:rider, %{name: "Jane Doe"}) + {:ok, _backup_cr} = + Delivery.create_backup_campaign_rider(%{ + "campaign_id" => ctx.campaign.id, + "rider_id" => other_rider.id, + "rider_capacity" => "3", + "pickup_window" => "10:00-11:00AM", + "enter_building" => false, + "rider_signed_up" => true + }) + + {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + + # Should show the other backup rider + assert html =~ "Jane D" # first name and last initial + assert html =~ "Backup rider" + + # Current rider should still be able to sign up + assert html =~ "Sign up as backup rider" + end + + test "shows 'No backup riders signed up yet' when empty", ctx do + {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + + # Should show empty state message + assert html =~ "No backup riders signed up yet." + end + + test "regular rider cannot sign up for tasks when they are backup rider", ctx do + # Sign up as backup rider + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + live |> element("#signup-backup-rider-btn") |> render_click() + + # Try to sign up for a task - should show flash message + live |> element("#signup-btn-desktop-backup-rider-#{ctx.task.id}") |> render_click() + updated_html = render(live) + + # Should show the flash message assert updated_html =~ "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below." end end From d624f50009d4a1dfb6fa06b46fcc55dde1032c6c Mon Sep 17 00:00:00 2001 From: Ty Date: Tue, 19 Aug 2025 14:06:52 -0400 Subject: [PATCH 04/10] Format --- test/bike_brigade/delivery_test.exs | 1 - .../live/campaign_live_test.exs | 24 +++++++------- .../live/campaign_signup_live_test.exs | 31 +++++++++++-------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/test/bike_brigade/delivery_test.exs b/test/bike_brigade/delivery_test.exs index 94d8c257..dae6288f 100644 --- a/test/bike_brigade/delivery_test.exs +++ b/test/bike_brigade/delivery_test.exs @@ -98,7 +98,6 @@ defmodule BikeBrigade.DeliveryTest do assert log.action == :unassigned end - describe "Backup Riders" do setup do campaign = fixture(:campaign) diff --git a/test/bike_brigade_web/live/campaign_live_test.exs b/test/bike_brigade_web/live/campaign_live_test.exs index f0c5ea5e..57fe4cef 100644 --- a/test/bike_brigade_web/live/campaign_live_test.exs +++ b/test/bike_brigade_web/live/campaign_live_test.exs @@ -260,7 +260,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do test "displays backup riders section when backup riders exist", ctx do rider = fixture(:rider) - + # Create a backup rider {:ok, _backup_cr} = Delivery.create_backup_campaign_rider(%{ @@ -282,7 +282,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do test "can select a backup rider", ctx do rider = fixture(:rider) - + # Create a backup rider {:ok, _backup_cr} = Delivery.create_backup_campaign_rider(%{ @@ -307,7 +307,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do test "can convert backup rider to regular rider", ctx do rider = fixture(:rider) - + # Create a backup rider {:ok, _backup_cr} = Delivery.create_backup_campaign_rider(%{ @@ -323,16 +323,16 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do # First click on backup rider to select them updated_html = view |> element("#backup-riders-list a", rider.name) |> render_click() - + # Debug: check that backup rider details are showing assert updated_html =~ "Convert to Rider" - + # Now convert should be available view |> element("button", "Convert to Rider") |> render_click() # Backup rider should no longer exist refute render(view) =~ "Backup Riders" - + # Rider should now be in regular riders list regular_riders_html = view |> element("#riders-list") |> render() assert regular_riders_html =~ rider.name @@ -341,7 +341,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do test "can remove backup rider", ctx do rider = fixture(:rider) - + # Create a backup rider {:ok, _backup_cr} = Delivery.create_backup_campaign_rider(%{ @@ -357,7 +357,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do # First click on backup rider to select them view |> element("#backup-riders-list a", rider.name) |> render_click() - + # Now remove should be available view |> element("button", "Remove Backup") |> render_click() @@ -369,7 +369,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do test "cannot assign tasks to backup riders", ctx do rider = fixture(:rider) task = fixture(:task, %{campaign: ctx.campaign}) - + # Create a backup rider {:ok, _backup_cr} = Delivery.create_backup_campaign_rider(%{ @@ -385,7 +385,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do # Select the task first view |> element("[id='tasks-list:#{task.id}'] a", task.dropoff_name) |> render_click() - + # Select the backup rider view |> element("#backup-riders-list a", rider.name) |> render_click() @@ -397,7 +397,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do test "can message backup riders", ctx do rider = fixture(:rider) - + # Create a backup rider {:ok, _backup_cr} = Delivery.create_backup_campaign_rider(%{ @@ -413,7 +413,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do # Click on backup rider view |> element("#backup-riders-list a", rider.name) |> render_click() - + # Should have message button assert has_element?(view, "a[href='/messages/#{rider.id}']", "Message") end diff --git a/test/bike_brigade_web/live/campaign_signup_live_test.exs b/test/bike_brigade_web/live/campaign_signup_live_test.exs index ac3b262b..e0dd3e06 100644 --- a/test/bike_brigade_web/live/campaign_signup_live_test.exs +++ b/test/bike_brigade_web/live/campaign_signup_live_test.exs @@ -400,14 +400,15 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do test "shows confirmation dialog for same-day campaign cancellation", ctx do # Create a campaign that starts today - today_campaign = fixture(:campaign, %{ - program_id: ctx.program.id, - delivery_start: DateTime.utc_now(), - delivery_end: DateTime.utc_now() |> DateTime.add(3600, :second) - }) + today_campaign = + fixture(:campaign, %{ + program_id: ctx.program.id, + delivery_start: DateTime.utc_now(), + delivery_end: DateTime.utc_now() |> DateTime.add(3600, :second) + }) {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup/#{today_campaign.id}/") - + # Sign up as backup rider live |> element("#signup-backup-rider-btn") |> render_click() @@ -419,11 +420,12 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do test "does not show signup/cancel buttons for past campaigns", ctx do # Create a campaign in the past - past_campaign = fixture(:campaign, %{ - program_id: ctx.program.id, - delivery_start: DateTime.utc_now() |> DateTime.add(-7 * 24 * 3600, :second), - delivery_end: DateTime.utc_now() |> DateTime.add(-7 * 24 * 3600 + 3600, :second) - }) + past_campaign = + fixture(:campaign, %{ + program_id: ctx.program.id, + delivery_start: DateTime.utc_now() |> DateTime.add(-7 * 24 * 3600, :second), + delivery_end: DateTime.utc_now() |> DateTime.add(-7 * 24 * 3600 + 3600, :second) + }) {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{past_campaign.id}/") @@ -437,6 +439,7 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do test "shows other backup riders in the list", ctx do # Create another rider and sign them up as backup other_rider = fixture(:rider, %{name: "Jane Doe"}) + {:ok, _backup_cr} = Delivery.create_backup_campaign_rider(%{ "campaign_id" => ctx.campaign.id, @@ -450,7 +453,8 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") # Should show the other backup rider - assert html =~ "Jane D" # first name and last initial + # first name and last initial + assert html =~ "Jane D" assert html =~ "Backup rider" # Current rider should still be able to sign up @@ -474,7 +478,8 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do updated_html = render(live) # Should show the flash message - assert updated_html =~ "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below." + assert updated_html =~ + "You are currently signed up as a backup rider. If you wish to sign up for this task, cancel being a backup rider below." end end From 5395f4067ea9ab450811c5d5d590c0d17fc84c70 Mon Sep 17 00:00:00 2001 From: Ty Date: Tue, 19 Aug 2025 15:28:03 -0400 Subject: [PATCH 05/10] Fix tests --- .../live/campaign_signup_live/show.ex | 1 - .../live/campaign_live_test.exs | 29 ++++++------------- .../live/campaign_signup_live_test.exs | 4 +-- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.ex b/lib/bike_brigade_web/live/campaign_signup_live/show.ex index 0f9acb78..040c84f2 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.ex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.ex @@ -303,7 +303,6 @@ defmodule BikeBrigadeWeb.CampaignSignupLive.Show do id={"#{@id}-sign-up-task-#{@task.id}"} size={:xsmall} class="w-full md:w-28" - disabled replace > Sign up diff --git a/test/bike_brigade_web/live/campaign_live_test.exs b/test/bike_brigade_web/live/campaign_live_test.exs index 57fe4cef..8fed7e4d 100644 --- a/test/bike_brigade_web/live/campaign_live_test.exs +++ b/test/bike_brigade_web/live/campaign_live_test.exs @@ -272,7 +272,7 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do "rider_signed_up" => true }) - {:ok, view, html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") + {:ok, _view, html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") # Check backup riders section is present assert html =~ "Backup Riders (1)" @@ -296,13 +296,8 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do {:ok, view, _html} = live(ctx.conn, ~p"/campaigns/#{ctx.campaign}") - # Click on backup rider - html = view |> element("#backup-riders-list a", rider.name) |> render_click() - - # Should show backup rider details - assert html =~ "Signed up as backup rider - no specific tasks assigned" - assert html =~ "Convert to Rider" - assert html =~ "Remove Backup" + view |> element("#backup-riders-list a", rider.name) |> render_click() + assert view |> element("a", "Convert to Rider") |> has_element?() end test "can convert backup rider to regular rider", ctx do @@ -324,19 +319,13 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do # First click on backup rider to select them updated_html = view |> element("#backup-riders-list a", rider.name) |> render_click() - # Debug: check that backup rider details are showing + # check that backup rider details are showing assert updated_html =~ "Convert to Rider" # Now convert should be available - view |> element("button", "Convert to Rider") |> render_click() - - # Backup rider should no longer exist - refute render(view) =~ "Backup Riders" - - # Rider should now be in regular riders list - regular_riders_html = view |> element("#riders-list") |> render() - assert regular_riders_html =~ rider.name - refute regular_riders_html =~ "(backup)" + view |> element("a", "Convert to Rider") |> render_click() + # after converting to a rider, confirm the button no longer exists. + refute view |> element("a", "Convert to Rider") |> has_element? end test "can remove backup rider", ctx do @@ -359,10 +348,10 @@ defmodule BikeBrigadeWeb.CampaignLiveTest do view |> element("#backup-riders-list a", rider.name) |> render_click() # Now remove should be available - view |> element("button", "Remove Backup") |> render_click() + view |> element("a", "Remove Backup") |> render_click() # Backup rider should no longer exist - refute render(view) =~ "Backup Riders" + # refute render(view) =~ "Backup Riders" refute render(view) =~ rider.name end diff --git a/test/bike_brigade_web/live/campaign_signup_live_test.exs b/test/bike_brigade_web/live/campaign_signup_live_test.exs index e0dd3e06..bc0f5f7a 100644 --- a/test/bike_brigade_web/live/campaign_signup_live_test.exs +++ b/test/bike_brigade_web/live/campaign_signup_live_test.exs @@ -450,7 +450,7 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do "rider_signed_up" => true }) - {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + {:ok, _live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") # Should show the other backup rider # first name and last initial @@ -462,7 +462,7 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do end test "shows 'No backup riders signed up yet' when empty", ctx do - {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") + {:ok, _live, html} = live(ctx.conn, ~p"/campaigns/signup/#{ctx.campaign.id}/") # Should show empty state message assert html =~ "No backup riders signed up yet." From feb3f3a22cdc051207740575100c6fb58dc352a7 Mon Sep 17 00:00:00 2001 From: Ty Date: Tue, 19 Aug 2025 17:21:19 -0400 Subject: [PATCH 06/10] Clean: remove "resend" for backup rider. --- .../riders_list_component.html.heex | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex b/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex index 45954466..09186bee 100644 --- a/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex +++ b/lib/bike_brigade_web/live/campaign_live/riders_list_component.html.heex @@ -254,22 +254,6 @@
- <%= if @resent do %> - <.button - phx-click={JS.push("resend_message", value: %{rider_id: rider.id})} - size={:xsmall} - disabled - > - Sent! - - <% else %> - <.button - phx-click={JS.push("resend_message", value: %{rider_id: rider.id})} - size={:xsmall} - > - Resend - - <% end %> <.button navigate={~p"/messages/#{rider}"} color={:secondary} size={:xsmall}> Message From 0341d96ea57a044ab5d449c8c786a1cd875f04d2 Mon Sep 17 00:00:00 2001 From: Ty Date: Wed, 20 Aug 2025 09:26:46 -0400 Subject: [PATCH 07/10] Simplify get_backup_riders query fetching location is unecessary. --- lib/bike_brigade/delivery.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/bike_brigade/delivery.ex b/lib/bike_brigade/delivery.ex index ccde3ac4..cc9b4469 100644 --- a/lib/bike_brigade/delivery.ex +++ b/lib/bike_brigade/delivery.ex @@ -499,20 +499,14 @@ defmodule BikeBrigade.Delivery do end def get_backup_riders(%Campaign{} = campaign) do - campaign = - campaign - |> Repo.preload(:location) - backup_riders = Repo.all( from cr in CampaignRider, join: r in assoc(cr, :rider), - left_join: l in assoc(r, :location), where: cr.campaign_id == ^campaign.id and cr.backup_rider == true, order_by: r.name, select: r, select_merge: %{ - distance: st_distance(l.coords, ^campaign.location.coords), task_notes: cr.notes, task_capacity: cr.rider_capacity, task_enter_building: cr.enter_building, @@ -520,7 +514,7 @@ defmodule BikeBrigade.Delivery do delivery_url_token: cr.token } ) - |> Repo.preload([:location, :total_stats]) + |> Repo.preload([:total_stats]) backup_riders end From 41c8b48a4bc4f0344e1950e1f219305bf00d039d Mon Sep 17 00:00:00 2001 From: Ty Date: Wed, 20 Aug 2025 09:27:03 -0400 Subject: [PATCH 08/10] Fix warnings and remove inspects. --- lib/bike_brigade_web/live/campaign_live/new.ex | 1 - .../live/campaign_signup_live/show.html.heex | 5 ----- ...164616_add_default_location_id_to_program.exs | 2 -- test/bike_brigade/delivery_test.exs | 5 ++--- test/support/fixtures.ex | 16 ++++++++-------- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/lib/bike_brigade_web/live/campaign_live/new.ex b/lib/bike_brigade_web/live/campaign_live/new.ex index a063f8d7..ba107a71 100644 --- a/lib/bike_brigade_web/live/campaign_live/new.ex +++ b/lib/bike_brigade_web/live/campaign_live/new.ex @@ -33,7 +33,6 @@ defmodule BikeBrigadeWeb.CampaignLive.New do end def handle_event("save", params, socket) do - IO.inspect(params) {:noreply, socket} end diff --git a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex index dd7de753..5ca14cfd 100644 --- a/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex +++ b/lib/bike_brigade_web/live/campaign_signup_live/show.html.heex @@ -94,11 +94,6 @@ color={:red} size={:small} id="cancel-backup-rider-btn" - data-confirm={ - if campaign_today?(@campaign), - do: - "This delivery starts today. If you need to cancel your backup signup, please also text dispatch to let us know!" - } > Cancel backup signup diff --git a/priv/repo/migrations/20220614164616_add_default_location_id_to_program.exs b/priv/repo/migrations/20220614164616_add_default_location_id_to_program.exs index c4a30651..ff0d06d4 100644 --- a/priv/repo/migrations/20220614164616_add_default_location_id_to_program.exs +++ b/priv/repo/migrations/20220614164616_add_default_location_id_to_program.exs @@ -14,8 +14,6 @@ defmodule BikeBrigade.Repo.Migrations.AddDefaultLocationIdToProgram do flush() - now = DateTime.utc_now() - {program_ids, locations} = from( p in "programs", diff --git a/test/bike_brigade/delivery_test.exs b/test/bike_brigade/delivery_test.exs index dae6288f..4ca59bf9 100644 --- a/test/bike_brigade/delivery_test.exs +++ b/test/bike_brigade/delivery_test.exs @@ -229,8 +229,7 @@ defmodule BikeBrigade.DeliveryTest do test "remove_backup_rider_from_campaign/2 only removes backup riders, not regular riders", %{ campaign: campaign, - rider: regular_rider, - backup_rider: backup_rider + rider: regular_rider } do # Create a regular campaign rider {:ok, _regular_cr} = @@ -269,7 +268,7 @@ defmodule BikeBrigade.DeliveryTest do }) # Create a task - task = fixture(:task, %{campaign: campaign}) + fixture(:task, %{campaign: campaign}) # Try to create a regular campaign rider for the backup rider (this should fail) attrs = %{ diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 9d79468a..5003516c 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -221,14 +221,6 @@ defmodule BikeBrigade.Fixtures do Map.merge(defaults, attrs) end - def fixture(:sms_message_from_rider, rider, attrs) do - fixture(:sms_message, Map.merge(attrs, %{rider_id: rider.id, from: rider.phone})) - end - - def fixture(:sms_message_to_rider, rider, attrs) do - fixture(:sms_message, Map.merge(attrs, %{rider_id: rider.id, to: rider.phone})) - end - def fixture(:banner, attrs) do user = fixture(:user, %{is_dispatcher: true}) @@ -247,6 +239,14 @@ defmodule BikeBrigade.Fixtures do banner end + def fixture(:sms_message_from_rider, rider, attrs) do + fixture(:sms_message, Map.merge(attrs, %{rider_id: rider.id, from: rider.phone})) + end + + def fixture(:sms_message_to_rider, rider, attrs) do + fixture(:sms_message, Map.merge(attrs, %{rider_id: rider.id, to: rider.phone})) + end + defp fake_name() do # Faker has names like O'Connel, and the apostrophe gets translated to ' in HTML output # This is annoying in tests so we'll just filter out the apostrophe. From 5592642f992b9750b33ff8d97d7ae57599d5ef85 Mon Sep 17 00:00:00 2001 From: Ty Date: Wed, 20 Aug 2025 09:32:49 -0400 Subject: [PATCH 09/10] Remove unecessary test We don't need data-confirmations on the cancelling of backup riders on same day campaigns, I don't think. --- .../live/campaign_signup_live_test.exs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/test/bike_brigade_web/live/campaign_signup_live_test.exs b/test/bike_brigade_web/live/campaign_signup_live_test.exs index bc0f5f7a..68de0d6e 100644 --- a/test/bike_brigade_web/live/campaign_signup_live_test.exs +++ b/test/bike_brigade_web/live/campaign_signup_live_test.exs @@ -398,26 +398,6 @@ defmodule BikeBrigadeWeb.CampaignSignupLiveTest do assert length(backup_riders) == 0 end - test "shows confirmation dialog for same-day campaign cancellation", ctx do - # Create a campaign that starts today - today_campaign = - fixture(:campaign, %{ - program_id: ctx.program.id, - delivery_start: DateTime.utc_now(), - delivery_end: DateTime.utc_now() |> DateTime.add(3600, :second) - }) - - {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup/#{today_campaign.id}/") - - # Sign up as backup rider - live |> element("#signup-backup-rider-btn") |> render_click() - - # Try to cancel - should have confirmation dialog - cancel_button_html = live |> element("#cancel-backup-rider-btn") |> render() - assert cancel_button_html =~ "data-confirm" - assert cancel_button_html =~ "This delivery starts today" - end - test "does not show signup/cancel buttons for past campaigns", ctx do # Create a campaign in the past past_campaign = From d3ef9c0ed8c46f7879d8547091cadfeec7084627 Mon Sep 17 00:00:00 2001 From: Ty Date: Mon, 25 Aug 2025 11:38:46 -0400 Subject: [PATCH 10/10] format --- lib/bike_brigade_web/live/campaign_live/new.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/bike_brigade_web/live/campaign_live/new.ex b/lib/bike_brigade_web/live/campaign_live/new.ex index c071ddbd..5d3539c5 100644 --- a/lib/bike_brigade_web/live/campaign_live/new.ex +++ b/lib/bike_brigade_web/live/campaign_live/new.ex @@ -32,7 +32,6 @@ defmodule BikeBrigadeWeb.CampaignLive.New do {:noreply, assign(socket, :changeset, changeset)} end - def handle_event("save", _params, socket) do {:noreply, socket} end