towerops/lib/towerops_web/live/reports_live.ex
Graham McIntire 5a7cdc7138 refactor: apply custom color palette across entire codebase
- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat
- Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast
- ~11,600 color replacements across ~99 files
- Update tests for new color class names
2026-06-23 10:58:48 -05:00

150 lines
4.9 KiB
Elixir

defmodule ToweropsWeb.ReportsLive do
@moduledoc """
Scheduled Reports management page.
Lists existing reports and provides a form to create new ones.
"""
use ToweropsWeb, :live_view
alias Towerops.Reports
alias Towerops.Reports.Report
alias Towerops.Workers.ReportWorker
@impl true
def mount(_params, _session, socket) do
organization = socket.assigns.current_scope.organization
reports = Reports.list_reports(organization.id)
{:ok,
socket
|> assign(:page_title, "Reports")
|> assign(:organization_id, organization.id)
|> assign(:reports, reports)
|> assign(:show_form, false)
|> assign(:form, to_form(Report.changeset(%Report{}, %{})))}
end
@impl true
def handle_event("show_form", _, socket) do
{:noreply, assign(socket, show_form: true)}
end
@impl true
def handle_event("hide_form", _, socket) do
{:noreply, assign(socket, show_form: false)}
end
@impl true
def handle_event("save", %{"report" => params}, socket) do
recipients =
(params["recipients_text"] || "")
|> String.split([",", "\n", ";"])
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
attrs =
params
|> Map.put("recipients", recipients)
|> Map.put("organization_id", socket.assigns.organization_id)
|> Map.put("created_by_id", socket.assigns.current_scope.user.id)
|> Map.put("schedule", %{"type" => params["schedule_type"] || "weekly"})
|> Map.put("scope", %{
"target" => params["scope_target"] || "all",
"days" => String.to_integer(params["scope_days"] || "7")
})
case Reports.create_report(attrs) do
{:ok, _report} ->
reports = Reports.list_reports(socket.assigns.organization_id)
{:noreply,
socket
|> put_flash(:info, "Report created successfully")
|> assign(:reports, reports)
|> assign(:show_form, false)
|> assign(:form, to_form(Report.changeset(%Report{}, %{})))}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
@impl true
def handle_event("toggle", %{"id" => id}, socket) do
with %_{} = report <- fetch_org_report(socket, id),
{:ok, _} <- Reports.toggle_report(report) do
reports = Reports.list_reports(socket.assigns.organization_id)
{:noreply, assign(socket, reports: reports)}
else
nil -> {:noreply, put_flash(socket, :error, "Report not found")}
{:error, _} -> {:noreply, put_flash(socket, :error, "Failed to toggle report")}
end
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
with %_{} = report <- fetch_org_report(socket, id),
{:ok, _} <- Reports.delete_report(report) do
reports = Reports.list_reports(socket.assigns.organization_id)
{:noreply,
socket
|> put_flash(:info, "Report deleted")
|> assign(:reports, reports)}
else
nil -> {:noreply, put_flash(socket, :error, "Report not found")}
{:error, _} -> {:noreply, put_flash(socket, :error, "Failed to delete report")}
end
end
@impl true
def handle_event("run_now", %{"id" => id}, socket) do
case fetch_org_report(socket, id) do
nil ->
{:noreply, put_flash(socket, :error, "Report not found")}
report ->
case %{"report_id" => report.id}
|> ReportWorker.new()
|> Oban.insert() do
{:ok, _} -> {:noreply, put_flash(socket, :info, "Report queued for delivery")}
{:error, _} -> {:noreply, put_flash(socket, :error, "Failed to queue report")}
end
end
end
# Scoped fetch that returns nil if the report belongs to a different
# organization, preventing IDOR on toggle/delete/run_now.
defp fetch_org_report(socket, id) do
Reports.get_organization_report(id, socket.assigns.organization_id)
end
# -- Helpers --
@doc false
def humanize_type("uptime_summary"), do: "Uptime Summary"
def humanize_type("alert_history"), do: "Alert History"
def humanize_type("capacity_trends"), do: "Capacity Trends"
def humanize_type("rf_link_health"), do: "RF Link Health"
def humanize_type(type), do: type
@doc false
def humanize_schedule(%{"type" => type}), do: String.capitalize(type)
def humanize_schedule(_), do: "-"
@doc false
def format_recipients(recipients) when is_list(recipients), do: Enum.join(recipients, ", ")
def format_recipients(_), do: "-"
@doc false
def status_badge("success"), do: {"Success", "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300"}
def status_badge("failed"),
do: {"Failed", "bg-sweet-salmon-100 text-sweet-salmon-700 dark:bg-sweet-salmon-900/50 dark:text-sweet-salmon-300"}
def status_badge("partial_failure"),
do: {"Partial", "bg-wheat-100 text-wheat-700 dark:bg-wheat-900/50 dark:text-wheat-300"}
def status_badge(_),
do: {"Never Run", "bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300"}
end