defmodule MicrowavepropWeb.Admin.ContactEditLive do
@moduledoc "Admin-only contact editor at `/admin/contacts/:id/edit` for manual QSO fixes."
use MicrowavepropWeb, :live_view
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Radio.ContactEdit
alias Microwaveprop.Radio
alias Microwaveprop.Radio.EditNotifier
def table_options do
%{exports: %{formats: [:csv]}}
end
def fields do
[
id: %{label: "ID", hidden: true},
contact_id: %{label: "Contact", renderer: &contact_cell/2},
user_id: %{label: "Submitted By", renderer: &user_cell/2},
proposed_changes: %{label: "Fields Changed", renderer: &changed_fields_cell/1},
inserted_at: %{label: "Submitted", sortable: true, renderer: &format_ts/1}
]
end
def filters, do: []
def actions do
[
review: fn %{record: edit} ->
assigns = %{edit: edit}
~H"""
"""
end
]
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Review Contact Edits")
|> assign(:reviewing, nil)
|> assign(:admin_note, "")
|> assign(:pending_count, Radio.pending_edit_count())
|> assign(:flagged_contacts, Radio.list_flagged_contacts())
|> assign(:data_provider, {Radio, :pending_edits_query, []})}
end
@impl true
def handle_event("review", %{"id" => id}, socket) do
edit = Radio.get_contact_edit!(id)
{:noreply, assign(socket, reviewing: edit, admin_note: "")}
end
def handle_event("cancel_review", _params, socket) do
{:noreply, assign(socket, reviewing: nil, admin_note: "")}
end
def handle_event("update_note", %{"note" => note}, socket) do
{:noreply, assign(socket, admin_note: note)}
end
def handle_event("approve", _params, socket) do
edit = socket.assigns.reviewing
admin = socket.assigns.current_scope.user
note = normalize_note(socket.assigns.admin_note)
case Radio.approve_edit(edit, admin, note) do
{:ok, approved} ->
_ = EditNotifier.deliver_edit_approved(approved)
{:noreply,
socket
|> assign(reviewing: nil, admin_note: "", pending_count: Radio.pending_edit_count())
|> put_flash(:info, "Edit approved and applied.")
|> push_patch(to: "/" <> (socket.assigns[:current_path] || "admin/contact-edits"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to approve edit.")}
end
end
def handle_event("reject", _params, socket) do
edit = socket.assigns.reviewing
admin = socket.assigns.current_scope.user
note = normalize_note(socket.assigns.admin_note)
case Radio.reject_edit(edit, admin, note) do
{:ok, rejected} ->
_ = EditNotifier.deliver_edit_rejected(rejected)
{:noreply,
socket
|> assign(reviewing: nil, admin_note: "", pending_count: Radio.pending_edit_count())
|> put_flash(:info, "Edit rejected.")
|> push_patch(to: "/" <> (socket.assigns[:current_path] || "admin/contact-edits"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to reject edit.")}
end
end
defp normalize_note(""), do: nil
defp normalize_note(note), do: String.trim(note)
defp format_band(nil), do: "?"
defp format_band(band) do
mhz = Decimal.to_integer(band)
if mhz >= 1000, do: "#{div(mhz, 1000)} GHz", else: "#{mhz} MHz"
end
defp format_ts(nil), do: "?"
defp format_ts(ts), do: Calendar.strftime(ts, "%Y-%m-%d %H:%M UTC")
defp changed_fields_summary(proposed_changes) do
proposed_changes
|> Map.keys()
|> Enum.sort()
|> Enum.join(", ")
end
defp field_label("station1"), do: "Station 1"
defp field_label("station2"), do: "Station 2"
defp field_label("grid1"), do: "Grid 1"
defp field_label("grid2"), do: "Grid 2"
defp field_label("band"), do: "Band"
defp field_label("mode"), do: "Mode"
defp field_label("qso_timestamp"), do: "Timestamp"
defp field_label(other), do: other
defp current_value(contact, "station1"), do: contact.station1
defp current_value(contact, "station2"), do: contact.station2
defp current_value(contact, "grid1"), do: contact.grid1
defp current_value(contact, "grid2"), do: contact.grid2
defp current_value(contact, "band"), do: if(contact.band, do: Decimal.to_string(contact.band))
defp current_value(contact, "mode"), do: contact.mode
defp current_value(contact, "qso_timestamp"), do: if(contact.qso_timestamp, do: format_ts(contact.qso_timestamp))
defp current_value(_contact, _), do: "?"
defp format_proposed("band", val), do: to_string(val)
defp format_proposed("qso_timestamp", val) when is_binary(val), do: val
defp format_proposed(_field, val), do: to_string(val)
defp contact_cell(_value, %{contact: contact}) when not is_nil(contact) do
assigns = %{contact: contact}
~H"""
<.link navigate={~p"/contacts/#{@contact.id}"} class="link link-primary">
{@contact.station1} / {@contact.station2}
{format_band(@contact.band)}
"""
end
defp contact_cell(_value, _record), do: "—"
defp user_cell(_value, %{user: %{callsign: callsign}}), do: callsign
defp user_cell(_value, _record), do: "—"
defp changed_fields_cell(changes) when is_map(changes), do: changed_fields_summary(changes)
defp changed_fields_cell(_), do: "—"
@impl true
def render(assigns) do
~H"""
<.header>
Review Contact Edits
<:subtitle>{@pending_count} pending
<:actions>
<.link navigate={~p"/status"} class="btn btn-sm btn-ghost">
<.icon name="hero-arrow-left" class="w-4 h-4" /> Status
<%= if @reviewing do %>
Review Edit from {@reviewing.user.callsign}
Contact:
<.link navigate={~p"/contacts/#{@reviewing.contact.id}"} class="link link-primary">
{@reviewing.contact.station1} / {@reviewing.contact.station2}
· {format_band(@reviewing.contact.band)} · {format_ts(
@reviewing.contact.qso_timestamp
)}
| Field |
Current |
Proposed |
<%= for {field, proposed_val} <- Enum.sort(@reviewing.proposed_changes) do %>
| {field_label(field)} |
{current_value(@reviewing.contact, field)} |
{format_proposed(field, proposed_val)} |
<% end %>
<% end %>
<.live_table
fields={fields()}
filters={filters()}
options={@options}
streams={@streams}
actions={actions()}
/>
<%= if @flagged_contacts != [] do %>
Flagged contacts ({length(@flagged_contacts)})
| Stations |
Band |
QSO (UTC) |
Added (UTC) |
|
<%= for c <- @flagged_contacts do %>
| {c.station1} / {c.station2} |
{format_band(c.band)} |
{format_ts(c.qso_timestamp)} |
{format_ts(c.inserted_at)} |
<.link navigate={~p"/contacts/#{c.id}"} class="btn btn-xs btn-ghost">
View
|
<% end %>
<% end %>
"""
end
end