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;
|
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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue