feat(admin): add admin payment tables + filters
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Has been cancelled

This commit is contained in:
2025-07-23 23:12:10 +05:00
parent dc3b5f9bf9
commit c34285c66d
4 changed files with 225 additions and 227 deletions

View File

@ -29,6 +29,7 @@ This is a web portal for SAR Link customers.
## Admin Controls ## Admin Controls
### Users ### Users
- [x] Show users table - [x] Show users table
- [ ] handle verify api no response case
- [ ] Add all relavant filters for users table - [ ] Add all relavant filters for users table
- [x] Verify or reject users with a custom message - [x] Verify or reject users with a custom message
- [ ] Add functionality to send custom sms to users in user:id page - [ ] Add functionality to send custom sms to users in user:id page
@ -40,6 +41,6 @@ This is a web portal for SAR Link customers.
- [x] Add all relevant filters for user devices table - [x] Add all relevant filters for user devices table
### User Payments ### User Payments
- [ ] Show user payments table - [x] Show user payments table
- [ ] Add relevant filters for user payments table - [x] Add relevant filters for user payments table
- [ ] Add computation of total value for filtered results - [ ] Add computation of total value for filtered results

View File

@ -93,7 +93,7 @@ type GetPaymentProps = {
[key: string]: string | number | undefined; // Allow additional properties for flexibility [key: string]: string | number | undefined; // Allow additional properties for flexibility
}; };
export async function getPayments(params: GetPaymentProps) { export async function getPayments(params: GetPaymentProps, allPayments = false) {
// Build query string from all defined params // Build query string from all defined params
const query = Object.entries(params) const query = Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== "") .filter(([_, value]) => value !== undefined && value !== "")
@ -101,7 +101,7 @@ export async function getPayments(params: GetPaymentProps) {
.join("&"); .join("&");
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const response = await fetch( const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/payment/?${query}`, `${process.env.SARLINK_API_BASE_URL}/api/billing/payment/?${query}&all_payments=${allPayments}`,
{ {
method: "GET", method: "GET",
headers: { headers: {

View File

@ -1,5 +1,6 @@
import { Suspense } from "react"; import { Suspense } from "react";
import { UsersPaymentsTable } from "@/components/admin/user-payments-table"; import { UsersPaymentsTable } from "@/components/admin/user-payments-table";
import DynamicFilter from "@/components/generic-filter";
export default async function UserPayments({ export default async function UserPayments({
searchParams, searchParams,
@ -18,6 +19,62 @@ export default async function UserPayments({
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">User Payments</h3> <h3 className="text-sarLinkOrange text-2xl">User Payments</h3>
</div> </div>
<DynamicFilter
description="Filter user payments by name, MAC address, or vendor."
title="User Payments Filter"
inputs={[
{
name: "user",
label: "User",
type: "string",
placeholder: "Enter user name",
},
{
name: "mib_reference",
label: "MIB Reference",
type: "string",
placeholder: "Enter MIB Reference",
},
{
type: "dual-range-slider",
label: "Amount Range",
name: "amount",
max: 1200,
min: 0,
step: 10,
},
{
type: "dual-range-slider",
label: "Duration Range",
name: "number_of_months",
max: 12,
min: 1,
step: 1,
},
{
type: "radio-group",
label: "Payment Status",
name: "status",
options: [
{ label: "All", value: "" },
{ label: "Pending", value: "PENDING" },
{ label: "Paid", value: "PAID" },
{ label: "Cancelled", value: "CANCELLED" },
],
},
{
type: "radio-group",
label: "Payment Method",
name: "method",
options: [
{ label: "All", value: "" },
{ label: "Wallet", value: "WALLET" },
{ label: "Transfer", value: "TRANSFER" },
]
},
]}
/>
<Suspense key={query} fallback={"loading...."}> <Suspense key={query} fallback={"loading...."}>
<UsersPaymentsTable searchParams={searchParams} /> <UsersPaymentsTable searchParams={searchParams} />
</Suspense> </Suspense>

View File

@ -1,236 +1,176 @@
// import Pagination from "@/components/pagination"; import Link from "next/link";
// import { Badge } from "@/components/ui/badge"; import { redirect } from "next/navigation";
// import { Button } from "@/components/ui/button"; import { getServerSession } from "next-auth";
// import { import { getPayments } from "@/actions/payment";
// Table, import { authOptions } from "@/app/auth";
// TableBody, import Pagination from "@/components/pagination";
// TableCaption, import { Badge } from "@/components/ui/badge";
// TableCell, import { Button } from "@/components/ui/button";
// TableFooter, import {
// TableHead, Table,
// TableHeader, TableBody,
// TableRow, TableCaption,
// } from "@/components/ui/table"; TableCell,
// import Link from "next/link"; TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { tryCatch } from "@/utils/tryCatch";
import ClientErrorMessage from "../client-error-message";
export async function UsersPaymentsTable({ export async function UsersPaymentsTable({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
query: string; [key: string]: unknown;
page: number;
sortBy: string;
status: string;
}>; }>;
}) { }) {
const query = (await searchParams)?.query || ""; const resolvedParams = await searchParams;
console.log(query);
// const page = (await searchParams)?.page;
// const sortBy = (await searchParams)?.sortBy || "asc";
// const totalPayments = await prisma.payment.count({
// where: {
// OR: [
// {
// user: {
// name: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// phoneNumber: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// address: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// {
// user: {
// id_card: {
// contains: query || "",
// mode: "insensitive",
// }
// },
// },
// ],
// },
// });
// const totalPages = Math.ceil(totalPayments / 10); const page = Number.parseInt(resolvedParams.page as string) || 1;
// const limit = 10; const limit = 10;
// const offset = (Number(page) - 1) * limit || 0; const offset = (page - 1) * limit;
// const payments = await prisma.payment.findMany({ // Build params object for getDevices
// where: { const apiParams: Record<string, string | number | undefined> = {};
// OR: [ for (const [key, value] of Object.entries(resolvedParams)) {
// { if (value !== undefined && value !== "") {
// user: { apiParams[key] = typeof value === "number" ? value : String(value);
// name: { }
// contains: query || "", }
// mode: "insensitive", apiParams.limit = limit;
// } apiParams.offset = offset;
// },
// }, const [error, payments] = await tryCatch(getPayments(apiParams, true));
// { if (error) {
// user: { if (error.message === "UNAUTHORIZED") {
// phoneNumber: { redirect("/auth/signin");
// contains: query || "", } else {
// mode: "insensitive", return <ClientErrorMessage message={error.message} />;
// } }
// }, }
// }, const { meta, data } = payments;
// {
// user: { // return <pre>{JSON.stringify(payments, null, 2)}</pre>;
// address: { return (
// contains: query || "", <div>
// mode: "insensitive", {data.length === 0 ? (
// } <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
// }, <h3>No user payments yet.</h3>
// }, </div>
// { ) : (
// user: { <>
// id_card: { <Table className="overflow-scroll">
// contains: query || "", <TableCaption>Table of all users.</TableCaption>
// mode: "insensitive", <TableHeader>
// } <TableRow>
// }, <TableHead>Devices paid</TableHead>
// }, <TableHead>User</TableHead>
// ], <TableHead>Amount</TableHead>
// }, <TableHead>Duration</TableHead>
// include: { <TableHead>Payment Status</TableHead>
// user: true, <TableHead>Payment Method</TableHead>
// devices: true, <TableHead>MIB Reference</TableHead>
// }, <TableHead>Paid At</TableHead>
// skip: offset, <TableHead>Action</TableHead>
// take: limit, </TableRow>
// orderBy: { </TableHeader>
// id: `${sortBy}` as "asc" | "desc", <TableBody className="overflow-scroll">
// }, {data.map((payment) => (
// }); <TableRow
className={`${payment.paid && "title-bg dark:bg-black"}`}
// const users = await prisma.user.findMany({ key={payment.id}
// where: { >
// role: "USER", <TableCell className="font-medium">
// }, <ol className="list-disc list-inside text-sm">
// include: { {payment.devices.map((device) => (
// atoll: true, <li
// island: true, key={device.id}
// }, className="text-sm text-muted-foreground"
// }); >
return null; {device.name}
// return ( </li>
// <div> ))}
// {payments.length === 0 ? ( </ol>
// <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> </TableCell>
// <h3>No user payments yet.</h3> <TableCell className="font-medium">
// </div> {/* {payment.user.id_card} */}
// ) : ( <div className="flex flex-col items-start">
// <> {payment.devices[0]?.user?.name}
// <Table className="overflow-scroll"> <span className="text-muted-foreground">
// <TableCaption>Table of all users.</TableCaption> {payment.devices[0]?.user?.id_card}
// <TableHeader> </span>
// <TableRow> </div>{" "}
// <TableHead>Devices paid</TableHead> </TableCell>
// <TableHead>User</TableHead> <TableCell>{payment.amount}</TableCell>
// <TableHead>Amount</TableHead> <TableCell>{payment.number_of_months} Months</TableCell>
// <TableHead>Duration</TableHead>
// <TableHead>Payment Status</TableHead> <TableCell>
// <TableHead>Payment Method</TableHead> {payment.status === "PENDING" ? (
// <TableHead>Paid At</TableHead> <Badge
// <TableHead>Action</TableHead> variant="outline"
// </TableRow> className="bg-yellow-100 text-black"
// </TableHeader> >
// <TableBody className="overflow-scroll"> {payment.status}
// {payments.map((payment) => ( </Badge>
// <TableRow ) : payment.status === "PAID" ? (
// className={`${payment.paid && "title-bg dark:bg-black"}`} <Badge
// key={payment.id} variant="outline"
// > className="bg-lime-100 text-black"
// <TableCell className="font-medium"> >
// <ol className="list-disc list-inside text-sm"> {payment.status}
// {payment.devices.map((device) => ( </Badge>
// <li ) : (
// key={device.id} <Badge
// className="text-sm text-muted-foreground" variant="outline"
// > className="bg-red-100 text-black"
// {device.name} >
// </li> {payment.status}
// ))} </Badge>
// </ol> )}
// </TableCell> </TableCell>
// <TableCell className="font-medium"> <TableCell>{payment.method}</TableCell>
// {payment.user.id_card} <TableCell>{payment.mib_reference}</TableCell>
// </TableCell> <TableCell>
// <TableCell>{payment.user?.name}</TableCell> {new Date(payment.paid_at ?? "").toLocaleDateString(
// <TableCell>{payment.user?.name}</TableCell> "en-US",
// <TableCell>{payment.id}</TableCell> {
month: "short",
// <TableCell> day: "2-digit",
// {payment.paid ? ( year: "numeric",
// <Badge minute: "2-digit",
// variant="outline" hour: "2-digit",
// className="bg-lime-100 text-black" },
// > )}
// Verified </TableCell>
// </Badge>
// ) : ( <TableCell>
// <Badge <Link href={`/payments/${payment.id}/verify`}>
// variant="outline" <Button>Details</Button>
// className="bg-yellow-100 text-black" </Link>
// > </TableCell>
// Unverified </TableRow>
// </Badge> ))}
// )} </TableBody>
// </TableCell> <TableFooter>
// <TableCell> <TableRow>
// {new Date(payment.paidAt ?? "").toLocaleDateString( <TableCell colSpan={10} className="text-muted-foreground">
// "en-US", {meta?.total === 1 ? (
// { <p className="text-center">Total {meta?.total} payment.</p>
// month: "short", ) : (
// day: "2-digit", <p className="text-center">Total {meta?.total} payments.</p>
// year: "numeric", )}{" "}
// }, </TableCell>
// )} </TableRow>
// </TableCell> </TableFooter>
</Table>
// <TableCell>{payment.id}</TableCell> <Pagination
// <TableCell> totalPages={meta?.last_page}
// <Link href={`/payments/${payment.id}/verify`}> currentPage={meta?.current_page}
// <Button>Details</Button> />
// </Link> </>
// </TableCell> )}
// </TableRow> </div>
// ))} );
// </TableBody>
// <TableFooter>
// <TableRow>
// <TableCell colSpan={8}>
// {query.length > 0 && (
// <p className="text-sm text-muted-foreground">
// Showing {payments.length} locations for &quot;{query}
// &quot;
// </p>
// )}
// </TableCell>
// <TableCell className="text-muted-foreground">
// {totalPayments} payments
// </TableCell>
// </TableRow>
// </TableFooter>
// </Table>
// <Pagination totalPages={totalPages} currentPage={page} />
// </>
// )}
// </div>
// );
} }