"Contacts callsign is in" section on /u/:callsign

Add a new card on the profile page listing every contact where the
callsign appears as either station1 or station2, case-insensitive,
newest first. Backed by a new `Radio.list_contacts_involving_callsign/1`
query with unit coverage for station1/station2 matching, case handling,
ordering, and blank input.
This commit is contained in:
Graham McIntire 2026-04-12 16:41:27 -05:00
parent 3a1da13eab
commit 1013df6858
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 113 additions and 2 deletions

View file

@ -128,6 +128,22 @@ defmodule Microwaveprop.Radio do
|> Repo.all()
end
@doc """
Returns all contacts where the given callsign appears as either
`station1` or `station2`, newest first. Matching is case-insensitive.
"""
@spec list_contacts_involving_callsign(String.t() | nil) :: [Contact.t()]
def list_contacts_involving_callsign(callsign) when callsign in [nil, ""], do: []
def list_contacts_involving_callsign(callsign) when is_binary(callsign) do
upcased = String.upcase(callsign)
Contact
|> where([c], fragment("upper(?)", c.station1) == ^upcased or fragment("upper(?)", c.station2) == ^upcased)
|> order_by([c], desc: c.qso_timestamp, desc: c.id)
|> Repo.all()
end
@spec list_contacts(keyword()) :: contact_page()
def list_contacts(opts \\ []) do
page = max(Keyword.get(opts, :page, 1), 1)

View file

@ -19,13 +19,15 @@ defmodule MicrowavepropWeb.UserProfileLive do
user ->
contacts = Radio.list_contacts_for_user(user)
beacons = Beacons.list_beacons_for_user(user)
involving = Radio.list_contacts_involving_callsign(user.callsign)
{:ok,
assign(socket,
page_title: user.callsign,
profile: user,
contacts: contacts,
beacons: beacons
beacons: beacons,
involving: involving
)}
end
end
@ -106,7 +108,7 @@ defmodule MicrowavepropWeb.UserProfileLive do
</div>
</div>
<div class="card bg-base-200 shadow-sm">
<div class="card bg-base-200 shadow-sm mb-6">
<div class="card-body">
<h2 class="card-title">Beacons</h2>
<%= if @beacons == [] do %>
@ -150,6 +152,44 @@ defmodule MicrowavepropWeb.UserProfileLive do
<% end %>
</div>
</div>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h2 class="card-title">Contacts {@profile.callsign} is in</h2>
<%= if @involving == [] do %>
<p class="text-sm opacity-70">No contacts on file with this callsign yet.</p>
<% else %>
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead>
<tr>
<th>When</th>
<th>Stations</th>
<th>Band</th>
<th>Mode</th>
<th>Distance</th>
</tr>
</thead>
<tbody>
<tr :for={contact <- @involving} class="hover">
<td class="whitespace-nowrap">
{Calendar.strftime(contact.qso_timestamp, "%Y-%m-%d %H:%M UTC")}
</td>
<td>
<.link navigate={~p"/contacts/#{contact.id}"} class="link link-hover font-mono">
{contact.station1} &harr; {contact.station2}
</.link>
</td>
<td>{contact.band && "#{contact.band} MHz"}</td>
<td>{contact.mode}</td>
<td>{contact.distance_km && "#{contact.distance_km} km"}</td>
</tr>
</tbody>
</table>
</div>
<% end %>
</div>
</div>
</Layouts.app>
"""
end

View file

@ -498,4 +498,59 @@ defmodule Microwaveprop.RadioTest do
assert Radio.list_contacts_for_user(user) == []
end
end
describe "list_contacts_involving_callsign/1" do
test "returns contacts where the callsign is station1" do
match = create_contact(%{station1: "W5ISP", station2: "K5OTR"})
_other = create_contact(%{station1: "W5OTH", station2: "K5OTR"})
assert [found] = Radio.list_contacts_involving_callsign("W5ISP")
assert found.id == match.id
end
test "returns contacts where the callsign is station2" do
match = create_contact(%{station1: "W5OTH", station2: "W5ISP"})
_other = create_contact(%{station1: "W5OTH", station2: "K5OTR"})
assert [found] = Radio.list_contacts_involving_callsign("W5ISP")
assert found.id == match.id
end
test "matching is case-insensitive" do
match = create_contact(%{station1: "W5ISP", station2: "K5OTR"})
assert [found] = Radio.list_contacts_involving_callsign("w5isp")
assert found.id == match.id
end
test "orders newest first" do
_old =
create_contact(%{
station1: "W5ISP",
station2: "K5A",
qso_timestamp: ~U[2020-01-01 00:00:00Z]
})
new =
create_contact(%{
station1: "K5B",
station2: "W5ISP",
qso_timestamp: ~U[2026-06-15 12:00:00Z]
})
assert [first, _] = Radio.list_contacts_involving_callsign("W5ISP")
assert first.id == new.id
end
test "returns empty list when no contacts match" do
create_contact(%{station1: "W5OTH", station2: "K5OTR"})
assert Radio.list_contacts_involving_callsign("W5ISP") == []
end
test "returns empty list for blank or nil callsign" do
assert Radio.list_contacts_involving_callsign("") == []
assert Radio.list_contacts_involving_callsign(nil) == []
end
end
end