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";
import type { PaymentType } from "@/lib/types";
import { formatMacAddress } from "@/lib/utils";
import { authOptions } from "@/app/auth";
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 { 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);
// const payment = await prisma.payment.create({
// data: {
// amount: data.amount,
// numberOfMonths: data.numberOfMonths,
// paid: data.paid,
// userId: data.userId,
// devices: {
// connect: data.deviceIds.map((id) => {
// return {
// id,
// };
// }),
// },
// },
// });
// redirect(`/payments/${payment.id}`);
const response = await fetch(
`${
process.env.SARLINK_API_BASE_URL // });
}/api/billing/payment/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
body: JSON.stringify(data),
},
);
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;
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 = {
@ -43,45 +160,45 @@ class InsufficientFundsError extends Error {
}
}
async function processWalletPayment(
user: { id: string; walletBalance: number } | null,
payment: PaymentWithDevices | null,
amount: number,
) {
if (!user || !payment) {
export async function processWalletPayment({
payment,
amount,
}: { payment: Payment | undefined; amount: number }) {
const session = await getServerSession(authOptions);
if (!session?.user || !payment) {
throw new Error("User or payment not found");
}
const walletBalance = user.walletBalance ?? 0;
const walletBalance = session.user.wallet_balance ?? 0;
if (walletBalance < amount) {
throw new InsufficientFundsError();
}
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
await prisma.$transaction([
prisma.payment.update({
where: { id: payment.id },
data: {
paid: true,
paidAt: new Date(),
method: "WALLET",
devices: {
updateMany: payment.devices.map((device) => ({
where: { id: device.id },
data: {
isActive: true,
expiryDate: expiryDate,
},
})),
},
},
const [updatePaymentError, _] = await tryCatch(
updatePayment({
id: payment.id,
method: "WALLET",
paid: true,
paid_at: new Date().toISOString(),
number_of_months: payment.number_of_months,
}),
prisma.user.update({
where: { id: user.id },
data: { walletBalance: walletBalance - amount },
);
if (updatePaymentError) {
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 =
@ -99,98 +216,98 @@ type VerifyPaymentResponse =
};
};
async function verifyExternalPayment(
data: VerifyPaymentType,
payment: PaymentWithDevices | null,
): Promise<VerifyPaymentResponse> {
console.log("payment verify data ->", data);
const response = await fetch(
"https://verifypaymentsapi.baraveli.dev/verify-payment",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
},
);
// async function verifyExternalPayment(
// data: VerifyPaymentType,
// payment: PaymentWithDevices | null,
// ): Promise<VerifyPaymentResponse> {
// console.log("payment verify data ->", data);
// const response = await fetch(
// "https://verifypaymentsapi.baraveli.dev/verify-payment",
// {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(data),
// },
// );
const json = await response.json();
console.log(json);
if (!payment) {
throw new Error("Payment verification failed or payment not found");
}
// const json = await response.json();
// console.log(json);
// if (!payment) {
// throw new Error("Payment verification failed or payment not found");
// }
if (json.success) {
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
await prisma.payment.update({
where: { id: payment.id },
data: {
paid: true,
paidAt: new Date(),
method: "TRANSFER",
devices: {
updateMany: payment.devices.map((device) => ({
where: { id: device.id },
data: {
isActive: true,
expiryDate: expiryDate,
},
})),
},
},
});
}
// if (json.success) {
// const expiryDate = new Date();
// expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
// await prisma.payment.update({
// where: { id: payment.id },
// data: {
// paid: true,
// paidAt: new Date(),
// method: "TRANSFER",
// devices: {
// updateMany: payment.devices.map((device) => ({
// where: { id: device.id },
// data: {
// isActive: true,
// expiryDate: expiryDate,
// },
// })),
// },
// },
// });
// }
return json;
}
// return json;
// }
async function updateDevices(payment: PaymentWithDevices | null) {
if (!payment) return;
// async function updateDevices(payment: PaymentWithDevices | null) {
// if (!payment) return;
const newDevices = payment.devices.map((d) => ({
name: d.name,
macAddress: formatMacAddress(d.mac),
}));
// const newDevices = payment.devices.map((d) => ({
// name: d.name,
// macAddress: formatMacAddress(d.mac),
// }));
return await addDevicesToGroup({
groupId: process.env.OMADA_GROUP_ID,
siteId: process.env.OMADA_SITE_ID,
newDevices,
});
}
// return await addDevicesToGroup({
// groupId: process.env.OMADA_GROUP_ID,
// siteId: process.env.OMADA_SITE_ID,
// newDevices,
// });
// }
export async function verifyPayment(data: VerifyPaymentType) {
try {
const [payment, user] = await Promise.all([
prisma.payment.findUnique({
where: { id: data.paymentId },
include: { devices: true },
}),
prisma.user.findUnique({
where: { id: data.userId },
}),
]);
// export async function verifyPayment(data: VerifyPaymentType) {
// try {
// const [payment, user] = await Promise.all([
// prisma.payment.findUnique({
// where: { id: data.paymentId },
// include: { devices: true },
// }),
// prisma.user.findUnique({
// where: { id: data.userId },
// }),
// ]);
if (data.type === "WALLET") {
console.log("WALLET");
await processWalletPayment(user, payment, Number(data.absAmount));
redirect("/payments");
}
if (data.type === "TRANSFER") {
console.log({ data, payment });
const verificationResult = await verifyExternalPayment(data, payment);
await updateDevices(payment);
// if (data.type === "WALLET") {
// console.log("WALLET");
// await processWalletPayment(user, payment, Number(data.absAmount));
// redirect("/payments");
// }
// if (data.type === "TRANSFER") {
// console.log({ data, payment });
// const verificationResult = await verifyExternalPayment(data, payment);
// await updateDevices(payment);
revalidatePath("/payment[paymentId]");
// revalidatePath("/payment[paymentId]");
return verificationResult;
}
} catch (error) {
console.error("Payment verification failed:", error);
throw error; // Re-throw to handle at a higher level
}
}
// return verificationResult;
// }
// } catch (error) {
// console.error("Payment verification failed:", error);
// throw error; // Re-throw to handle at a higher level
// }
// }
export async function addDevicesToOmada() {
console.log("hi");
}
// export async function addDevicesToOmada() {
// 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 { auth } from "@/app/auth";
import prisma from "@/lib/db";
import { cn } from "@/lib/utils";
import { tryCatch } from "@/utils/tryCatch";
import { getServerSession } from "next-auth";
import { headers } from "next/headers";
import React from "react";
export default async function PaymentPage({
@ -9,23 +11,13 @@ export default async function PaymentPage({
}: {
params: Promise<{ paymentId: string }>;
}) {
const session = await auth.api.getSession({
headers: await headers(),
});
const user = await prisma.user.findUnique({
where: {
id: session?.session.userId,
},
});
const session = await getServerSession(authOptions);
const paymentId = (await params).paymentId;
const payment = await prisma.payment.findUnique({
where: {
id: paymentId,
},
include: {
devices: true,
},
});
const [error, payment] = await tryCatch(getPayment({ id: paymentId }));
if (error) {
return <div>Error getting payment: {error.message}</div>;
}
return (
<div>
<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"
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>
);

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

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

View File

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

View File

@ -31,7 +31,7 @@ export async function ApplicationLayout({
<SidebarTrigger className="-ml-1" />
<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">
Welcome back,{" "}
Welcome,{" "}
<span className="font-semibold">
{session?.user?.first_name} {session?.user?.last_name}
</span>
@ -39,7 +39,7 @@ export async function ApplicationLayout({
</div>
<div className="flex items-center gap-2">
{/* <Wallet walletBalance={user?.walletBalance || 0} /> */}
<Wallet walletBalance={session?.user?.wallet_balance || 0} />
<ModeToggle />
<AccountPopover />
</div>

View File

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

View File

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

View File

@ -1,189 +1,207 @@
'use client'
import { verifyPayment } from "@/actions/payment";
"use client";
import { processWalletPayment } from "@/actions/payment";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableRow,
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableRow,
} from "@/components/ui/table";
import type { Payment } from "@/lib/backend-types";
import { formatDate } from "@/lib/utils";
import type { Prisma, User } from "@prisma/client";
import { BadgeDollarSign, Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react";
import {
BadgeDollarSign,
Clipboard,
ClipboardCheck,
Loader2,
Wallet,
} from "lucide-react";
import type { User } from "next-auth";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "./ui/button";
type PaymentWithDevices = Prisma.PaymentGetPayload<{
include: {
devices: true;
};
}>;
export default function DevicesToPay({
payment,
user
}: { payment?: PaymentWithDevices, user?: User }) {
const [verifying, setVerifying] = useState(false)
payment,
user,
}: { payment?: Payment; user?: User }) {
const [verifying, setVerifying] = useState(false);
const devices = payment?.devices;
if (devices?.length === 0) {
return null;
}
// 100+(n1)×75
const walletBalance = user?.walletBalance ?? 0;
const isWalletPayVisible = walletBalance > (payment?.amount ?? 0);
const devices = payment?.devices;
if (devices?.length === 0) {
return null;
}
// 100+(n1)×75
// const walletBalance = user?.walletBalance ?? 0;
// TODO - get wallet balance from backend
const walletBalance = 110;
const isWalletPayVisible = walletBalance > (payment?.amount ?? 0);
return (
<div className="w-full">
<div className="p-2 flex flex-col gap-2">
<h3 className="title-bg my-1 p-2 border border-dashed rounded-md font-semibold text-lg">
{!payment?.paid ? "Devices to pay" : "Devices Paid"}
</h3>
<div className="flex flex-col gap-2">
{devices?.map((device) => (
<div
key={device.id}
className="bg-muted border rounded p-2 flex gap-2 items-center"
>
<div className="flex flex-col">
<div className="text-sm font-medium">{device.name}</div>
<div className="text-xs text-muted-foreground">
{device.mac}
</div>
</div>
</div>
))}
</div>
</div>
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
<Table>
<TableCaption>
<div className="max-w-sm mx-auto">
<p>Please send the following amount to the payment address</p>
<AccountInfomation
accName="Baraveli Dev"
accountNo="90101400028321000"
/>
{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>
) : (
<div className="flex flex-col gap-2">
{isWalletPayVisible && (
<Button
disabled={verifying}
onClick={async () => {
setVerifying(true);
await verifyPayment({
userId: user?.id ?? "",
paymentId: payment?.id,
benefName: user?.name ?? "",
accountNo: user?.accNo ?? "",
absAmount: String(payment?.amount),
time: formatDate(new Date(payment?.createdAt || "")),
type: "WALLET",
});
setVerifying(false);
}}
variant={"secondary"} size={"lg"}>
{verifying ? "Paying..." : "Pay with wallet"}
<Wallet />
</Button>
)}
<Button
disabled={verifying}
onClick={async () => {
setVerifying(true);
const res = await verifyPayment({
userId: user?.id ?? "",
paymentId: payment?.id,
benefName: user?.name ?? "",
accountNo: user?.accNo ?? "",
absAmount: String(payment?.amount),
type: "TRANSFER",
time: formatDate(new Date(payment?.createdAt || "")),
});
setVerifying(false);
switch (true) {
case res?.success === true:
toast.success(res.message);
break;
case res?.success === false:
toast.error(res?.message);
break;
default:
toast.error("Unexpected error occurred.");
}
}}
size={"lg"} className="mb-4">
{verifying ? "Verifying..." : "Verify Payment"}
{verifying ? <Loader2 className="animate-spin" /> : <BadgeDollarSign />}
</Button>
</div>
)}
</div>
</TableCaption>
<TableBody className="">
<TableRow>
<TableCell>Total Devices</TableCell>
<TableCell className="text-right text-xl">{devices?.length}</TableCell>
</TableRow>
<TableRow>
<TableCell>Duration</TableCell>
<TableCell className="text-right text-xl">{payment?.numberOfMonths} Months</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold">{payment?.amount.toFixed(2)}</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
);
return (
<div className="w-full">
<div className="p-2 flex flex-col gap-2">
<h3 className="title-bg my-1 p-2 border border-dashed rounded-md font-semibold text-lg">
{!payment?.paid ? "Devices to pay" : "Devices Paid"}
</h3>
<div className="flex flex-col gap-2">
{devices?.map((device) => (
<div
key={device.id}
className="bg-muted border rounded p-2 flex gap-2 items-center"
>
<div className="flex flex-col">
<div className="text-sm font-medium">{device.name}</div>
<div className="text-xs text-muted-foreground">
{device.mac}
</div>
</div>
</div>
))}
</div>
</div>
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
<Table>
<TableCaption>
<div className="max-w-sm mx-auto">
<p>Please send the following amount to the payment address</p>
<AccountInfomation
accName="Baraveli Dev"
accountNo="90101400028321000"
/>
{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>
) : (
<div className="flex flex-col gap-2">
{isWalletPayVisible && (
<Button
disabled={verifying}
onClick={async () => {
setVerifying(true);
await processWalletPayment({
amount: payment?.amount ?? 0,
payment: payment,
});
setVerifying(false);
}}
variant={"secondary"}
size={"lg"}
>
{verifying ? "Paying..." : "Pay with wallet"}
<Wallet />
</Button>
)}
<Button
disabled={verifying}
onClick={async () => {
setVerifying(true);
// const res = await verifyPayment({
// userId: user?.id ?? "",
// paymentId: payment?.id,
// benefName: user?.name ?? "",
// accountNo: user?.accNo ?? "",
// absAmount: String(payment?.amount),
// type: "TRANSFER",
// time: formatDate(new Date(payment?.createdAt || "")),
// });
setVerifying(false);
// switch (true) {
// case res?.success === true:
// toast.success(res.message);
// break;
// case res?.success === false:
// toast.error(res?.message);
// break;
// default:
// toast.error("Unexpected error occurred.");
// }
}}
size={"lg"}
className="mb-4"
>
{verifying ? "Verifying..." : "Verify Payment"}
{verifying ? (
<Loader2 className="animate-spin" />
) : (
<BadgeDollarSign />
)}
</Button>
</div>
)}
</div>
</TableCaption>
<TableBody className="">
<TableRow>
<TableCell>Total Devices</TableCell>
<TableCell className="text-right text-xl">
{devices?.length}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Duration</TableCell>
<TableCell className="text-right text-xl">
{payment?.number_of_months} Months
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold">
{payment?.amount?.toFixed(2)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
);
}
function AccountInfomation({
accountNo,
accName,
accountNo,
accName,
}: {
accountNo: string;
accName: string;
accountNo: string;
accName: string;
}) {
const [accNo, setAccNo] = useState(false)
return (
<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">
Account Information
</h6>
<div className="border justify-start flex flex-col items-start bg-white/10 w-full p-2 rounded">
<div className="text-sm font-semibold">Account Name</div>
<span>{accName}</span>
</div>
<div className="border flex justify-between items-start gap-2 bg-white/10 w-full p-2 rounded">
<div className="flex flex-col items-start justify-start">
<p className="text-sm font-semibold">Account No</p>
<span>{accountNo}</span>
</div>
<Button
onClick={() => {
setTimeout(() => {
setAccNo(true)
navigator.clipboard.writeText(accountNo)
}, 2000)
toast.success("Account number copied!")
setAccNo((prev) => !prev)
}}
variant={"link"}>
{accNo ? <Clipboard /> : <ClipboardCheck color="green" />}
</Button>
</div>
</div>
);
const [accNo, setAccNo] = useState(false);
return (
<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">
Account Information
</h6>
<div className="border justify-start flex flex-col items-start bg-white/10 w-full p-2 rounded">
<div className="text-sm font-semibold">Account Name</div>
<span>{accName}</span>
</div>
<div className="border flex justify-between items-start gap-2 bg-white/10 w-full p-2 rounded">
<div className="flex flex-col items-start justify-start">
<p className="text-sm font-semibold">Account No</p>
<span>{accountNo}</span>
</div>
<Button
onClick={() => {
setTimeout(() => {
setAccNo(true);
navigator.clipboard.writeText(accountNo);
}, 2000);
toast.success("Account number copied!");
setAccNo((prev) => !prev);
}}
variant={"link"}
>
{accNo ? <Clipboard /> : <ClipboardCheck color="green" />}
</Button>
</div>
</div>
);
}

View File

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

View File

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

View File

@ -50,3 +50,23 @@ export interface Api400Error {
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 = {
amount: number;
userId: string;

View File

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

View File

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

View File

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