Sit the Plani's two cards side by side, and show its settings

c514686 · Gabriel Morell · 2026-09-04 14:54

11 files +112 -3
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.ex
+31 −0
@@ -172,6 +172,37 @@ defmodule RoomSanctumWeb.PlaniLive.Show do
172 172 }
173 173 end
174 174
175 + @doc """
176 + Everything a Plani was configured with, for the settings card.
177 +
178 + Built here rather than read field by field in the template so that the
179 + "not set" cases are decided once: a blank bike limit means everything inside
180 + the radius, and saying so is more use than an empty cell.
181 + """
182 + def settings(plani) do
183 + [
184 + {"Home foci", :text, foci_name(plani.home_foci_id)},
185 + {"Ankyra", :text, plani.ankyra_id && "##{plani.ankyra_id}"},
186 + {"Client", :text, plani.client_id},
187 + {"Radius", :text, "#{plani.radius} m"},
188 + {"How many of each", :text, plani.limit},
189 + {"Bikes and docks per source", :text, plani.bike_limit || "all within the radius"},
190 + {"One card per stop", :flag, plani.break_out},
191 + {"One stop per line", :flag, plani.nearest_per_route},
192 + {"Following tint", :text, plani.follow_tint}
193 + ]
194 + end
195 +
196 + # A foci that has been deleted out from under a Plani should show as missing
197 + # rather than take the page down -- `get_foci!` raises.
198 + defp foci_name(nil), do: nil
199 +
200 + defp foci_name(id) do
201 + Configuration.get_foci!(id).name
202 + rescue
203 + Ecto.NoResultsError -> "missing (##{id})"
204 + end
205 +
175 206 @doc "A point as something readable, or nil."
176 207 def coordinates(%{anchor: %Geo.Point{coordinates: {lon, lat}}}) do
177 208 "#{Float.round(lat, 5)}, #{Float.round(lon, 5)}"
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.html.heex
+42 −3
@@ -120,8 +120,11 @@
120 120 </div>
121 121 </div>
122 122
123 −<div class="mx-4 py-2 flex flex-wrap">
124 − <div class="card card-compact w-full md:w-1/2 bg-base-100 shadow-xl m-2">
123 +<%!-- A grid rather than flex-wrap: these were already md:w-1/2, but the m-2
124 + beside it made each half a full rem wider than half, so the pair never
125 + fitted on one row and silently stacked. A gap belongs to the container. --%>
126 +<div class="mx-4 py-2 grid gap-4 md:grid-cols-2 items-start">
127 + <div class="card card-compact w-full bg-base-100 shadow-xl">
125 128 <div class="card-body">
126 129 <h3 class="card-title">
127 130 <i class="fas fa-fw fa-location-crosshairs"></i> Where it is
@@ -153,7 +156,7 @@
153 156 </div>
154 157 </div>
155 158
156 − <div class="card card-compact w-full md:w-1/2 bg-base-100 shadow-xl m-2">
159 + <div class="card card-compact w-full bg-base-100 shadow-xl">
157 160 <div class="card-body">
158 161 <h3 class="card-title"><i class="fas fa-fw fa-layer-group"></i> What it is asking</h3>
159 162 <% asked = RoomSanctumWeb.PlaniLive.Show.asked(@plani, @sources) %>
@@ -204,3 +207,39 @@
204 207 </div>
205 208 </div>
206 209 </div>
210 +
211 +<div class="mx-4 py-2">
212 + <%!-- Collapsed by default, and with a checkbox rather than an event: this is
213 + a reference the reader opens once to check a number, not state the
214 + server has any business remembering. --%>
215 + <div class="collapse collapse-arrow bg-base-100 shadow-xl">
216 + <input type="checkbox" />
217 + <div class="collapse-title card-title text-base">
218 + <i class="fas fa-fw fa-sliders"></i> How it is set up
219 + </div>
220 + <div class="collapse-content">
221 + <div class="grid gap-x-6 gap-y-1 sm:grid-cols-2 lg:grid-cols-3">
222 + <div
223 + :for={{label, kind, value} <- RoomSanctumWeb.PlaniLive.Show.settings(@plani)}
224 + class="flex items-center justify-between gap-2 border-b border-base-300 py-1"
225 + >
226 + <span class="text-sm opacity-70"><%= label %></span>
227 + <%= case {kind, value} do %>
228 + <% {:flag, true} -> %>
229 + <span class="badge badge-accent badge-sm gap-1">
230 + <i class="fas fa-fw fa-check text-xs"></i> on
231 + </span>
232 + <% {:flag, _} -> %>
233 + <span class="badge badge-ghost badge-sm">off</span>
234 + <% {_, nil} -> %>
235 + <span class="text-sm opacity-40">not set</span>
236 + <% {_, ""} -> %>
237 + <span class="text-sm opacity-40">not set</span>
238 + <% {_, shown} -> %>
239 + <span class="font-mono text-sm"><%= shown %></span>
240 + <% end %>
241 + </div>
242 + </div>
243 + </div>
244 + </div>
245 +</div>
added apps/room_sanctum/test/room_sanctum_web/plani_settings_test.exs
+39 −0
@@ -0,0 +1,39 @@
1 +defmodule RoomSanctumWeb.PlaniSettingsTest do
2 + use ExUnit.Case, async: true
3 +
4 + alias RoomSanctum.Configuration.Plani
5 + alias RoomSanctumWeb.PlaniLive.Show
6 +
7 + defp rows(plani), do: Show.settings(plani) |> Map.new(fn {l, k, v} -> {l, {k, v}} end)
8 +
9 + test "a blank bike limit says what blank means" do
10 + # "not set" would be true and useless: what it actually does is publish
11 + # everything inside the radius, which is the thing worth knowing.
12 + settings = rows(%Plani{radius: 800, limit: 5, bike_limit: nil})
13 +
14 + assert {:text, "all within the radius"} = settings["Bikes and docks per source"]
15 + end
16 +
17 + test "a set bike limit shows the number" do
18 + assert {:text, 12} = rows(%Plani{bike_limit: 12})["Bikes and docks per source"]
19 + end
20 +
21 + test "the flags are flags, so the template can draw them as flags" do
22 + settings = rows(%Plani{break_out: true, nearest_per_route: false})
23 +
24 + assert {:flag, true} = settings["One card per stop"]
25 + assert {:flag, false} = settings["One stop per line"]
26 + end
27 +
28 + test "the radius carries its unit" do
29 + assert {:text, "800 m"} = rows(%Plani{radius: 800})["Radius"]
30 + end
31 +
32 + test "nothing configured is nil rather than a made-up blank" do
33 + settings = rows(%Plani{})
34 +
35 + assert {:text, nil} = settings["Following tint"]
36 + assert {:text, nil} = settings["Home foci"]
37 + assert {:text, nil} = settings["Client"]
38 + end
39 +end

Parents: eac203f