Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

140 - Rider campaign signup #263

Merged
merged 8 commits into from
Jan 5, 2024
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
54 changes: 51 additions & 3 deletions lib/bike_brigade/delivery.ex
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,54 @@ defmodule BikeBrigade.Delivery do
|> Repo.preload(preload)
end

@doc """
Fetches how many open vs filled tasks there are (optionally, by week)
and groups them by campaign ID.
Written in order to show how "full" a campaign is.
"""
def get_total_tasks_and_open_tasks(week \\ nil) do
query =
from t in Task,
as: :task,
join: c in assoc(t, :campaign),
as: :campaign

query =
if week do
start_date = LocalizedDateTime.new!(week, ~T[00:00:00])
end_date = Date.add(week, 6) |> LocalizedDateTime.new!(~T[23:59:59])

query
|> where([campaign: c], c.delivery_start >= ^start_date and c.delivery_start <= ^end_date)
else
query
end

query =
from [task: t, campaign: c] in query,
group_by: c.id,
select: %{
campaign_id: c.id,
total_tasks: count(t.id),
filled_tasks:
sum(fragment("CASE WHEN ? IS NULL THEN 0 ELSE 1 END", t.assigned_rider_id)),
rider_ids: fragment("array_agg(?)", t.assigned_rider_id)
}

Repo.all(query)
|> Enum.into(
%{},
fn x ->
{x.campaign_id,
%{
rider_ids_counts: Enum.frequencies(x.rider_ids),
total_tasks: x.total_tasks,
filled_tasks: x.filled_tasks
}}
end
)
end

alias BikeBrigade.Delivery.CampaignRider

def get_campaign_rider!(token) do
Expand Down Expand Up @@ -539,14 +587,14 @@ defmodule BikeBrigade.Delivery do
end
end

def remove_rider_from_campaign(campaign, rider) do
if cr = Repo.get_by(CampaignRider, campaign_id: campaign.id, rider_id: rider.id) do
def remove_rider_from_campaign(campaign, rider_id) do
if cr = Repo.get_by(CampaignRider, campaign_id: campaign.id, rider_id: rider_id) do
delete_campaign_rider(cr)
end

tasks =
from(t in Task,
where: t.campaign_id == ^campaign.id and t.assigned_rider_id == ^rider.id
where: t.campaign_id == ^campaign.id and t.assigned_rider_id == ^rider_id
)
|> Repo.all()

Expand Down
6 changes: 6 additions & 0 deletions lib/bike_brigade_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ defmodule BikeBrigadeWeb.Layouts do
</.sidebar_link>
<!-- Rider Specific links -->
<div :if={!@is_dispatcher}>
<.sidebar_link selected={@current_page == :campaigns_signup} navigate={~p"/campaigns/signup"}>
<:icon>
<Heroicons.inbox />
</:icon>
Campaigns
</.sidebar_link>
<.sidebar_link selected={@current_page == :itinerary} href={~p"/itinerary"}>
<:icon>
<Heroicons.calendar_days solid />
Expand Down
7 changes: 5 additions & 2 deletions lib/bike_brigade_web/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ defmodule BikeBrigadeWeb.CoreComponents do

attr :color, :atom,
default: :primary,
values: [:primary, :secondary, :white, :green, :red, :lightred, :clear, :black]
values: [:primary, :secondary, :white, :green, :red, :lightred, :clear, :black, :disabled]

attr :rounded, :atom,
default: :normal,
values: [:none, :small, :normal, :medium, :full]

attr :class, :string, default: nil
attr :rest, :global, include: ~w(href patch navigate disabled)
attr :rest, :global, include: ~w(href patch navigate disabled replace)
slot :inner_block, required: true

