refactor: update payment types and user interface, enhance error handling, and adjust API base URL
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 3m14s

This commit is contained in:
i701 2025-04-05 23:25:17 +05:00
parent aa18484475
commit 9e2a2f430e
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
15 changed files with 596 additions and 423 deletions

View File

@ -1,29 +1,146 @@
"use server"; "use server";
import type { PaymentType } from "@/lib/types"; import { authOptions } from "@/app/auth";
import { formatMacAddress } from "@/lib/utils"; import type { ApiResponse, NewPayment, Payment } from "@/lib/backend-types";
import type { User } from "@/lib/types/user";
import { tryCatch } from "@/utils/tryCatch";
import { getServerSession } from "next-auth";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { addDevicesToGroup } from "./omada-actions";
export async function createPayment(data: PaymentType) { export async function createPayment(data: NewPayment) {
const session = await getServerSession(authOptions);
console.log("data", data); console.log("data", data);
// const payment = await prisma.payment.create({ const response = await fetch(
// data: { `${
// amount: data.amount, process.env.SARLINK_API_BASE_URL // });
// numberOfMonths: data.numberOfMonths, }/api/billing/payment/`,
// paid: data.paid, {
// userId: data.userId, method: "POST",
// devices: { headers: {
// connect: data.deviceIds.map((id) => { "Content-Type": "application/json",
// return { Authorization: `Token ${session?.apiToken}`,
// id, },
// }; body: JSON.stringify(data),
// }), },
// }, );
// },
// }); if (!response.ok) {
// redirect(`/payments/${payment.id}`); const errorData = await response.json();
// Throw an error with the message from the API
throw new Error(errorData.message || "Something went wrong.");
}
const payment = (await response.json()) as Payment;
revalidatePath("/devices");
redirect(`/payments/${payment.id}`);
}
export async function getPayment({ id }: { id: string }) {
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/payment/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
},
);
if (!response.ok) {
const errorData = await response.json();
// Throw an error with the message from the API
throw new Error(errorData.message || "Something went wrong.");
}
const payment = (await response.json()) as Payment;
return payment;
}
export async function getPayments() {
const session = await getServerSession(authOptions);
const respose = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/payment/`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
},
);
const data = (await respose.json()) as ApiResponse<Payment>;
return data;
}
type UpdatePayment = Pick<
Payment,
"id" | "paid" | "paid_at" | "method" | "number_of_months"
>;
export async function updatePayment({
id,
method,
paid,
paid_at,
number_of_months,
}: UpdatePayment) {
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/payment/${id}/update/`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
body: JSON.stringify({
method,
paid,
paid_at,
number_of_months,
}),
},
);
if (!response.ok) {
const errorData = await response.json();
// Throw an error with the message from the API
throw new Error(errorData.message || "Something went wrong.");
}
const payment = (await response.json()) as Payment;
return payment;
}
type TUpdateWalletBalance = Pick<User, "id" | "wallet_balance">;
export async function updateWalletBalance({
id,
wallet_balance,
}: TUpdateWalletBalance) {
const session = await getServerSession(authOptions);
console.log("wallet bal in server action", wallet_balance);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/update-wallet/${id}/`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
body: JSON.stringify({
wallet_balance: Number.parseFloat(wallet_balance?.toFixed(2) ?? "0"),
}),
},
);
if (!response.ok) {
const errorData = await response.json();
// Throw an error with the message from the API
throw new Error(errorData.message || "Something went wrong.");
}
const message = (await response.json()) as {
message: "Wallet balance updated successfully.";
};
return message;
} }
type VerifyPaymentType = { type VerifyPaymentType = {
@ -43,45 +160,45 @@ class InsufficientFundsError extends Error {
} }
} }
async function processWalletPayment( export async function processWalletPayment({
user: { id: string; walletBalance: number } | null, payment,
payment: PaymentWithDevices | null, amount,
amount: number, }: { payment: Payment | undefined; amount: number }) {
) { const session = await getServerSession(authOptions);
if (!user || !payment) { if (!session?.user || !payment) {
throw new Error("User or payment not found"); throw new Error("User or payment not found");
} }
const walletBalance = user.walletBalance ?? 0; const walletBalance = session.user.wallet_balance ?? 0;
if (walletBalance < amount) { if (walletBalance < amount) {
throw new InsufficientFundsError(); throw new InsufficientFundsError();
} }
const expiryDate = new Date(); const [updatePaymentError, _] = await tryCatch(
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths); updatePayment({
await prisma.$transaction([ id: payment.id,
prisma.payment.update({
where: { id: payment.id },
data: {
paid: true,
paidAt: new Date(),
method: "WALLET", method: "WALLET",
devices: { paid: true,
updateMany: payment.devices.map((device) => ({ paid_at: new Date().toISOString(),
where: { id: device.id }, number_of_months: payment.number_of_months,
data: {
isActive: true,
expiryDate: expiryDate,
},
})),
},
},
}), }),
prisma.user.update({ );
where: { id: user.id }, if (updatePaymentError) {
data: { walletBalance: walletBalance - amount }, throw new Error(updatePaymentError.message);
}
console.log("Wallet balance before update:", walletBalance);
const updated_balance = walletBalance - amount;
const [walletUpdateError, response] = await tryCatch(
updateWalletBalance({
id: session.user.id,
wallet_balance: Number.parseFloat(updated_balance?.toFixed(2) ?? "0"),
}), }),
]); );
if (walletUpdateError) {
throw new Error(walletUpdateError.message);
}
revalidatePath("/payments/[paymentsId]", "page");
return response;
} }
type VerifyPaymentResponse = type VerifyPaymentResponse =
@ -99,98 +216,98 @@ type VerifyPaymentResponse =
}; };
}; };
async function verifyExternalPayment( // async function verifyExternalPayment(
data: VerifyPaymentType, // data: VerifyPaymentType,
payment: PaymentWithDevices | null, // payment: PaymentWithDevices | null,
): Promise<VerifyPaymentResponse> { // ): Promise<VerifyPaymentResponse> {
console.log("payment verify data ->", data); // console.log("payment verify data ->", data);
const response = await fetch( // const response = await fetch(
"https://verifypaymentsapi.baraveli.dev/verify-payment", // "https://verifypaymentsapi.baraveli.dev/verify-payment",
{ // {
method: "POST", // method: "POST",
headers: { "Content-Type": "application/json" }, // headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), // body: JSON.stringify(data),
}, // },
); // );
const json = await response.json(); // const json = await response.json();
console.log(json); // console.log(json);
if (!payment) { // if (!payment) {
throw new Error("Payment verification failed or payment not found"); // throw new Error("Payment verification failed or payment not found");
} // }
if (json.success) { // if (json.success) {
const expiryDate = new Date(); // const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths); // expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
await prisma.payment.update({ // await prisma.payment.update({
where: { id: payment.id }, // where: { id: payment.id },
data: { // data: {
paid: true, // paid: true,
paidAt: new Date(), // paidAt: new Date(),
method: "TRANSFER", // method: "TRANSFER",
devices: { // devices: {
updateMany: payment.devices.map((device) => ({ // updateMany: payment.devices.map((device) => ({
where: { id: device.id }, // where: { id: device.id },
data: { // data: {
isActive: true, // isActive: true,
expiryDate: expiryDate, // expiryDate: expiryDate,
}, // },
})), // })),
}, // },
}, // },
}); // });
} // }
return json; // return json;
} // }
async function updateDevices(payment: PaymentWithDevices | null) { // async function updateDevices(payment: PaymentWithDevices | null) {
if (!payment) return; // if (!payment) return;
const newDevices = payment.devices.map((d) => ({ // const newDevices = payment.devices.map((d) => ({
name: d.name, // name: d.name,
macAddress: formatMacAddress(d.mac), // macAddress: formatMacAddress(d.mac),
})); // }));
return await addDevicesToGroup({ // return await addDevicesToGroup({
groupId: process.env.OMADA_GROUP_ID, // groupId: process.env.OMADA_GROUP_ID,
siteId: process.env.OMADA_SITE_ID, // siteId: process.env.OMADA_SITE_ID,
newDevices, // newDevices,
}); // });
} // }
export async function verifyPayment(data: VerifyPaymentType) { // export async function verifyPayment(data: VerifyPaymentType) {
try { // try {
const [payment, user] = await Promise.all([ // const [payment, user] = await Promise.all([
prisma.payment.findUnique({ // prisma.payment.findUnique({
where: { id: data.paymentId }, // where: { id: data.paymentId },
include: { devices: true }, // include: { devices: true },
}), // }),
prisma.user.findUnique({ // prisma.user.findUnique({
where: { id: data.userId }, // where: { id: data.userId },
}), // }),
]); // ]);
if (data.type === "WALLET") { // if (data.type === "WALLET") {
console.log("WALLET"); // console.log("WALLET");
await processWalletPayment(user, payment, Number(data.absAmount)); // await processWalletPayment(user, payment, Number(data.absAmount));
redirect("/payments"); // redirect("/payments");
} // }
if (data.type === "TRANSFER") { // if (data.type === "TRANSFER") {
console.log({ data, payment }); // console.log({ data, payment });
const verificationResult = await verifyExternalPayment(data, payment); // const verificationResult = await verifyExternalPayment(data, payment);
await updateDevices(payment); // await updateDevices(payment);
revalidatePath("/payment[paymentId]"); // revalidatePath("/payment[paymentId]");
return verificationResult; // return verificationResult;
} // }
} catch (error) { // } catch (error) {
console.error("Payment verification failed:", error); // console.error("Payment verification failed:", error);
throw error; // Re-throw to handle at a higher level // throw error; // Re-throw to handle at a higher level
} // }
} // }
export async function addDevicesToOmada() { // export async function addDevicesToOmada() {
console.log("hi"); // console.log("hi");
} // }

View File

@ -1,7 +1,9 @@
import { getPayment } from "@/actions/payment";
import { authOptions } from "@/app/auth";
import DevicesToPay from "@/components/devices-to-pay"; import DevicesToPay from "@/components/devices-to-pay";
import { auth } from "@/app/auth";
import prisma from "@/lib/db";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { tryCatch } from "@/utils/tryCatch";
import { getServerSession } from "next-auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import React from "react"; import React from "react";
export default async function PaymentPage({ export default async function PaymentPage({
@ -9,23 +11,13 @@ export default async function PaymentPage({
}: { }: {
params: Promise<{ paymentId: string }>; params: Promise<{ paymentId: string }>;
}) { }) {
const session = await auth.api.getSession({ const session = await getServerSession(authOptions);
headers: await headers(),
});
const user = await prisma.user.findUnique({
where: {
id: session?.session.userId,
},
});
const paymentId = (await params).paymentId; const paymentId = (await params).paymentId;
const payment = await prisma.payment.findUnique({ const [error, payment] = await tryCatch(getPayment({ id: paymentId }));
where: { if (error) {
id: paymentId, return <div>Error getting payment: {error.message}</div>;
}, }
include: {
devices: true,
},
});
return ( return (
<div> <div>
<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
@ -46,7 +38,10 @@ export default async function PaymentPage({
id="user-filters" id="user-filters"
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
> >
<DevicesToPay user={user || undefined} payment={payment || undefined} /> <DevicesToPay
user={session?.user || undefined}
payment={payment || undefined}
/>
</div> </div>
</div> </div>
); );

2
app/next-auth.d.ts vendored
View File

@ -16,6 +16,8 @@ declare module "next-auth" {
username?: string; username?: string;
user_permissions?: { id: number; name: string }[]; user_permissions?: { id: number; name: string }[];
id_card?: string; id_card?: string;
mobile?: string;
wallet_balance?: number;
first_name?: string; first_name?: string;
last_name?: string; last_name?: string;
last_login?: string; last_login?: string;

View File

@ -27,15 +27,16 @@ export function AccountPopover() {
<UserIcon /> <UserIcon />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-fit"> <PopoverContent className="w-fit mr-4">
<div className="grid gap-4"> <div className="grid gap-4">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-medium leading-none"> <h4 className="font-medium leading-none">
{session.data?.user?.name} {session.data?.user?.first_name} {session.data?.user?.last_name}
</h4> </h4>
<p className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{session.data?.user?.id_card} <p className="font-semibold">{session.data?.user?.id_card}</p>
</p> <p>{session.data?.user?.mobile}</p>
</div>
</div> </div>
<Button <Button
disabled={loading} disabled={loading}

View File

@ -31,7 +31,7 @@ export async function ApplicationLayout({
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="mr-2 h-4" />
<div className="text-sm font-mono px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400"> <div className="text-sm font-mono px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400">
Welcome back,{" "} Welcome,{" "}
<span className="font-semibold"> <span className="font-semibold">
{session?.user?.first_name} {session?.user?.last_name} {session?.user?.first_name} {session?.user?.last_name}
</span> </span>
@ -39,7 +39,7 @@ export async function ApplicationLayout({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* <Wallet walletBalance={user?.walletBalance || 0} /> */} <Wallet walletBalance={session?.user?.wallet_balance || 0} />
<ModeToggle /> <ModeToggle />
<AccountPopover /> <AccountPopover />
</div> </div>

View File

@ -1,31 +1,33 @@
'use client' "use client";
import { import { TableCell, TableRow } from "@/components/ui/table";
TableCell,
TableRow
} from "@/components/ui/table";
import { deviceCartAtom } from "@/lib/atoms"; import { deviceCartAtom } from "@/lib/atoms";
import type { Device } from "@/lib/backend-types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Device } from "@prisma/client"; import { pl } from "date-fns/locale";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import Link from 'next/link'; import Link from "next/link";
import AddDevicesToCartButton from "./add-devices-to-cart-button"; import AddDevicesToCartButton from "./add-devices-to-cart-button";
import BlockDeviceDialog from "./block-device-dialog"; import BlockDeviceDialog from "./block-device-dialog";
export default function ClickableRow({ device, parentalControl, admin = false }: { device: Device, parentalControl?: boolean, admin?: boolean }) { export default function ClickableRow({
const [devices, setDeviceCart] = useAtom(deviceCartAtom) device,
parentalControl,
admin = false,
}: { device: Device; parentalControl?: boolean; admin?: boolean }) {
const [devices, setDeviceCart] = useAtom(deviceCartAtom);
return ( return (
<TableRow <TableRow
key={device.id} key={device.id}
className={cn(parentalControl === false && "cursor-pointer hover:bg-muted",)} className={cn(
parentalControl === false && "cursor-pointer hover:bg-muted",
)}
onClick={() => { onClick={() => {
if (parentalControl === true) return;
if (parentalControl === true) return
setDeviceCart((prev) => setDeviceCart((prev) =>
devices.some((d) => d.id === device.id) devices.some((d) => d.id === device.id)
? prev.filter((d) => d.id !== device.id) ? prev.filter((d) => d.id !== device.id)
: [...prev, device] : [...prev, device],
) );
}} }}
> >
<TableCell> <TableCell>
@ -37,20 +39,23 @@ export default function ClickableRow({ device, parentalControl, admin = false }:
> >
{device.name} {device.name}
</Link> </Link>
{device.is_active ? (
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Active until{" "} Active until{" "}
{new Date(device.expiryDate || "").toLocaleDateString("en-US", { {new Date(device.expiry_date || "").toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
})} })}
</span> </span>
{(device.blockedBy === "ADMIN" && device.blocked) && ( ) : (
<p>Inactive</p>
)}
{device.blocked_by === "ADMIN" && device.blocked && (
<div className="p-2 rounded border my-2"> <div className="p-2 rounded border my-2">
<span>Comment: </span> <span>Comment: </span>
<p className="text-neutral-500"> <p className="text-neutral-500">{device?.reason_for_blocking}</p>
{device?.reasonForBlocking}
</p>
</div> </div>
)} )}
</div> </div>
@ -60,9 +65,13 @@ export default function ClickableRow({ device, parentalControl, admin = false }:
{!parentalControl ? ( {!parentalControl ? (
<AddDevicesToCartButton device={device} /> <AddDevicesToCartButton device={device} />
) : ( ) : (
<BlockDeviceDialog admin={admin} type={device.blocked ? "unblock" : "block"} device={device} /> <BlockDeviceDialog
admin={admin}
type={device.blocked ? "unblock" : "block"}
device={device}
/>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
) );
} }

View File

@ -5,12 +5,14 @@ import DeviceCard from "@/components/device-card";
import NumberInput from "@/components/number-input"; import NumberInput from "@/components/number-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { deviceCartAtom, numberOfMonths } from "@/lib/atoms"; import { deviceCartAtom, numberOfMonths } from "@/lib/atoms";
import type { PaymentType } from "@/lib/types"; import type { NewPayment, Payment } from "@/lib/backend-types";
import { tryCatch } from "@/utils/tryCatch";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { CircleDollarSign, Loader2 } from "lucide-react"; import { CircleDollarSign, Loader2 } from "lucide-react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
export default function DevicesForPayment() { export default function DevicesForPayment() {
const baseAmount = 100; const baseAmount = 100;
const discountPercentage = 75; const discountPercentage = 75;
@ -37,12 +39,10 @@ export default function DevicesForPayment() {
return null; return null;
} }
const data: PaymentType = { const data: NewPayment = {
numberOfMonths: months, amount: 100,
userId: session?.data?.user?.id ?? "", number_of_months: 2,
deviceIds: devices.map((device) => device.id), device_ids: devices.map((device) => device.id),
amount: Number.parseFloat(total.toFixed(2)),
paid: false,
}; };
return ( return (
@ -69,7 +69,12 @@ export default function DevicesForPayment() {
<Button <Button
onClick={async () => { onClick={async () => {
setDisabled(true); setDisabled(true);
await createPayment(data); const [error, respose] = await tryCatch(createPayment(data));
if (error) {
setDisabled(false);
toast.error(error.message);
return;
}
setDeviceCart([]); setDeviceCart([]);
setMonths(1); setMonths(1);
setDisabled(false); setDisabled(false);

View File

@ -1,5 +1,5 @@
'use client' "use client";
import { verifyPayment } from "@/actions/payment"; import { processWalletPayment } from "@/actions/payment";
import { import {
Table, Table,
TableBody, TableBody,
@ -8,33 +8,36 @@ import {
TableFooter, TableFooter,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { Payment } from "@/lib/backend-types";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import type { Prisma, User } from "@prisma/client"; import {
import { BadgeDollarSign, Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react"; BadgeDollarSign,
Clipboard,
ClipboardCheck,
Loader2,
Wallet,
} from "lucide-react";
import type { User } from "next-auth";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
type PaymentWithDevices = Prisma.PaymentGetPayload<{
include: {
devices: true;
};
}>;
export default function DevicesToPay({ export default function DevicesToPay({
payment, payment,
user user,
}: { payment?: PaymentWithDevices, user?: User }) { }: { payment?: Payment; user?: User }) {
const [verifying, setVerifying] = useState(false) const [verifying, setVerifying] = useState(false);
const devices = payment?.devices; const devices = payment?.devices;
if (devices?.length === 0) { if (devices?.length === 0) {
return null; return null;
} }
// 100+(n1)×75 // 100+(n1)×75
const walletBalance = user?.walletBalance ?? 0; // const walletBalance = user?.walletBalance ?? 0;
const isWalletPayVisible = walletBalance > (payment?.amount ?? 0); // TODO - get wallet balance from backend
const walletBalance = 110;
const isWalletPayVisible = walletBalance > (payment?.amount ?? 0);
return ( return (
<div className="w-full"> <div className="w-full">
@ -68,7 +71,14 @@ export default function DevicesToPay({
accountNo="90101400028321000" accountNo="90101400028321000"
/> />
{payment?.paid ? ( {payment?.paid ? (
<Button size={"lg"} variant={"secondary"} disabled className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold">Payment Verified</Button> <Button
size={"lg"}
variant={"secondary"}
disabled
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
>
Payment Verified
</Button>
) : ( ) : (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{isWalletPayVisible && ( {isWalletPayVisible && (
@ -76,18 +86,15 @@ export default function DevicesToPay({
disabled={verifying} disabled={verifying}
onClick={async () => { onClick={async () => {
setVerifying(true); setVerifying(true);
await verifyPayment({ await processWalletPayment({
userId: user?.id ?? "", amount: payment?.amount ?? 0,
paymentId: payment?.id, payment: payment,
benefName: user?.name ?? "",
accountNo: user?.accNo ?? "",
absAmount: String(payment?.amount),
time: formatDate(new Date(payment?.createdAt || "")),
type: "WALLET",
}); });
setVerifying(false); setVerifying(false);
}} }}
variant={"secondary"} size={"lg"}> variant={"secondary"}
size={"lg"}
>
{verifying ? "Paying..." : "Pay with wallet"} {verifying ? "Paying..." : "Pay with wallet"}
<Wallet /> <Wallet />
</Button> </Button>
@ -96,51 +103,61 @@ export default function DevicesToPay({
disabled={verifying} disabled={verifying}
onClick={async () => { onClick={async () => {
setVerifying(true); setVerifying(true);
const res = await verifyPayment({ // const res = await verifyPayment({
userId: user?.id ?? "", // userId: user?.id ?? "",
paymentId: payment?.id, // paymentId: payment?.id,
benefName: user?.name ?? "", // benefName: user?.name ?? "",
accountNo: user?.accNo ?? "", // accountNo: user?.accNo ?? "",
absAmount: String(payment?.amount), // absAmount: String(payment?.amount),
type: "TRANSFER", // type: "TRANSFER",
time: formatDate(new Date(payment?.createdAt || "")), // time: formatDate(new Date(payment?.createdAt || "")),
}); // });
setVerifying(false); setVerifying(false);
switch (true) { // switch (true) {
case res?.success === true: // case res?.success === true:
toast.success(res.message); // toast.success(res.message);
break; // break;
case res?.success === false: // case res?.success === false:
toast.error(res?.message); // toast.error(res?.message);
break; // break;
default: // default:
toast.error("Unexpected error occurred."); // toast.error("Unexpected error occurred.");
} // }
}} }}
size={"lg"} className="mb-4"> size={"lg"}
className="mb-4"
>
{verifying ? "Verifying..." : "Verify Payment"} {verifying ? "Verifying..." : "Verify Payment"}
{verifying ? <Loader2 className="animate-spin" /> : <BadgeDollarSign />} {verifying ? (
<Loader2 className="animate-spin" />
) : (
<BadgeDollarSign />
)}
</Button> </Button>
</div> </div>
)} )}
</div> </div>
</TableCaption> </TableCaption>
<TableBody className=""> <TableBody className="">
<TableRow> <TableRow>
<TableCell>Total Devices</TableCell> <TableCell>Total Devices</TableCell>
<TableCell className="text-right text-xl">{devices?.length}</TableCell> <TableCell className="text-right text-xl">
{devices?.length}
</TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell>Duration</TableCell> <TableCell>Duration</TableCell>
<TableCell className="text-right text-xl">{payment?.numberOfMonths} Months</TableCell> <TableCell className="text-right text-xl">
{payment?.number_of_months} Months
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow className=""> <TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell> <TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold">{payment?.amount.toFixed(2)}</TableCell> <TableCell className="text-right text-3xl font-bold">
{payment?.amount?.toFixed(2)}
</TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
@ -156,7 +173,7 @@ function AccountInfomation({
accountNo: string; accountNo: string;
accName: string; accName: string;
}) { }) {
const [accNo, setAccNo] = useState(false) const [accNo, setAccNo] = useState(false);
return ( return (
<div className="justify-start items-start border my-4 flex flex-col gap-2 p-2 rounded-md"> <div className="justify-start items-start border my-4 flex flex-col gap-2 p-2 rounded-md">
<h6 className="title-bg uppercase p-2 border rounded w-full font-semibold"> <h6 className="title-bg uppercase p-2 border rounded w-full font-semibold">
@ -174,13 +191,14 @@ function AccountInfomation({
<Button <Button
onClick={() => { onClick={() => {
setTimeout(() => { setTimeout(() => {
setAccNo(true) setAccNo(true);
navigator.clipboard.writeText(accountNo) navigator.clipboard.writeText(accountNo);
}, 2000) }, 2000);
toast.success("Account number copied!") toast.success("Account number copied!");
setAccNo((prev) => !prev) setAccNo((prev) => !prev);
}} }}
variant={"link"}> variant={"link"}
>
{accNo ? <Clipboard /> : <ClipboardCheck color="green" />} {accNo ? <Clipboard /> : <ClipboardCheck color="green" />}
</Button> </Button>
</div> </div>

View File

@ -10,7 +10,10 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import Link from "next/link"; import Link from "next/link";
import { getPayments } from "@/actions/payment";
import type { Payment } from "@/lib/backend-types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { tryCatch } from "@/utils/tryCatch";
import { Calendar } from "lucide-react"; import { Calendar } from "lucide-react";
import Pagination from "./pagination"; import Pagination from "./pagination";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
@ -26,6 +29,7 @@ export async function PaymentsTable({
sortBy: string; sortBy: string;
}>; }>;
}) { }) {
const query = (await searchParams)?.query || "";
// const session = await auth.api.getSession({ // const session = await auth.api.getSession({
// headers: await headers(), // headers: await headers(),
// }); // });
@ -79,11 +83,15 @@ export async function PaymentsTable({
// createdAt: "desc", // createdAt: "desc",
// }, // },
// }); // });
const [error, payments] = await tryCatch(getPayments());
return null; if (error) {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
const { data, meta, links } = payments;
return ( return (
<div> <div>
{payments.length === 0 ? ( {data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No Payments yet.</h3> <h3>No Payments yet.</h3>
</div> </div>
@ -101,7 +109,7 @@ export async function PaymentsTable({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody className="overflow-scroll"> <TableBody className="overflow-scroll">
{payments.map((payment) => ( {payments?.data?.map((payment) => (
<TableRow key={payment.id}> <TableRow key={payment.id}>
<TableCell> <TableCell>
<div <div
@ -115,7 +123,7 @@ export async function PaymentsTable({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} /> <Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{new Date(payment.createdAt).toLocaleDateString( {new Date(payment.created_at).toLocaleDateString(
"en-US", "en-US",
{ {
month: "short", month: "short",
@ -162,7 +170,7 @@ export async function PaymentsTable({
</div> </div>
</TableCell> </TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium">
{payment.numberOfMonths} Months {payment.number_of_months} Months
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
@ -178,21 +186,25 @@ export async function PaymentsTable({
<TableCell colSpan={2}> <TableCell colSpan={2}>
{query.length > 0 && ( {query.length > 0 && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Showing {payments.length} locations for &quot;{query} Showing {payments?.data?.length} locations for &quot;
{query}
&quot; &quot;
</p> </p>
)} )}
</TableCell> </TableCell>
<TableCell className="text-muted-foreground"> <TableCell className="text-muted-foreground">
{totalPayments} payments {meta.total} payments
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
<Pagination totalPages={totalPages} currentPage={page} /> <Pagination
totalPages={meta.total / meta.per_page}
currentPage={meta.current_page}
/>
</div> </div>
<div className="sm:hidden block"> <div className="sm:hidden block">
{payments.map((payment) => ( {data.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} /> <MobilePaymentDetails key={payment.id} payment={payment} />
))} ))}
</div> </div>
@ -202,7 +214,7 @@ export async function PaymentsTable({
); );
} }
function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) { function MobilePaymentDetails({ payment }: { payment: Payment }) {
return ( return (
<div <div
className={cn( className={cn(
@ -215,7 +227,7 @@ function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} /> <Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground text-sm"> <span className="text-muted-foreground text-sm">
{new Date(payment.createdAt).toLocaleDateString("en-US", { {new Date(payment.created_at).toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
@ -256,7 +268,7 @@ function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) {
<Separator className="my-2" /> <Separator className="my-2" />
<h3 className="text-sm font-medium">Duration</h3> <h3 className="text-sm font-medium">Duration</h3>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{payment.numberOfMonths} Months {payment.number_of_months} Months
</span> </span>
<Separator className="my-2" /> <Separator className="my-2" />
<h3 className="text-sm font-medium">Amount</h3> <h3 className="text-sm font-medium">Amount</h3>

View File

@ -37,7 +37,7 @@ export function Wallet({
} }
const data: TopupType = { const data: TopupType = {
userId: session?.data?.user.id ?? "", userId: session?.data?.user?.id ?? "",
amount: Number.parseFloat(amount.toFixed(2)), amount: Number.parseFloat(amount.toFixed(2)),
paid: false, paid: false,
}; };

View File

@ -50,3 +50,23 @@ export interface Api400Error {
message: string; message: string;
}; };
} }
export interface Payment {
id: string;
devices: Device[];
number_of_months: number;
amount: number;
paid: boolean;
paid_at: string | null;
method: string;
expires_at: string | null;
created_at: string;
updated_at: string;
user: number;
}
export interface NewPayment {
device_ids: number[];
number_of_months: number;
amount: number;
}

View File

@ -1,11 +1,3 @@
export type PaymentType = {
numberOfMonths: number;
userId: string;
deviceIds: string[];
amount: number;
paid: boolean;
};
export type TopupType = { export type TopupType = {
amount: number; amount: number;
userId: string; userId: string;

View File

@ -19,6 +19,8 @@ export interface User {
user_permissions: Permission[]; user_permissions: Permission[];
first_name: string; first_name: string;
last_name: string; last_name: string;
mobile?: string;
wallet_balance?: number;
is_superuser: boolean; is_superuser: boolean;
date_joined: string; date_joined: string;
last_login: string; last_login: string;

View File

@ -28,7 +28,7 @@ export async function login({
export async function logout({ token }: { token: string }) { export async function logout({ token }: { token: string }) {
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/logout/`, `${process.env.SARLINK_API_BASE_URL}/auth/logout/`,
{ {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -4,7 +4,7 @@ axios.defaults.xsrfCookieName = "csrftoken";
axios.defaults.xsrfHeaderName = "X-CSRFToken"; axios.defaults.xsrfHeaderName = "X-CSRFToken";
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL, baseURL: process.env.SARLINK_API_BASE_URL,
validateStatus: (status) => { validateStatus: (status) => {
return status < 500; // Resolve only if the status code is less than 500 return status < 500; // Resolve only if the status code is less than 500
}, },