diff --git a/actions/payment.ts b/actions/payment.ts index 33143e5..1f07ba0 100644 --- a/actions/payment.ts +++ b/actions/payment.ts @@ -24,8 +24,7 @@ export async function createPayment(data: NewPayment) { const session = await getServerSession(authOptions); console.log("data", data); const response = await fetch( - `${ - process.env.SARLINK_API_BASE_URL // }); + `${process.env.SARLINK_API_BASE_URL // }); }/api/billing/payment/`, { method: "POST", diff --git a/app/(dashboard)/wallet/page.tsx b/app/(dashboard)/wallet/page.tsx index 1fb3473..2f4c9ee 100644 --- a/app/(dashboard)/wallet/page.tsx +++ b/app/(dashboard)/wallet/page.tsx @@ -1,11 +1,63 @@ -import React from "react"; +import { Suspense } from "react"; +import DynamicFilter from "@/components/generic-filter"; +import { WalletTransactionsTable } from "@/components/wallet-transactions-table"; + +export default async function Wallet({ + searchParams, +}: { + searchParams: Promise<{ + query: string; + page: number; + sortBy: string; + status: string; + }>; +}) { + const query = (await searchParams)?.query || ""; -export default function UserWallet() { return (

My Wallet

+
+ +
+ + +
); } diff --git a/app/globals.css b/app/globals.css index 49fec38..ea1be82 100644 --- a/app/globals.css +++ b/app/globals.css @@ -76,6 +76,14 @@ } } +.credit-bg { + background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%2340b02f' fill-opacity='0.29' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.debit-bg { + background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23d35c5c' fill-opacity='0.2' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E"); +} + .error-bg { background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23e06f10' fill-opacity='0.35' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E"); } diff --git a/components/ui/app-sidebar.tsx b/components/ui/app-sidebar.tsx index 09acee5..1a62557 100644 --- a/components/ui/app-sidebar.tsx +++ b/components/ui/app-sidebar.tsx @@ -40,17 +40,17 @@ type Categories = { id: string; children: ( | { - title: string; - link: string; - perm_identifier: string; - icon: React.JSX.Element; - } + title: string; + link: string; + perm_identifier: string; + icon: React.JSX.Element; + } | { - title: string; - link: string; - icon: React.JSX.Element; - perm_identifier?: undefined; - } + title: string; + link: string; + icon: React.JSX.Element; + perm_identifier?: undefined; + } )[]; }[]; @@ -96,7 +96,7 @@ export async function AppSidebar({ title: "Wallet", link: "/wallet", icon: , - perm_identifier: "wallet", + perm_identifier: "wallet transaction", }, ], }, diff --git a/components/wallet-transactions-table.tsx b/components/wallet-transactions-table.tsx new file mode 100644 index 0000000..4cf6314 --- /dev/null +++ b/components/wallet-transactions-table.tsx @@ -0,0 +1,225 @@ +import { Calendar } from "lucide-react"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { WalletTransaction } from "@/lib/backend-types"; +import { cn } from "@/lib/utils"; +import { getWaleltTransactions } from "@/queries/wallet"; +import { tryCatch } from "@/utils/tryCatch"; +import Pagination from "./pagination"; +import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; + +export async function WalletTransactionsTable({ + searchParams, +}: { + searchParams: Promise<{ + [key: string]: unknown; + }>; +}) { + const resolvedParams = await searchParams; + const page = Number.parseInt(resolvedParams.page as string) || 1; + const limit = 10; + const offset = (page - 1) * limit; + // Build params object + const apiParams: Record = {}; + for (const [key, value] of Object.entries(resolvedParams)) { + if (value !== undefined && value !== "") { + apiParams[key] = typeof value === "number" ? value : String(value); + } + } + apiParams.limit = limit; + apiParams.offset = offset; + const [error, transactions] = await tryCatch( + getWaleltTransactions(apiParams), + ); + + if (error) { + if (error.message.includes("Unauthorized")) { + redirect("/auth/signin"); + } else { + return
{JSON.stringify(error, null, 2)}
; + } + } + const { data, meta } = transactions; + return ( +
+ {data?.length === 0 ? ( +
+

No transactions yet.

+
+ ) : ( + <> +
+ + Table of all transactions. + + + Description + Amount + Transaction Type + View Details + Created at + + + + {transactions?.data?.map((trx) => ( + + + + {trx.description} + + + {trx.amount.toFixed(2)} MVR + + + + {trx.transaction_type === "CREDIT" ? ( + + {trx.transaction_type} + + ) : ( + + {trx.transaction_type} + + )} + + + + + + {new Date(trx.created_at).toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + minute: "2-digit", + hour: "2-digit", + })} + + + + + + + ))} + + + + + {meta?.total === 1 ? ( +

+ Total {meta?.total} transaction. +

+ ) : ( +

+ Total {meta?.total} transactions. +

+ )} +
+
+
+
+
+
+ {data.map((trx) => ( + + ))} +
+ + + )} +
+ ); +} + +function MobileTransactionDetails({ trx }: { trx: WalletTransaction }) { + return ( +
+
+
+ + + {new Date(trx.created_at).toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + minute: "2-digit", + hour: "2-digit", + })} + +
+

{trx.description}

+
+ +
+
+

Amount

+ + {trx.amount.toFixed(2)} MVR + +
+ + {trx.transaction_type === "CREDIT" ? ( + + {trx.transaction_type} + + ) : ( + + {trx.transaction_type} + + )} + +
+
+ + + +
+
+ ); +} diff --git a/lib/backend-types.ts b/lib/backend-types.ts index e40b441..3f74075 100644 --- a/lib/backend-types.ts +++ b/lib/backend-types.ts @@ -97,3 +97,16 @@ export interface NewPayment { number_of_months: number; amount: number; } + + +export interface WalletTransaction { + id: string; + user: Pick & { + name: string; + }; + amount: number; + transaction_type: "DEBIT" | "CREDIT"; + description: string; + reference_id: string; + created_at: string; +} \ No newline at end of file diff --git a/queries/wallet.ts b/queries/wallet.ts new file mode 100644 index 0000000..b224dd7 --- /dev/null +++ b/queries/wallet.ts @@ -0,0 +1,44 @@ +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/auth"; +import type { ApiError, ApiResponse, WalletTransaction } from "@/lib/backend-types"; + +type GenericGetResponseProps = { + offset?: number; + limit?: number; + page?: number; + [key: string]: string | number | undefined; +}; +export async function getWaleltTransactions( + params: GenericGetResponseProps, + allTransactions = false, +) { + // Build query string from all defined params + const query = Object.entries(params) + .filter(([_, value]) => value !== undefined && value !== "") + .map( + ([key, value]) => + `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`, + ) + .join("&"); + const session = await getServerSession(authOptions); + const response = await fetch( + `${process.env.SARLINK_API_BASE_URL}/api/billing/wallet-transactions/?${query}&all_transactions=${allTransactions}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Token ${session?.apiToken}`, + }, + }, + ); + if (!response.ok) { + const errorData = (await response.json()) as ApiError; + const errorMessage = + errorData.message || errorData.detail || "An error occurred."; + const error = new Error(errorMessage); + (error as ApiError & { details?: ApiError }).details = errorData; // Attach the errorData to the error object + throw error; + } + const data = (await response.json()) as ApiResponse; + return data; +} \ No newline at end of file