@button_base_classes [
Expand Down Expand Up @@ -119,6 +119,9 @@ defmodule BikeBrigadeWeb.CoreComponents do

:black ->
"border-gray-300 text-white bg-black hover:bg-white hover:text-black"

:disabled ->
"border-gray-300 text-neutral-900 bg-neutral-100 cursor-not-allowed"
end
end

Expand Down
2 changes: 0 additions & 2 deletions lib/bike_brigade_web/live/campaign_live/new.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ defmodule BikeBrigadeWeb.CampaignLive.New do

@impl Phoenix.LiveView
def handle_event("validate", %{"campaign" => campaign_params}, socket) do
IO.inspect(campaign_params)

changeset =
socket.assigns.campaign
|> Delivery.change_campaign(campaign_params)
Expand Down
2 changes: 1 addition & 1 deletion lib/bike_brigade_web/live/campaign_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ defmodule BikeBrigadeWeb.CampaignLive.Show do
def handle_event("remove_rider", %{"rider_id" => rider_id}, socket) do
rider = get_rider(socket, rider_id)

Delivery.remove_rider_from_campaign(socket.assigns.campaign, rider)
Delivery.remove_rider_from_campaign(socket.assigns.campaign, rider.id)

{:noreply, socket}
end
Expand Down
131 changes: 131 additions & 0 deletions lib/bike_brigade_web/live/campaign_signup_live/index.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
defmodule BikeBrigadeWeb.CampaignSignupLive.Index do
use BikeBrigadeWeb, :live_view

alias BikeBrigade.Utils
alias BikeBrigade.LocalizedDateTime
alias BikeBrigade.Delivery

import BikeBrigadeWeb.CampaignHelpers

@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Delivery.subscribe()
end

current_week =
LocalizedDateTime.today()
|> Date.beginning_of_week()

campaigns = fetch_campaigns(current_week)

{:ok,
socket
|> assign(:page, :campaigns)
|> assign(:page_title, "Campaign Signup List")
|> assign(:current_week, current_week)
|> assign(:campaign_task_counts, Delivery.get_total_tasks_and_open_tasks(current_week))
|> assign(:campaigns, campaigns)}
end

@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end

# -- Delivery callbacks

@broadcasted_infos [
:task_created,
:task_deleted,
:task_updated,
:campaign_rider_created,
:campaign_rider_deleted
]

@impl Phoenix.LiveView
def handle_info({event, entity}, socket) when event in @broadcasted_infos do
if entity_in_campaigns?(socket, entity.campaign_id) do
{:noreply, refetch_and_assign_data(socket)}
else
{:noreply, socket}
end
end

## -- End Delivery callbacks

defp apply_action(socket, :index, params) do
socket =
case params do
%{"current_week" => week} ->
week = Date.from_iso8601!(week)

assign(socket,
current_week: week,
campaigns: fetch_campaigns(week),
campaign_task_counts: Delivery.get_total_tasks_and_open_tasks(week)
)

_ ->
socket
end

socket
|> assign(:campaign, nil)
end

defp fetch_campaigns(current_week) do
Delivery.list_campaigns(current_week,
preload: [:program, :stats, :latest_message, :scheduled_message]
)
|> Enum.reverse()
|> Utils.ordered_group_by(&LocalizedDateTime.to_date(&1.delivery_start))
|> Enum.reverse()
end

# TODO HACK: right now everytime something about a task, or campaign rider
# changes (add, edit, delete), we refetch all tasks and campaign riders.
# This may eventually become a problem.
defp refetch_and_assign_data(socket) do
week = socket.assigns.current_week

socket
|> assign(:campaign_task_counts, Delivery.get_total_tasks_and_open_tasks(week))
|> assign(:campaigns, fetch_campaigns(week))
end

defp campaign_is_in_past(campaign) do
date_now = DateTime.utc_now()

case DateTime.compare(campaign.delivery_end, date_now) do
:gt -> false
:eq -> false
:lt -> true
end
end

defp get_signup_text(campaign_id, rider_id, campaign_task_counts) do
count_tasks_for_current_rider =
campaign_task_counts[campaign_id].rider_ids_counts[rider_id] || 0

cond do
count_tasks_for_current_rider > 0 ->
"Signed up for #{count_tasks_for_current_rider} deliveries"

true ->
"Sign up"
end
end

defp campaign_tasks_fully_assigned?(c_id, campaign_task_count) do
campaign_task_count[c_id][:filled_tasks] == campaign_task_count[c_id][:total_tasks]
end

# Use this to determine if we need to refetch data to update the liveview.
# ex: dispatcher changes riders/tasks, or another rider signs up -> refetch.
defp entity_in_campaigns?(socket, entity_campaign_id) do
socket.assigns.campaigns
|> Enum.flat_map(fn {_date, campaigns} -> campaigns end)
|> Enum.find(false, fn c -> c.id == entity_campaign_id end)
end
end
112 changes: 112 additions & 0 deletions lib/bike_brigade_web/live/campaign_signup_live/index.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<nav
class="flex flex-col md:flex-row md:items-center justify-between px-4 py-3 mb-4 border-b-2 border-gray-200"
aria-label="Pagination"
>
<div class="flex-1">
<p class="font-medium xl:flex">
<span class="mr-2">Showing week of</span>
<time datetime={@current_week} class="font-medium">
<%= Calendar.strftime(@current_week, "%B %-d, %Y") %>
</time>
</p>
</div>
<div class="flex justify-between mt-4 md:mt-0 align-end">
<span class="relative z-0 inline-flex rounded-md shadow-sm">
<.link
patch={~p"/campaigns/signup?current_week=#{Date.add(@current_week, -7)}"}
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
>
<Heroicons.chevron_left solid class="w-5 h-5" />
</.link>
<.link
patch={~p"/campaigns/signup?current_week=#{Date.beginning_of_week(LocalizedDateTime.today())}"}
class="relative items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 sm:inline-flex hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
>
Today
</.link>

<.link
patch={~p"/campaigns/signup?current_week=#{Date.add(@current_week, 7)}"}
class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
>
<Heroicons.chevron_right solid class="w-5 h-5" />
</.link>
</span>
</div>
</nav>
<%= if @campaigns != [] do %>
<%= for {date, campaigns} <- @campaigns do %>
<div class="flex flex-col md:flex-row w-full bg-white shadow sm:my-1 sm:rounded-md">
<div class="w-32 py-4 pl-4">
<.date date={date} />
</div>
<ul role="list" class="w-full divide-y divide-gray-200">
<%= for c <- campaigns do %>
<li id={"campaign-#{c.id}"}>
<div class="px-4 py-4">
<div class="items-center justify-between md:flex">
<div class="flex items-center mb-2 space-x-1">
<p
class="text-sm font-medium"
data-test-group="campaign-name"
>
<%= name(c) %>
</p>
</div>
<div class="flex-shrink-0 space-y-1 md:space-y-0 md:space-x-2 md:flex">
<%= cond do %>
<% campaign_is_in_past(c) -> %>
<.button
size={:xsmall}
color={:disabled}>
Completed
</.button>

<% campaign_tasks_fully_assigned?(c.id, @campaign_task_counts) -> %>
<.button
size={:xsmall}
color={:secondary}
navigate={~p"/campaigns/signup/#{c}/"}>
Campaign Filled
</.button>

<% true -> %>
<.button
size={:xsmall}
color={:primary}
navigate={~p"/campaigns/signup/#{c}/"}>
<%= get_signup_text(c.id, @current_user.rider_id, @campaign_task_counts) %>
</.button>
<% end %>
</div>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="flex flex-col md:flex-row justify-between w-full">

<p class="flex items-center mt-0 text-sm text-gray-700">
<Icons.maki_bicycle_share class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-500" />
<span>
<%= @campaign_task_counts[c.id][:filled_tasks] %> /
<%= @campaign_task_counts[c.id][:total_tasks] %> Tasks filled
</span>
</p>

<p class="flex items-center text-sm text-gray-700">
<Heroicons.clock
mini
aria-label="Pickup Time"
class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-500"
/>
Pickup time: <%= pickup_window(c) %>
</p>
</div>
</div>
</div>
</li>
<% end %>
</ul>
</div>
<% end %>
<% else %>
<div class="py-4 pl-4">No campaigns found</div>
<% end %>
Loading
Loading