diff --git a/lib/bike_brigade_web/live/campaign_signup_live/index.ex b/lib/bike_brigade_web/live/campaign_signup_live/index.ex new file mode 100644 index 00000000..e6e32f80 --- /dev/null +++ b/lib/bike_brigade_web/live/campaign_signup_live/index.ex @@ -0,0 +1,130 @@ +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) + # REVIEW: rename this to `campaign_meta` ? + |> 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 + + 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 + |> Enum.count(fn i -> i == Integer.to_string(rider_id) end) + + 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 diff --git a/lib/bike_brigade_web/live/campaign_signup_live/index.html.heex b/lib/bike_brigade_web/live/campaign_signup_live/index.html.heex new file mode 100644 index 00000000..95ae60a2 --- /dev/null +++ b/lib/bike_brigade_web/live/campaign_signup_live/index.html.heex @@ -0,0 +1,113 @@ + +<%= if @campaigns != [] do %> + <%= for {date, campaigns} <- @campaigns do %> +
+
+ <.date date={date} /> +
+ +
+ <% end %> +<% else %> +
No campaigns found
+<% 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 new file mode 100644 index 00000000..ee474484 --- /dev/null +++ b/test/bike_brigade_web/live/campaign_signup_live_test.exs @@ -0,0 +1,107 @@ +defmodule BikeBrigadeWeb.CampaignSignupLiveTest do + use BikeBrigadeWeb.ConnCase, only: [] + + import Phoenix.LiveViewTest + + @week_in_sec 604_900 + + describe "Index - General" do + setup ctx do + program = fixture(:program, %{name: "ACME Delivery"}) + res = login_as_rider(ctx) + Map.merge(res, %{program: program}) + end + + test "It displays the expected number of campaigns for this week", ctx do + campaigns = + for _n <- 1..3 do + fixture(:campaign, %{program_id: ctx.program.id}) + end + + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup") + + for c <- campaigns do + assert has_element?(live, "#campaign-#{c.id}") + end + end + + test "It displays a campaign in a future week", ctx do + campaign = + fixture(:campaign, %{ + program_id: ctx.program.id, + delivery_start: DateTime.utc_now() |> DateTime.add(@week_in_sec), + delivery_end: + DateTime.utc_now() |> DateTime.add(@week_in_sec) |> DateTime.add(60, :second) + }) + + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup") + refute has_element?(live, "#campaign-#{campaign.id}") + + week_ahead = Date.utc_today() |> Date.add(7) + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup?current_week=#{week_ahead}") + assert has_element?(live, "#campaign-#{campaign.id}") + end + + test "It displays a campaign in a previous week; button says 'Completed'", ctx do + campaign = + fixture(:campaign, %{ + program_id: ctx.program.id, + delivery_start: DateTime.utc_now() |> DateTime.add(-@week_in_sec), + delivery_end: + DateTime.utc_now() |> DateTime.add(-@week_in_sec) |> DateTime.add(60, :second) + }) + + {:ok, live, _html} = live(ctx.conn, ~p"/campaigns/signup") + refute has_element?(live, "#campaign-#{campaign.id}") + + week_ago = Date.utc_today() |> Date.add(-7) + {:ok, live, html} = live(ctx.conn, ~p"/campaigns/signup?current_week=#{week_ago}") + assert has_element?(live, "#campaign-#{campaign.id}") + assert html =~ "Completed" + end + end + + describe "Index - Campaign shows correct signup button" do + setup ctx do + program = fixture(:program, %{name: "ACME Delivery"}) + res = login_as_rider(ctx) + Map.merge(res, %{program: program}) + end + + test "A campaign shows the correct filled to total tasks", ctx do + campaign = fixture(:campaign, %{program_id: ctx.program.id}) + rider_1 = fixture(:rider, %{name: "Hannah Bannana"}) + _rider_2 = fixture(:rider, %{name: "Kiwi Stevie"}) + fixture(:task, %{campaign: campaign, rider: rider_1}) + fixture(:task, %{campaign: campaign, rider: nil}) + + {:ok, _live, html} = live(ctx.conn, ~p"/campaigns/signup") + + # HACK to cleanup html with tons of whitespace. + # Could also just use Floki to find the element and test it's there. + normalized_html = html |> String.split() |> Enum.join(" ") + assert normalized_html =~ "1 / 2 Tasks filled" + end + + test "'signup' when rider hasn't signed up and there are open tasks", ctx do + campaign = fixture(:campaign, %{program_id: ctx.program.id}) + rider_1 = fixture(:rider, %{name: "Hannah Bannana"}) + fixture(:rider, %{name: "Kiwi Stevie"}) + fixture(:task, %{campaign: campaign, rider: rider_1}) + fixture(:task, %{campaign: campaign, rider: nil}) + + {:ok, _live, html} = live(ctx.conn, ~p"/campaigns/signup") + assert html =~ "Sign up" + end + + test "'signed up for N deliveries' if open deliveries and rider has at least one.", ctx do + campaign = fixture(:campaign, %{program_id: ctx.program.id}) + fixture(:task, %{campaign: campaign, rider: ctx.rider}) + fixture(:task, %{campaign: campaign, rider: ctx.rider}) + fixture(:task, %{campaign: campaign, rider: nil}) + + {:ok, _live, html} = live(ctx.conn, ~p"/campaigns/signup") + assert html =~ "Signed up for 2 deliveries" + end + end +end