chore: add client/src/pages/admin/customers.tsx
This commit is contained in:
parent
c3b09e39ea
commit
3f899d558d
1 changed files with 91 additions and 0 deletions
91
client/src/pages/admin/customers.tsx
Normal file
91
client/src/pages/admin/customers.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { AdminNav } from "./dashboard";
|
||||||
|
import { Search, Mail } from "lucide-react";
|
||||||
|
|
||||||
|
interface Customer {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
phone?: string;
|
||||||
|
storeCredit: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminCustomers() {
|
||||||
|
const [q, setQ] = useState("");
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery<Customer[]>({
|
||||||
|
queryKey: ["/api/admin/customers", { q: search, page }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams({ page: String(page), limit: "30" });
|
||||||
|
if (search) params.set("q", search);
|
||||||
|
const r = await fetch(`/api/admin/customers?${params}`, { credentials: "include" });
|
||||||
|
if (r.status === 401) { window.location.href = "/admin/login"; return []; }
|
||||||
|
return r.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden" data-testid="admin-customers">
|
||||||
|
<AdminNav active="/admin/customers" />
|
||||||
|
<main className="flex-1 overflow-y-auto p-8">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-2xl font-bold">Customers</h1>
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); setSearch(q); setPage(1); }} className="flex gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search customers..."
|
||||||
|
className="pl-8 pr-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none w-56"
|
||||||
|
data-testid="input-customer-search" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="px-3 py-2 bg-primary text-primary-foreground rounded-md text-sm">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-xl overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-border text-xs text-muted-foreground">
|
||||||
|
<th className="text-left px-4 py-3 font-medium">Customer</th>
|
||||||
|
<th className="text-left px-4 py-3 font-medium">Phone</th>
|
||||||
|
<th className="text-right px-4 py-3 font-medium">Store credit</th>
|
||||||
|
<th className="text-right px-4 py-3 font-medium">Member since</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
{isLoading ? (
|
||||||
|
Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<tr key={i}><td colSpan={4} className="px-4 py-3"><div className="h-4 bg-muted rounded animate-pulse" /></td></tr>
|
||||||
|
))
|
||||||
|
) : data?.map((c) => (
|
||||||
|
<tr key={c.id} className="hover:bg-muted/20 transition-colors" data-testid={`customer-row-${c.id}`}>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-7 h-7 rounded-full bg-primary/10 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||||
|
{(c.firstName?.[0] || c.email[0]).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{(c.firstName || c.lastName) && <p className="text-sm font-medium">{[c.firstName, c.lastName].filter(Boolean).join(" ")}</p>}
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center gap-1"><Mail size={10} />{c.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-muted-foreground">{c.phone || "—"}</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm">{parseFloat(c.storeCredit) > 0 ? `$${parseFloat(c.storeCredit).toFixed(2)}` : "—"}</td>
|
||||||
|
<td className="px-4 py-3 text-right text-xs text-muted-foreground">{new Date(c.createdAt).toLocaleDateString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!isLoading && !data?.length && (
|
||||||
|
<tr><td colSpan={4} className="text-center py-12 text-muted-foreground">No customers yet</td></tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue