Add nonzero balance filter to users page, link from dashboard card

This commit is contained in:
2026-05-08 14:24:31 +05:00
parent 2dfc42821c
commit 5547b427bb
3 changed files with 46 additions and 10 deletions
+2 -2
View File
@@ -170,7 +170,7 @@ export default async function AdminDashboard() {
</div> </div>
</div> </div>
<div className="card-aubade p-5"> <Link href="/admin/users?balance=nonzero" className="card-aubade p-5 block">
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--muted-foreground)" }}> <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--muted-foreground)" }}>
На балансах На балансах
</p> </p>
@@ -178,7 +178,7 @@ export default async function AdminDashboard() {
{totalBalance.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {totalBalance.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p> </p>
<p className="text-xs mt-0.5" style={{ color: "var(--muted-foreground)" }}>сумма по всем пользователям</p> <p className="text-xs mt-0.5" style={{ color: "var(--muted-foreground)" }}>сумма по всем пользователям</p>
</div> </Link>
</div> </div>
</div> </div>
</div> </div>
+16 -3
View File
@@ -8,14 +8,25 @@ import { UsersSearch } from "@/components/admin/users-search";
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
interface Props { interface Props {
searchParams: Promise<{ search?: string; role?: string; page?: string }>; searchParams: Promise<{ search?: string; role?: string; page?: string; balance?: string }>;
} }
export default async function UsersPage({ searchParams }: Props) { export default async function UsersPage({ searchParams }: Props) {
const { search = "", role = "", page = "1" } = await searchParams; const { search = "", role = "", page = "1", balance = "" } = await searchParams;
const currentPage = Math.max(1, parseInt(page) || 1); const currentPage = Math.max(1, parseInt(page) || 1);
const skip = (currentPage - 1) * PAGE_SIZE; const skip = (currentPage - 1) * PAGE_SIZE;
// Collect userIds with non-zero balance if filter is active
let balanceUserIds: string[] | null = null;
if (balance === "nonzero") {
const groups = await prisma.balanceTransaction.groupBy({
by: ["userId"],
_sum: { amount: true },
having: { amount: { _sum: { not: { equals: 0 } } } },
});
balanceUserIds = groups.map((g) => g.userId);
}
const where = { const where = {
...(search ...(search
? { ? {
@@ -26,6 +37,7 @@ export default async function UsersPage({ searchParams }: Props) {
} }
: {}), : {}),
...(role ? { role } : {}), ...(role ? { role } : {}),
...(balanceUserIds !== null ? { id: { in: balanceUserIds } } : {}),
}; };
const [users, total] = await Promise.all([ const [users, total] = await Promise.all([
@@ -66,6 +78,7 @@ export default async function UsersPage({ searchParams }: Props) {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (search) params.set("search", search); if (search) params.set("search", search);
if (role) params.set("role", role); if (role) params.set("role", role);
if (balance) params.set("balance", balance);
params.set("page", String(p)); params.set("page", String(p));
return `/admin/users?${params.toString()}`; return `/admin/users?${params.toString()}`;
} }
@@ -88,7 +101,7 @@ export default async function UsersPage({ searchParams }: Props) {
{/* Filters */} {/* Filters */}
<Suspense> <Suspense>
<UsersSearch initialSearch={search} initialRole={role} /> <UsersSearch initialSearch={search} initialRole={role} initialBalance={balance} />
</Suspense> </Suspense>
<UsersTable users={tableUsers} /> <UsersTable users={tableUsers} />
+28 -5
View File
@@ -13,15 +13,24 @@ const inputStyle: React.CSSProperties = {
fontFamily: "inherit", fontFamily: "inherit",
}; };
export function UsersSearch({ initialSearch, initialRole }: { initialSearch: string; initialRole: string }) { export function UsersSearch({
initialSearch,
initialRole,
initialBalance,
}: {
initialSearch: string;
initialRole: string;
initialBalance: string;
}) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
function update(search: string, role: string) { function update(search: string, role: string, balance: string) {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (search) params.set("search", search); if (search) params.set("search", search);
if (role) params.set("role", role); if (role) params.set("role", role);
if (balance) params.set("balance", balance);
startTransition(() => router.push(`${pathname}?${params.toString()}`)); startTransition(() => router.push(`${pathname}?${params.toString()}`));
} }
@@ -36,7 +45,7 @@ export function UsersSearch({ initialSearch, initialRole }: { initialSearch: str
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => { onBlur={(e) => {
e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.borderColor = "var(--border)";
update(e.currentTarget.value.trim(), initialRole); update(e.currentTarget.value.trim(), initialRole, initialBalance);
}} }}
onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }} onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }}
/> />
@@ -44,7 +53,7 @@ export function UsersSearch({ initialSearch, initialRole }: { initialSearch: str
<select <select
defaultValue={initialRole} defaultValue={initialRole}
onChange={(e) => update(initialSearch, e.target.value)} onChange={(e) => update(initialSearch, e.target.value, initialBalance)}
style={{ ...inputStyle, appearance: "none", cursor: "pointer" }} style={{ ...inputStyle, appearance: "none", cursor: "pointer" }}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
@@ -55,7 +64,21 @@ export function UsersSearch({ initialSearch, initialRole }: { initialSearch: str
<option value="admin">Администраторы</option> <option value="admin">Администраторы</option>
</select> </select>
{(initialSearch || initialRole) && ( <button
type="button"
onClick={() => update(initialSearch, initialRole, initialBalance === "nonzero" ? "" : "nonzero")}
className="text-xs px-3"
style={{
border: "2px solid var(--border)",
background: initialBalance === "nonzero" ? "var(--foreground)" : "transparent",
color: initialBalance === "nonzero" ? "var(--background)" : "var(--muted-foreground)",
cursor: "pointer",
}}
>
С балансом
</button>
{(initialSearch || initialRole || initialBalance) && (
<button <button
type="button" type="button"
onClick={() => startTransition(() => router.push(pathname))} onClick={() => startTransition(() => router.push(pathname))}