towerops/lib/towerops_web/live/maintenance_live/form.html.heex
Graham McIntie 0f9fac76d9 Add maintenance windows LiveView UI
- Index page with All/Active/Upcoming/Past filter tabs
- Show page with active banner, edit/delete actions
- Form page with scope picker (org-wide/site/device), datetime fields
- Nav link added to desktop and mobile nav
2026-02-16 10:26:10 -06:00

231 lines
10 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="maintenance"
>
<div class="mb-6">
<.link
navigate={~p"/maintenance"}
class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<.icon name="hero-arrow-left" class="h-4 w-4" />
{t("Back to Maintenance Windows")}
</.link>
</div>
<h1 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{@page_title}</h1>
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-6">
<.form
for={@changeset}
phx-change="validate"
phx-submit="save"
id="maintenance-form"
>
<div class="space-y-6">
<%!-- Name --%>
<div>
<label for="maintenance_window_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Name")} <span class="text-red-500">*</span>
</label>
<input
type="text"
name="maintenance_window[name]"
id="maintenance_window_name"
value={Ecto.Changeset.get_field(@changeset, :name) || ""}
required
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500"
placeholder={t("e.g., Tower firmware upgrade")}
/>
<%= for {msg, _} <- (@changeset.errors[:name] || []) do %>
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{msg}</p>
<% end %>
</div>
<%!-- Reason --%>
<div>
<label for="maintenance_window_reason" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Reason")}
</label>
<textarea
name="maintenance_window[reason]"
id="maintenance_window_reason"
rows="3"
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500"
placeholder={t("Optional description of the maintenance work")}
>{Ecto.Changeset.get_field(@changeset, :reason) || ""}</textarea>
</div>
<%!-- Scope --%>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{t("Scope")}</label>
<div class="flex items-center gap-4 mb-3">
<label class="inline-flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="scope_type"
value="org"
checked={@scope_type == "org"}
phx-click="change_scope"
phx-value-scope_type="org"
class="text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Org-wide")}</span>
</label>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="scope_type"
value="site"
checked={@scope_type == "site"}
phx-click="change_scope"
phx-value-scope_type="site"
class="text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Site")}</span>
</label>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="scope_type"
value="device"
checked={@scope_type == "device"}
phx-click="change_scope"
phx-value-scope_type="device"
class="text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Device")}</span>
</label>
</div>
<%= if @scope_type == "site" do %>
<select
name="maintenance_window[site_id]"
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800 dark:text-white"
>
<option value="">{t("Select a site...")}</option>
<%= for site <- @sites do %>
<option value={site.id} selected={Ecto.Changeset.get_field(@changeset, :site_id) == site.id}>
{site.name}
</option>
<% end %>
</select>
<% end %>
<%= if @scope_type == "device" do %>
<select
name="maintenance_window[device_id]"
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800 dark:text-white"
>
<option value="">{t("Select a device...")}</option>
<%= for device <- @devices do %>
<option value={device.id} selected={Ecto.Changeset.get_field(@changeset, :device_id) == device.id}>
{device.name}
</option>
<% end %>
</select>
<% end %>
</div>
<%!-- Time Range --%>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="maintenance_window_starts_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Starts At")} <span class="text-red-500">*</span>
</label>
<input
type="datetime-local"
name="maintenance_window[starts_at]"
id="maintenance_window_starts_at"
value={format_datetime_local(Ecto.Changeset.get_field(@changeset, :starts_at))}
required
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800 dark:text-white"
/>
<%= for {msg, _} <- (@changeset.errors[:starts_at] || []) do %>
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{msg}</p>
<% end %>
</div>
<div>
<label for="maintenance_window_ends_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Ends At")} <span class="text-red-500">*</span>
</label>
<input
type="datetime-local"
name="maintenance_window[ends_at]"
id="maintenance_window_ends_at"
value={format_datetime_local(Ecto.Changeset.get_field(@changeset, :ends_at))}
required
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800 dark:text-white"
/>
<%= for {msg, _} <- (@changeset.errors[:ends_at] || []) do %>
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{msg}</p>
<% end %>
</div>
</div>
<%!-- Suppress Alerts --%>
<div>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="hidden" name="maintenance_window[suppress_alerts]" value="false" />
<input
type="checkbox"
name="maintenance_window[suppress_alerts]"
value="true"
checked={Ecto.Changeset.get_field(@changeset, :suppress_alerts) == true}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Suppress alerts during this window")}</span>
</label>
</div>
<%!-- Recurring --%>
<div>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="hidden" name="maintenance_window[recurring]" value="false" />
<input
type="checkbox"
name="maintenance_window[recurring]"
value="true"
checked={Ecto.Changeset.get_field(@changeset, :recurring) == true}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Recurring")}</span>
</label>
</div>
<%!-- Recurrence Rule --%>
<%= if Ecto.Changeset.get_field(@changeset, :recurring) == true do %>
<div>
<label for="maintenance_window_recurrence_rule" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Recurrence Rule")}
</label>
<input
type="text"
name="maintenance_window[recurrence_rule]"
id="maintenance_window_recurrence_rule"
value={Ecto.Changeset.get_field(@changeset, :recurrence_rule) || ""}
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-blue-500 dark:border-white/10 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500"
placeholder={t("e.g., FREQ=WEEKLY;BYDAY=SU")}
/>
</div>
<% end %>
<%!-- Submit --%>
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 dark:border-white/10">
<.link
navigate={~p"/maintenance"}
class="rounded-lg px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors"
>
{t("Cancel")}
</.link>
<button
type="submit"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
{t("Save")}
</button>
</div>
</div>
</.form>
</div>
</Layouts.authenticated>