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:
parent
0f3e87b67a
commit
87e795c837
2 changed files with 18 additions and 5 deletions
|
|
@ -7,6 +7,7 @@ interface User {
|
|||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
phone?: string | null;
|
||||
familyId: string;
|
||||
familyName: string;
|
||||
createdAt: string;
|
||||
|
|
@ -122,13 +123,17 @@ export default function AdminUsers() {
|
|||
|
||||
const filteredUsers = users.filter((u) =>
|
||||
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 headers = ["Email", "Name", "Family", "Created"];
|
||||
const rows = filteredUsers.map((u) => [u.email, u.name, u.familyName, u.createdAt]);
|
||||
const csv = [headers, ...rows].map((row) => row.join(",")).join("\n");
|
||||
const headers = ["Email", "Name", "Phone", "Family", "Created"];
|
||||
const rows = filteredUsers.map((u) => [u.email, u.name, u.phone || "", u.familyName, u.createdAt]);
|
||||
// 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 url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
|
|
@ -206,7 +211,7 @@ export default function AdminUsers() {
|
|||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by name or email…"
|
||||
placeholder="Search by name, email or phone…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
|
@ -216,6 +221,7 @@ export default function AdminUsers() {
|
|||
<thead className="bg-gray-700">
|
||||
<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">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">Password</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="text-xs text-gray-500">{user.email}</div>
|
||||
</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">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export async function GET(request: Request) {
|
|||
u.id,
|
||||
u.email,
|
||||
u.name,
|
||||
u.phone,
|
||||
u.password_hash,
|
||||
fm.family_id,
|
||||
fm.id as member_id,
|
||||
|
|
@ -32,6 +33,7 @@ export async function GET(request: Request) {
|
|||
id: u.id,
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
phone: u.phone || null,
|
||||
hasPassword: !!u.password_hash,
|
||||
familyId: u.family_id,
|
||||
memberId: u.member_id,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue