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