mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-04-20 03:50:20 +00:00
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
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 3m14s
This commit is contained in:
parent
aa18484475
commit
9e2a2f430e
@ -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({
|
method: "WALLET",
|
||||||
where: { id: payment.id },
|
paid: true,
|
||||||
data: {
|
paid_at: new Date().toISOString(),
|
||||||
paid: true,
|
number_of_months: payment.number_of_months,
|
||||||
paidAt: new Date(),
|
|
||||||
method: "WALLET",
|
|
||||||
devices: {
|
|
||||||
updateMany: payment.devices.map((device) => ({
|
|
||||||
where: { id: device.id },
|
|
||||||
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");
|
||||||
}
|
// }
|
||||||
|
@ -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
2
app/next-auth.d.ts
vendored
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -1,68 +1,77 @@
|
|||||||
'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 (
|
||||||
|
<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 (
|
{device.blocked_by === "ADMIN" && device.blocked && (
|
||||||
<TableRow
|
<div className="p-2 rounded border my-2">
|
||||||
key={device.id}
|
<span>Comment: </span>
|
||||||
className={cn(parentalControl === false && "cursor-pointer hover:bg-muted",)}
|
<p className="text-neutral-500">{device?.reason_for_blocking}</p>
|
||||||
onClick={() => {
|
</div>
|
||||||
|
)}
|
||||||
if (parentalControl === true) return
|
</div>
|
||||||
setDeviceCart((prev) =>
|
</TableCell>
|
||||||
devices.some((d) => d.id === device.id)
|
<TableCell className="font-medium">{device.mac}</TableCell>
|
||||||
? prev.filter((d) => d.id !== device.id)
|
<TableCell>
|
||||||
: [...prev, device]
|
{!parentalControl ? (
|
||||||
)
|
<AddDevicesToCartButton device={device} />
|
||||||
}}
|
) : (
|
||||||
>
|
<BlockDeviceDialog
|
||||||
<TableCell>
|
admin={admin}
|
||||||
<div className="flex flex-col items-start">
|
type={device.blocked ? "unblock" : "block"}
|
||||||
<Link
|
device={device}
|
||||||
className="font-medium hover:underline"
|
/>
|
||||||
href={`/devices/${device.id}`}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
</TableCell>
|
||||||
>
|
</TableRow>
|
||||||
{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 >
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -1,189 +1,207 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import { verifyPayment } from "@/actions/payment";
|
import { processWalletPayment } from "@/actions/payment";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
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+(n−1)×75
|
// 100+(n−1)×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">
|
||||||
<div className="p-2 flex flex-col gap-2">
|
<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">
|
<h3 className="title-bg my-1 p-2 border border-dashed rounded-md font-semibold text-lg">
|
||||||
{!payment?.paid ? "Devices to pay" : "Devices Paid"}
|
{!payment?.paid ? "Devices to pay" : "Devices Paid"}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{devices?.map((device) => (
|
{devices?.map((device) => (
|
||||||
<div
|
<div
|
||||||
key={device.id}
|
key={device.id}
|
||||||
className="bg-muted border rounded p-2 flex gap-2 items-center"
|
className="bg-muted border rounded p-2 flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-sm font-medium">{device.name}</div>
|
<div className="text-sm font-medium">{device.name}</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{device.mac}
|
{device.mac}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
|
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
|
||||||
<Table>
|
<Table>
|
||||||
<TableCaption>
|
<TableCaption>
|
||||||
<div className="max-w-sm mx-auto">
|
<div className="max-w-sm mx-auto">
|
||||||
<p>Please send the following amount to the payment address</p>
|
<p>Please send the following amount to the payment address</p>
|
||||||
<AccountInfomation
|
<AccountInfomation
|
||||||
accName="Baraveli Dev"
|
accName="Baraveli Dev"
|
||||||
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"}
|
||||||
<div className="flex flex-col gap-2">
|
variant={"secondary"}
|
||||||
{isWalletPayVisible && (
|
disabled
|
||||||
<Button
|
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
|
||||||
disabled={verifying}
|
>
|
||||||
onClick={async () => {
|
Payment Verified
|
||||||
setVerifying(true);
|
</Button>
|
||||||
await verifyPayment({
|
) : (
|
||||||
userId: user?.id ?? "",
|
<div className="flex flex-col gap-2">
|
||||||
paymentId: payment?.id,
|
{isWalletPayVisible && (
|
||||||
benefName: user?.name ?? "",
|
<Button
|
||||||
accountNo: user?.accNo ?? "",
|
disabled={verifying}
|
||||||
absAmount: String(payment?.amount),
|
onClick={async () => {
|
||||||
time: formatDate(new Date(payment?.createdAt || "")),
|
setVerifying(true);
|
||||||
type: "WALLET",
|
await processWalletPayment({
|
||||||
});
|
amount: payment?.amount ?? 0,
|
||||||
setVerifying(false);
|
payment: payment,
|
||||||
}}
|
});
|
||||||
variant={"secondary"} size={"lg"}>
|
setVerifying(false);
|
||||||
{verifying ? "Paying..." : "Pay with wallet"}
|
}}
|
||||||
<Wallet />
|
variant={"secondary"}
|
||||||
</Button>
|
size={"lg"}
|
||||||
)}
|
>
|
||||||
<Button
|
{verifying ? "Paying..." : "Pay with wallet"}
|
||||||
disabled={verifying}
|
<Wallet />
|
||||||
onClick={async () => {
|
</Button>
|
||||||
setVerifying(true);
|
)}
|
||||||
const res = await verifyPayment({
|
<Button
|
||||||
userId: user?.id ?? "",
|
disabled={verifying}
|
||||||
paymentId: payment?.id,
|
onClick={async () => {
|
||||||
benefName: user?.name ?? "",
|
setVerifying(true);
|
||||||
accountNo: user?.accNo ?? "",
|
// const res = await verifyPayment({
|
||||||
absAmount: String(payment?.amount),
|
// userId: user?.id ?? "",
|
||||||
type: "TRANSFER",
|
// paymentId: payment?.id,
|
||||||
time: formatDate(new Date(payment?.createdAt || "")),
|
// benefName: user?.name ?? "",
|
||||||
});
|
// accountNo: user?.accNo ?? "",
|
||||||
setVerifying(false);
|
// absAmount: String(payment?.amount),
|
||||||
switch (true) {
|
// type: "TRANSFER",
|
||||||
case res?.success === true:
|
// time: formatDate(new Date(payment?.createdAt || "")),
|
||||||
toast.success(res.message);
|
// });
|
||||||
break;
|
setVerifying(false);
|
||||||
case res?.success === false:
|
// switch (true) {
|
||||||
toast.error(res?.message);
|
// case res?.success === true:
|
||||||
break;
|
// toast.success(res.message);
|
||||||
default:
|
// break;
|
||||||
toast.error("Unexpected error occurred.");
|
// case res?.success === false:
|
||||||
}
|
// toast.error(res?.message);
|
||||||
}}
|
// break;
|
||||||
size={"lg"} className="mb-4">
|
// default:
|
||||||
{verifying ? "Verifying..." : "Verify Payment"}
|
// toast.error("Unexpected error occurred.");
|
||||||
{verifying ? <Loader2 className="animate-spin" /> : <BadgeDollarSign />}
|
// }
|
||||||
</Button>
|
}}
|
||||||
</div>
|
size={"lg"}
|
||||||
|
className="mb-4"
|
||||||
)}
|
>
|
||||||
|
{verifying ? "Verifying..." : "Verify Payment"}
|
||||||
</div>
|
{verifying ? (
|
||||||
</TableCaption>
|
<Loader2 className="animate-spin" />
|
||||||
<TableBody className="">
|
) : (
|
||||||
<TableRow>
|
<BadgeDollarSign />
|
||||||
<TableCell>Total Devices</TableCell>
|
)}
|
||||||
<TableCell className="text-right text-xl">{devices?.length}</TableCell>
|
</Button>
|
||||||
</TableRow>
|
</div>
|
||||||
<TableRow>
|
)}
|
||||||
<TableCell>Duration</TableCell>
|
</div>
|
||||||
<TableCell className="text-right text-xl">{payment?.numberOfMonths} Months</TableCell>
|
</TableCaption>
|
||||||
</TableRow>
|
<TableBody className="">
|
||||||
</TableBody>
|
<TableRow>
|
||||||
<TableFooter>
|
<TableCell>Total Devices</TableCell>
|
||||||
<TableRow className="">
|
<TableCell className="text-right text-xl">
|
||||||
<TableCell colSpan={1}>Total Due</TableCell>
|
{devices?.length}
|
||||||
<TableCell className="text-right text-3xl font-bold">{payment?.amount.toFixed(2)}</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableFooter>
|
<TableRow>
|
||||||
</Table>
|
<TableCell>Duration</TableCell>
|
||||||
</div>
|
<TableCell className="text-right text-xl">
|
||||||
</div>
|
{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({
|
function AccountInfomation({
|
||||||
accountNo,
|
accountNo,
|
||||||
accName,
|
accName,
|
||||||
}: {
|
}: {
|
||||||
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">
|
||||||
Account Information
|
Account Information
|
||||||
</h6>
|
</h6>
|
||||||
<div className="border justify-start flex flex-col items-start bg-white/10 w-full p-2 rounded">
|
<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>
|
<div className="text-sm font-semibold">Account Name</div>
|
||||||
<span>{accName}</span>
|
<span>{accName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border flex justify-between items-start gap-2 bg-white/10 w-full p-2 rounded">
|
<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">
|
<div className="flex flex-col items-start justify-start">
|
||||||
<p className="text-sm font-semibold">Account No</p>
|
<p className="text-sm font-semibold">Account No</p>
|
||||||
<span>{accountNo}</span>
|
<span>{accountNo}</span>
|
||||||
</div>
|
</div>
|
||||||
<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" />}
|
>
|
||||||
</Button>
|
{accNo ? <Clipboard /> : <ClipboardCheck color="green" />}
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 "{query}
|
Showing {payments?.data?.length} locations for "
|
||||||
|
{query}
|
||||||
"
|
"
|
||||||
</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>
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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: {
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user