feat: show user phone in admin users page

Answer to "is phone visible in admin?": it wasn't — added it.

- /api/admin/users: SELECT u.phone, return phone in the DTO
- admin users page: new Phone column (tap-to-call tel: link, "—" when empty),
  searchable by phone, included in CSV export (now properly quoted so
  commas/empty cells don't shift columns)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-06-02 21:48:57 +05:30
parent 0f3e87b67a
commit 87e795c837
2 changed files with 18 additions and 5 deletions

View file

@ -7,6 +7,7 @@ interface User {
id: string; id: string;
email: string; email: string;
name: string; name: string;
phone?: string | null;
familyId: string; familyId: string;
familyName: string; familyName: string;
createdAt: string; createdAt: string;
@ -122,13 +123,17 @@ export default function AdminUsers() {
const filteredUsers = users.filter((u) => const filteredUsers = users.filter((u) =>
u.email.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase()) ||
(u.name || "").toLowerCase().includes(search.toLowerCase()) (u.name || "").toLowerCase().includes(search.toLowerCase()) ||
(u.phone || "").includes(search)
); );
const exportCSV = () => { const exportCSV = () => {
const headers = ["Email", "Name", "Family", "Created"]; const headers = ["Email", "Name", "Phone", "Family", "Created"];
const rows = filteredUsers.map((u) => [u.email, u.name, u.familyName, u.createdAt]); const rows = filteredUsers.map((u) => [u.email, u.name, u.phone || "", u.familyName, u.createdAt]);
const csv = [headers, ...rows].map((row) => row.join(",")).join("\n"); // Quote each cell so commas/empties don't shift columns
const csv = [headers, ...rows]
.map((row) => row.map((cell) => `"${String(cell ?? "").replace(/"/g, '""')}"`).join(","))
.join("\n");
const blob = new Blob([csv], { type: "text/csv" }); const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
@ -206,7 +211,7 @@ export default function AdminUsers() {
<Input <Input
type="text" type="text"
placeholder="Search by name or email…" placeholder="Search by name, email or phone…"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
@ -216,6 +221,7 @@ export default function AdminUsers() {
<thead className="bg-gray-700"> <thead className="bg-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-sm font-medium">User</th> <th className="px-4 py-3 text-left text-sm font-medium">User</th>
<th className="px-4 py-3 text-left text-sm font-medium">Phone</th>
<th className="px-4 py-3 text-left text-sm font-medium">Family</th> <th className="px-4 py-3 text-left text-sm font-medium">Family</th>
<th className="px-4 py-3 text-left text-sm font-medium">Password</th> <th className="px-4 py-3 text-left text-sm font-medium">Password</th>
<th className="px-4 py-3 text-left text-sm font-medium">Joined</th> <th className="px-4 py-3 text-left text-sm font-medium">Joined</th>
@ -229,6 +235,11 @@ export default function AdminUsers() {
<div className="font-medium">{user.name || user.email}</div> <div className="font-medium">{user.name || user.email}</div>
<div className="text-xs text-gray-500">{user.email}</div> <div className="text-xs text-gray-500">{user.email}</div>
</td> </td>
<td className="px-4 py-3 text-sm">
{user.phone
? <a href={`tel:${user.phone}`} className="text-gray-300 hover:text-rose-400">{user.phone}</a>
: <span className="text-gray-600"></span>}
</td>
<td className="px-4 py-3 text-gray-300">{user.familyName || <span className="text-gray-600"></span>}</td> <td className="px-4 py-3 text-gray-300">{user.familyName || <span className="text-gray-600"></span>}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<button <button

View file

@ -16,6 +16,7 @@ export async function GET(request: Request) {
u.id, u.id,
u.email, u.email,
u.name, u.name,
u.phone,
u.password_hash, u.password_hash,
fm.family_id, fm.family_id,
fm.id as member_id, fm.id as member_id,
@ -32,6 +33,7 @@ export async function GET(request: Request) {
id: u.id, id: u.id,
email: u.email, email: u.email,
name: u.name, name: u.name,
phone: u.phone || null,
hasPassword: !!u.password_hash, hasPassword: !!u.password_hash,
familyId: u.family_id, familyId: u.family_id,
memberId: u.member_id, memberId: u.member_id,