towerops/lib/towerops_web/live/coverage_live/form.html.heex
Graham McIntire ebcd54be5b refactor: remove daisyUI, use straight Tailwind v4 with custom color palette
- Remove daisyUI plugins and theme from app.css
- Define 5 custom color palettes: sweet-salmon, desert-sand, wheat, cool-steel, cerulean
- Add @utility card, badge, btn classes to replace daisyUI components
- Replace all daisyUI classes across 16+ template files
- Update tests for new class return values
- Set light mode as default theme
2026-06-23 10:44:20 -05:00

245 lines
7 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="coverage"
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<.breadcrumb items={[
%{label: t("Dashboard"), navigate: ~p"/dashboard"},
%{label: t("Coverage"), navigate: ~p"/coverage"},
%{label: @page_title}
]} />
<.header>
{@page_title}
<:subtitle>
{t(
"Configure the radio and antenna. Coverage will be computed in the background once you save."
)}
</:subtitle>
<:actions>
<span class="inline-flex items-center px-3 py-1.5 rounded-md border border-blue-200 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20 text-sm">
<span class="text-blue-700 dark:text-blue-300 font-medium mr-2">EIRP:</span>
<span class="text-blue-900 dark:text-blue-100 font-mono">
{Float.round(@computed_eirp, 1)} dBm
</span>
</span>
</:actions>
</.header>
<.form
for={@form}
id="coverage-form"
phx-change="validate"
phx-submit="save"
class="mt-6 space-y-8"
>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<%!-- Identity --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Identity")}
</h3>
<.input
field={@form[:name]}
type="text"
label={t("Name")}
placeholder={t("e.g. North sector 5 GHz")}
required
/>
<.input
field={@form[:site_id]}
type="select"
label={t("Site")}
prompt={t("Select a site")}
options={Enum.map(@sites, &{&1.name, &1.id})}
required
/>
<.input
:if={@devices != []}
field={@form[:device_id]}
type="select"
label={t("Attach to device (optional)")}
prompt={t("No specific device")}
options={Enum.map(@devices, &{device_label(&1), &1.id})}
/>
<p
:if={@form[:site_id].value not in [nil, ""] and @devices == []}
class="text-xs text-gray-500 dark:text-gray-400"
>
{t("This site has no devices yet — coverage will not be attached to a specific device.")}
</p>
<.input
field={@form[:antenna_slug]}
type="select"
label={t("Antenna")}
prompt={t("Select an antenna")}
options={@antenna_options}
required
/>
</div>
<%!-- Radio location: draggable marker on a Leaflet map. --%>
<div class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Radio location")}
</h3>
<button
:if={radio_overridden?(@form)}
type="button"
phx-click="use_site_location"
class="text-xs text-blue-600 hover:underline dark:text-blue-400"
>
{t("Use site location")}
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
{t("Drag the marker to where the radio is mounted. Defaults to the site's coordinates.")}
</p>
<div
id="coverage-location-picker"
phx-hook="CoverageLocationPicker"
phx-update="ignore"
class="h-72 w-full rounded border border-gray-200 dark:border-white/10 bg-gray-100 dark:bg-gray-800"
data-marker-lat={radio_marker_lat(@form, @sites)}
data-marker-lon={radio_marker_lon(@form, @sites)}
>
</div>
<div
:if={radio_marker_lat(@form, @sites)}
class="text-xs font-mono text-gray-600 dark:text-gray-400"
>
{radio_marker_lat(@form, @sites)}, {radio_marker_lon(@form, @sites)}
<span :if={radio_overridden?(@form)} class="ml-2 text-blue-600 dark:text-blue-400">
({t("override")})
</span>
</div>
<.input field={@form[:latitude_override]} type="hidden" />
<.input field={@form[:longitude_override]} type="hidden" />
</div>
<%!-- Radio --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Radio")}
</h3>
<.input
field={@form[:frequency_ghz]}
type="number"
label={t("Frequency (GHz)")}
step="0.001"
min="0.7"
max="90"
required
/>
<.input
field={@form[:tx_power_dbm]}
type="number"
label={t("TX power (dBm)")}
step="0.1"
min="-10"
max="50"
required
/>
<.input
field={@form[:radius_mi]}
type="number"
label={t("Range (mi)")}
step="0.1"
min="0.3"
max="25"
required
/>
</div>
<%!-- Mounting --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Mounting")}
</h3>
<.input
field={@form[:height_agl_ft]}
type="number"
label={t("Height above ground (ft)")}
step="1"
min="3"
max="650"
required
/>
<.input
field={@form[:azimuth_deg]}
type="number"
label={t("Azimuth (°, true north)")}
step="1"
min="0"
max="360"
required
/>
<.input
field={@form[:downtilt_deg]}
type="number"
label={t("Tilt (°, positive = down)")}
step="0.1"
min="-10"
max="30"
/>
</div>
<%!-- Foliage --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Environment")}
</h3>
<div>
<label
for="coverage_foliage_tuning"
class="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-1"
>
{t("Foliage tuning")}: {Phoenix.HTML.Form.input_value(@form, :foliage_tuning) || 0}
</label>
<input
type="range"
id="coverage_foliage_tuning"
name="coverage[foliage_tuning]"
value={Phoenix.HTML.Form.input_value(@form, :foliage_tuning) || 0}
min="0"
max="100"
step="1"
class="w-full"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t("Higher values reduce predicted signal in wooded areas.")}
</p>
</div>
</div>
</div>
<div class="flex justify-end gap-2">
<.link
navigate={~p"/coverage"}
class="btn border border-cool-steel-300 bg-transparent dark:border-cool-steel-600"
>
{t("Cancel")}
</.link>
<.button type="submit" variant="primary">
{if @live_action == :new, do: t("Create coverage"), else: t("Save changes")}
</.button>
</div>
</.form>
</Layouts.authenticated>