Skip to content
Merged
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
85 changes: 84 additions & 1 deletion lib/bike_brigade/delivery.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

def create_campaign_rider_without_backup_check(attrs) do
%CampaignRider{}
|> CampaignRider.changeset(attrs)
|> Repo.insert(
Expand Down Expand Up @@ -442,7 +476,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: %{
Expand All @@ -464,6 +498,55 @@ defmodule BikeBrigade.Delivery do
{riders, tasks}
end

def get_backup_riders(%Campaign{} = campaign) do
backup_riders =
Repo.all(
from cr in CampaignRider,
join: r in assoc(cr, :rider),
where: cr.campaign_id == ^campaign.id and cr.backup_rider == true,
order_by: r.name,
select: r,
select_merge: %{
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([: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
Expand Down
4 changes: 3 additions & 1 deletion lib/bike_brigade/delivery/campaign_rider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,113 @@
</li>
<% end %>
</ul>

<!-- Backup Riders Section -->
<div :if={@backup_riders && Enum.count(@backup_riders) > 0} class="border-t mt-4 pt-4">
<div class="ml-2 mb-2">
<h3 class="text-sm font-medium leading-6 text-gray-900 ml-2">
Backup Riders ({Enum.count(@backup_riders)})
</h3>
</div>

<ul class="overflow-y-auto scrolling-touch" id="backup-riders-list">
<%= for {_id, rider} <- @backup_riders do %>
<li id={"backup-riders-list:#{rider.id}"}>
<.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"}"
}
>
<div class="flex flex-row items-center px-4 py-4">
<div class="text-sm font-medium">
{rider.name}
<span class="text-xs text-blue-600">(backup)</span>
</div>

<%= if rider.task_enter_building do %>
<Heroicons.building_office
mini
class="flex-shrink-0 w-4 h-4 mx-1 text-gray-500 justify-self-end"
/>
<% end %>

<%= if novice_participant?(rider) do %>
<.with_tooltip>
<Heroicons.star
mini
class="flex-shrink-0 w-4 h-4 mx-1 text-gray-500 justify-self-end"
/>
<:tooltip>
<div class="w-18">
New rider (less than 5 campaigns)
</div>
</:tooltip>
</.with_tooltip>
<% end %>

<%= if has_notes?(rider) do %>
<Heroicons.pencil_square mini class="flex-shrink-0 w-4 h-4 mx-1 text-gray-500" />
<% end %>

<%= if rider.text_based_itinerary do %>
<.with_tooltip>
<Heroicons.signal_slash mini class="flex-shrink-0 w-4 h-4 mx-1 text-gray-500" />
<:tooltip>
<div class="w-40">
This rider will receive an extra message with text-only delivery instructions
</div>
</:tooltip>
</.with_tooltip>
<% end %>
</div>
</.link>
<%= if selected?(@selected_rider, rider) do %>
<div class="p-2 mx-1 rounded-b shadow-inner bg-blue-50">
<div class="flex flex-row my-2">
<Heroicons.clock mini class="flex-shrink-0 w-4 h-4 text-gray-500" />
<div class="mx-1 text-sm leading-5 text-gray-800">
{pickup_window(@campaign, rider)}
</div>
</div>
<%= if has_notes?(rider) do %>
<div class="flex flex-row my-2">
<Heroicons.pencil_square mini class="flex-shrink-0 w-4 h-4 text-gray-500" />
<div class="mx-1 text-sm leading-5 text-gray-600">
{rider.task_notes}
</div>
</div>
<% end %>

<div class="mx-1 text-sm leading-5 text-gray-800">
Signed up as backup rider - no specific tasks assigned
</div>

<div class="flex flex-row mt-5 space-x-1">
<.button navigate={~p"/messages/#{rider}"} color={:secondary} size={:xsmall}>
Message
</.button>
<.button
phx-click={JS.push("convert_backup_to_rider", value: %{rider_id: rider.id})}
color={:green}
size={:xsmall}
>
Convert to Rider
</.button>
</div>
<div class="mt-1">
<.button
phx-click={JS.push("remove_backup_rider", value: %{rider_id: rider.id})}
color={:red}
size={:xsmall}
>
Remove Backup
</.button>
</div>
</div>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
83 changes: 79 additions & 4 deletions lib/bike_brigade_web/live/campaign_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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]

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/bike_brigade_web/live/campaign_live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
tasks_query={@tasks_query}
selected_task={@selected_task}
selected_rider={@selected_rider}
backup_riders={@backup_riders}
/>
</div>
<div class="px-2 py-3 bg-white shadow min-h-96 max-h-128 md:min-h-0 md:max-h-full md:h-full md:order-last sm:rounded-lg">
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading