diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index fb00feba..c801e49e 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -27,7 +27,7 @@ defmodule BikeBrigade.Locations.CommunityFridge do def changeset(community_fridge, params) do community_fridge |> cast(params, @fields ++ [:location_id]) - |> validate_required([:name, :location_id]) + |> validate_required([:name]) |> unique_constraint(:location_id) |> assoc_constraint(:location) end diff --git a/lib/bike_brigade_web/components/layouts.ex b/lib/bike_brigade_web/components/layouts.ex index 7771b87d..9685aa26 100644 --- a/lib/bike_brigade_web/components/layouts.ex +++ b/lib/bike_brigade_web/components/layouts.ex @@ -32,6 +32,7 @@ defmodule BikeBrigadeWeb.Layouts do :campaigns, :opportunities, :riders, + :community_fridges, :stats, :users, :messages, @@ -69,6 +70,15 @@ defmodule BikeBrigadeWeb.Layouts do Riders + <.sidebar_link + selected={@current_page == :community_fridges} + navigate={~p"/community_fridges"} + > + <:icon> + + + Community Fridges + <.sidebar_link selected={@current_page == :stats} navigate={~p"/stats"}> <:icon> diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.ex b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex new file mode 100644 index 00000000..e63130fd --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex @@ -0,0 +1,55 @@ +defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do + use BikeBrigadeWeb, :live_component + + alias BikeBrigade.Locations + + @impl true + def update(%{community_fridge: community_fridge} = assigns, socket) do + changeset = Locations.change_community_fridge(community_fridge) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"community_fridge" => params}, socket) do + changeset = + socket.assigns.community_fridge + |> Locations.change_community_fridge(params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"community_fridge" => params}, socket) do + save_community_fridge(socket, socket.assigns.action, params) + end + + defp save_community_fridge(socket, :edit, params) do + case Locations.update_community_fridge(socket.assigns.community_fridge, params) do + {:ok, _community_fridge} -> + {:noreply, + socket + |> put_flash(:info, "Community fridge updated successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_community_fridge(socket, :new, params) do + case Locations.create_community_fridge(params) do + {:ok, _community_fridge} -> + {:noreply, + socket + |> put_flash(:info, "Community fridge created successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +end diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex new file mode 100644 index 00000000..7741fd21 --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex @@ -0,0 +1,37 @@ +
+ <.header>{@title} + <.simple_form + :let={f} + for={@changeset} + id="community-fridge-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input type="text" field={f[:name]} label="Name" /> + + <.input type="textarea" field={f[:description]} label="Description" /> + + <.input type="text" field={f[:photo]} label="Photo URL" /> + + <.input + type="checkbox" + field={f[:active]} + label="Active" + /> + + <.input + type="checkbox" + field={f[:pair_preferred]} + label="Pair Preferred" + help_text="Deliveries to this fridge prefer a rider pair" + /> + + <:actions> + <.button navigate={~p"/community_fridges"} color={:white}> + Cancel + + <.button type="submit" phx-disable-with="Saving...">Save + + +
diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.ex b/lib/bike_brigade_web/live/community_fridge_live/index.ex new file mode 100644 index 00000000..ac341c8a --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/index.ex @@ -0,0 +1,39 @@ +defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do + use BikeBrigadeWeb, :live_view + + alias BikeBrigade.Locations + alias BikeBrigade.Locations.CommunityFridge + + @impl Phoenix.LiveView + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page, :community_fridges) + |> assign(:page_title, "Community Fridges") + |> assign(:community_fridge, nil) + |> assign(:community_fridges, Locations.list_community_fridges())} + end + + @impl Phoenix.LiveView + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Community Fridges") + |> assign(:community_fridge, nil) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Fridge") + |> assign(:community_fridge, %CommunityFridge{}) + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Fridge") + |> assign(:community_fridge, Locations.get_community_fridge!(id)) + end +end diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex new file mode 100644 index 00000000..dd8e8ee9 --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex @@ -0,0 +1,47 @@ +<.header> + Community Fridges + <:subtitle> + Community fridges are neighbourhood food-sharing locations. Dispatchers can manage fridge details here. + + +
+ <.button patch={~p"/community_fridges/new"}> + New Community Fridge + +
+<.table id="community_fridges" rows={@community_fridges}> + <:col :let={community_fridge} label="Name"> + {community_fridge.name} + + <:col :let={community_fridge} label="Description"> + {community_fridge.description} + + <:col :let={community_fridge} label="Pair Preferred"> + {if community_fridge.pair_preferred, do: "Yes", else: "No"} + + <:col :let={community_fridge} label="Active"> + {if community_fridge.active, do: "Yes", else: "No"} + + + <:action :let={community_fridge}> + <.link patch={~p"/community_fridges/#{community_fridge.id}/edit"} class="link"> + Edit + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="community-fridge-modal" + show + on_cancel={JS.navigate(~p"/community_fridges")} +> + <.live_component + module={BikeBrigadeWeb.CommunityFridgeLive.FormComponent} + id={@community_fridge.id || :new} + action={@live_action} + title={@page_title} + community_fridge={@community_fridge} + navigate={~p"/community_fridges"} + /> + diff --git a/lib/bike_brigade_web/router.ex b/lib/bike_brigade_web/router.ex index aa288e6a..3080f591 100644 --- a/lib/bike_brigade_web/router.ex +++ b/lib/bike_brigade_web/router.ex @@ -180,6 +180,10 @@ defmodule BikeBrigadeWeb.Router do live "/tags/:id/edit", TagLive.Index, :edit live "/delivery_notes", DeliveryNoteLive.Index, :index + + live "/community_fridges", CommunityFridgeLive.Index, :index + live "/community_fridges/new", CommunityFridgeLive.Index, :new + live "/community_fridges/:id/edit", CommunityFridgeLive.Index, :edit end get "/stats/leaderboard/download", ExportStatsController, :leaderboard diff --git a/test/bike_brigade_web/live/community_fridge_live_test.exs b/test/bike_brigade_web/live/community_fridge_live_test.exs new file mode 100644 index 00000000..8248e1f0 --- /dev/null +++ b/test/bike_brigade_web/live/community_fridge_live_test.exs @@ -0,0 +1,91 @@ +defmodule BikeBrigadeWeb.CommunityFridgeLiveTest do + use BikeBrigadeWeb.ConnCase + import Phoenix.LiveViewTest + + describe "Community Fridges index (dispatcher)" do + setup [:login] + + test "lists all community fridges", ctx do + fridge = fixture(:community_fridge, %{name: "Test Fridge"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ "Community Fridges" + assert html =~ fridge.name + end + + test "can create a new community fridge", ctx do + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges") + + view |> element("a", "New Community Fridge") |> render_click() + assert_patched(view, ~p"/community_fridges/new") + + {:ok, _view, html} = + view + |> form("#community-fridge-form", community_fridge: %{name: "Brand New Fridge"}) + |> render_submit() + |> follow_redirect(ctx.conn) + + assert html =~ "Community fridge created successfully" + assert html =~ "Brand New Fridge" + end + + test "validates name is required", ctx do + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges/new") + + html = + view + |> form("#community-fridge-form", community_fridge: %{name: ""}) + |> render_submit() + + assert html =~ "can't be blank" + end + + test "can edit a community fridge", ctx do + fridge = fixture(:community_fridge, %{name: "Original Fridge"}) + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges") + + view |> element("a", "Edit") |> render_click() + assert_patched(view, ~p"/community_fridges/#{fridge.id}/edit") + + {:ok, _view, html} = + view + |> form("#community-fridge-form", community_fridge: %{name: "Updated Fridge"}) + |> render_submit() + |> follow_redirect(ctx.conn) + + assert html =~ "Community fridge updated successfully" + assert html =~ "Updated Fridge" + refute html =~ "Original Fridge" + end + + test "can navigate directly to edit page", ctx do + fridge = fixture(:community_fridge, %{name: "Edit Me"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges/#{fridge.id}/edit") + + assert html =~ "Edit Fridge" + end + + test "edit preserves existing values", ctx do + fridge = + fixture(:community_fridge, %{ + name: "Preserve Me", + description: "Some description", + active: true, + pair_preferred: true + }) + + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges/#{fridge.id}/edit") + + assert html =~ "Preserve Me" + assert html =~ "Some description" + end + end + + describe "Community Fridges access control" do + setup [:login_as_rider] + + test "non-dispatchers cannot access community fridges page", ctx do + assert {:error, {:redirect, %{to: "/login"}}} = live(ctx.conn, ~p"/community_fridges") + end + end +end