mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-22 09:02:01 +00:00
Enhance payment processing and user interaction features
- Updated createPayment function to log payment data more clearly. - Introduced verifyPayment function for validating payments via an external API. - Enhanced DevicesToPay component to include user information and payment verification functionality. - Added formatDate utility for consistent date formatting across the application. - Updated Prisma schema to include account number for users. - Refactored layout and device cart components for improved user experience and responsiveness.
This commit is contained in:
parent
40b40ad3d1
commit
36f22c0614
@ -5,7 +5,7 @@ import type { PaymentType } from "@/lib/types";
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export async function createPayment(data: PaymentType) {
|
export async function createPayment(data: PaymentType) {
|
||||||
console.log("hi", data);
|
console.log("data", data);
|
||||||
const payment = await prisma.payment.create({
|
const payment = await prisma.payment.create({
|
||||||
data: {
|
data: {
|
||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
@ -24,3 +24,48 @@ export async function createPayment(data: PaymentType) {
|
|||||||
revalidatePath("/devices");
|
revalidatePath("/devices");
|
||||||
return payment;
|
return payment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VerifyPaymentType = {
|
||||||
|
paymentId?: string;
|
||||||
|
benefName: string;
|
||||||
|
accountNo?: string;
|
||||||
|
absAmount: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function verifyPayment(data: VerifyPaymentType) {
|
||||||
|
console.log({ data });
|
||||||
|
try {
|
||||||
|
const payment = await prisma.payment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: data.paymentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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 (json.success === true) {
|
||||||
|
await prisma.payment.update({
|
||||||
|
where: {
|
||||||
|
id: payment?.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
paid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
revalidatePath("/payment[paymentId]");
|
||||||
|
return json;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
import DevicesToPay from "@/components/devices-to-pay";
|
import DevicesToPay from "@/components/devices-to-pay";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
import { hasSession } from "@/lib/auth-guard";
|
import { hasSession } from "@/lib/auth-guard";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
|
import { headers } from "next/headers";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default async function PaymentPage({
|
export default async function PaymentPage({
|
||||||
params,
|
params,
|
||||||
}: { params: Promise<{ paymentId: string }> }) {
|
}: {
|
||||||
|
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 paymentId = (await params).paymentId;
|
const paymentId = (await params).paymentId;
|
||||||
const payment = await prisma.payment.findUnique({
|
const payment = await prisma.payment.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@ -28,6 +39,7 @@ export default async function PaymentPage({
|
|||||||
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
|
<DevicesToPay
|
||||||
|
user={user || undefined}
|
||||||
billFormula={formula ?? undefined}
|
billFormula={formula ?? undefined}
|
||||||
payment={payment || undefined}
|
payment={payment || undefined}
|
||||||
/>
|
/>
|
||||||
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@ -8,37 +8,37 @@ import { Toaster } from "sonner";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import QueryProvider from "@/components/query-provider";
|
import QueryProvider from "@/components/query-provider";
|
||||||
const barlow = Barlow({
|
const barlow = Barlow({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["100", "300", "400", "500", "600", "700", "800", "900"],
|
weight: ["100", "300", "400", "500", "600", "700", "800", "900"],
|
||||||
variable: "--font-barlow",
|
variable: "--font-barlow",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SAR Link Portal",
|
title: "SAR Link Portal",
|
||||||
description: "Sarlink Portal",
|
description: "Sarlink Portal",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${barlow.variable} antialiased font-sans`}>
|
<body className={`${barlow.variable} antialiased font-sans`}>
|
||||||
<Provider>
|
<Provider>
|
||||||
<NextTopLoader showSpinner={false} zIndex={9999} />
|
<NextTopLoader color="#f49d1b" showSpinner={false} zIndex={9999} />
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<QueryProvider>{children}</QueryProvider>
|
<QueryProvider>{children}</QueryProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ export function DeviceCartDrawer({
|
|||||||
} else {
|
} else {
|
||||||
setMessage("");
|
setMessage("");
|
||||||
}
|
}
|
||||||
setTotal(baseAmount + (devices.length - 1) * discountPercentage);
|
setTotal(baseAmount + ((devices.length + 1) - 1) * discountPercentage);
|
||||||
}, [months, devices.length, baseAmount, discountPercentage]);
|
}, [months, devices.length, baseAmount, discountPercentage]);
|
||||||
|
|
||||||
if (pathname === "/payment") {
|
if (pathname === "/payment") {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
import { verifyPayment } from "@/actions/payment";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -5,35 +7,50 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table";
|
||||||
import type { BillFormula, Prisma } from "@prisma/client"
|
import { formatDate } from "@/lib/utils";
|
||||||
import React from 'react'
|
import type { BillFormula, Prisma, User } from "@prisma/client";
|
||||||
|
import { Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
type PaymentWithDevices = Prisma.PaymentGetPayload<{
|
type PaymentWithDevices = Prisma.PaymentGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
devices: true
|
devices: true;
|
||||||
}
|
};
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export default function DevicesToPay({ billFormula, payment }: { billFormula?: BillFormula, payment?: PaymentWithDevices }) {
|
export default function DevicesToPay({
|
||||||
const devices = payment?.devices
|
billFormula,
|
||||||
|
payment,
|
||||||
|
user
|
||||||
|
}: { billFormula?: BillFormula; payment?: PaymentWithDevices, user?: User }) {
|
||||||
|
const [verifying, setVerifying] = useState(false)
|
||||||
|
|
||||||
|
const devices = payment?.devices;
|
||||||
if (devices?.length === 0) {
|
if (devices?.length === 0) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
const baseAmount = billFormula?.baseAmount ?? 100
|
const baseAmount = billFormula?.baseAmount ?? 100;
|
||||||
const discountPercentage = billFormula?.discountPercentage ?? 75
|
const discountPercentage = billFormula?.discountPercentage ?? 75;
|
||||||
// 100+(n−1)×75
|
// 100+(n−1)×75
|
||||||
const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage
|
const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 font-semibold text-lg'>
|
<h3 className="title-bg my-1 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 key={device.id} className="bg-muted border rounded p-2 flex gap-2 items-center">
|
<div
|
||||||
|
key={device.id}
|
||||||
|
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">
|
||||||
@ -44,9 +61,49 @@ export default function DevicesToPay({ billFormula, payment }: { billFormula?: B
|
|||||||
))}
|
))}
|
||||||
</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>Please send the following amount to the payment address</TableCaption>
|
<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="text-green-400 bg-green-800">Payment Verified</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
disabled={verifying}
|
||||||
|
onClick={async () => {
|
||||||
|
setVerifying(true);
|
||||||
|
const res = await verifyPayment({
|
||||||
|
paymentId: payment?.id,
|
||||||
|
benefName: user?.name ?? "",
|
||||||
|
accountNo: user?.accNo ?? "",
|
||||||
|
absAmount: String(total),
|
||||||
|
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" /> : <Wallet />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</TableCaption>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Total Devices</TableCell>
|
<TableCell>Total Devices</TableCell>
|
||||||
@ -62,6 +119,44 @@ export default function DevicesToPay({ billFormula, payment }: { billFormula?: B
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
)
|
}
|
||||||
|
|
||||||
|
function AccountInfomation({
|
||||||
|
accountNo,
|
||||||
|
accName,
|
||||||
|
}: {
|
||||||
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
12
lib/utils.ts
12
lib/utils.ts
@ -4,3 +4,15 @@ import { twMerge } from "tailwind-merge";
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatDate = (date: Date): string => {
|
||||||
|
const pad = (num: number): string => num.toString().padStart(2, "0");
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = pad(date.getMonth() + 1); // Months are zero-based
|
||||||
|
const day = pad(date.getDate());
|
||||||
|
const hours = pad(date.getHours());
|
||||||
|
const minutes = pad(date.getMinutes() + 5);
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
2
prisma/migrations/20241209164451_add/migration.sql
Normal file
2
prisma/migrations/20241209164451_add/migration.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "user" ADD COLUMN "accNo" TEXT;
|
@ -20,6 +20,7 @@ model User {
|
|||||||
emailVerified Boolean @default(false)
|
emailVerified Boolean @default(false)
|
||||||
firstPaymentDone Boolean @default(false)
|
firstPaymentDone Boolean @default(false)
|
||||||
verified Boolean @default(false)
|
verified Boolean @default(false)
|
||||||
|
accNo String?
|
||||||
// island String?
|
// island String?
|
||||||
address String?
|
address String?
|
||||||
id_card String? @unique
|
id_card String? @unique
|
||||||
|
Loading…
x
Reference in New Issue
Block a user