mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-21 18:22:00 +00:00
Enhance payment processing and device management features
- Introduced wallet payment option in verifyPayment function to allow users to pay using their wallet balance. - Added new BlockDeviceDialog component for managing device blocking and unblocking actions. - Updated DeviceCard component to display device status and integrate blocking functionality. - Refactored DevicesTable to utilize DeviceCard for better UI representation of devices. - Implemented Wallet component to manage wallet balance and top-up functionality. - Enhanced API routes and Prisma schema to support wallet transactions and device blocking reasons. - Improved overall user experience with responsive design adjustments and new UI elements. These changes improve user control over payments and device management, enhancing the overall functionality of the application.
This commit is contained in:
parent
c06c4fee3f
commit
75ad431160
@ -4,6 +4,7 @@ import prisma from "@/lib/db";
|
||||
import type { PaymentType } from "@/lib/types";
|
||||
import { formatMacAddress } from "@/lib/utils";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { addDevicesToGroup } from "./omada-actions";
|
||||
|
||||
export async function createPayment(data: PaymentType) {
|
||||
@ -28,77 +29,159 @@ export async function createPayment(data: PaymentType) {
|
||||
}
|
||||
|
||||
type VerifyPaymentType = {
|
||||
userId: string;
|
||||
paymentId?: string;
|
||||
benefName: string;
|
||||
accountNo?: string;
|
||||
absAmount: string;
|
||||
time: string;
|
||||
type?: "TRANSFER" | "WALLET";
|
||||
};
|
||||
|
||||
export async function verifyPayment(data: VerifyPaymentType) {
|
||||
console.log({ data });
|
||||
try {
|
||||
const payment = await prisma.payment.findUnique({
|
||||
where: {
|
||||
id: data.paymentId,
|
||||
},
|
||||
include: {
|
||||
devices: true,
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
"https://verifypaymentsapi.baraveli.dev/verify-payment",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
type PaymentWithDevices = {
|
||||
id: string;
|
||||
devices: Array<{
|
||||
name: string;
|
||||
mac: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
class InsufficientFundsError extends Error {
|
||||
constructor() {
|
||||
super("Insufficient funds in wallet");
|
||||
this.name = "InsufficientFundsError";
|
||||
}
|
||||
}
|
||||
|
||||
async function processWalletPayment(
|
||||
user: { id: string; walletBalance: number } | null,
|
||||
payment: PaymentWithDevices | null,
|
||||
amount: number,
|
||||
) {
|
||||
if (!user || !payment) {
|
||||
throw new Error("User or payment not found");
|
||||
}
|
||||
|
||||
const walletBalance = user.walletBalance ?? 0;
|
||||
if (walletBalance < amount) {
|
||||
throw new InsufficientFundsError();
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.payment.update({
|
||||
where: { id: payment.id },
|
||||
data: {
|
||||
paid: true,
|
||||
paidAt: new Date(),
|
||||
devices: {
|
||||
updateMany: {
|
||||
where: { paymentId: payment.id },
|
||||
data: { isActive: true },
|
||||
},
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
const json = await response.json();
|
||||
console.log(json);
|
||||
const newDevices = payment?.devices.map((d) => {
|
||||
return {
|
||||
name: d.name,
|
||||
macAddress: formatMacAddress(d.mac),
|
||||
}),
|
||||
prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { walletBalance: walletBalance - amount },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
type VerifyPaymentResponse =
|
||||
| {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: boolean;
|
||||
message: string;
|
||||
transaction: {
|
||||
ref: string;
|
||||
sourceBank: string;
|
||||
trxDate: string;
|
||||
};
|
||||
};
|
||||
|
||||
async function verifyExternalPayment(
|
||||
data: VerifyPaymentType,
|
||||
payment: PaymentWithDevices | null,
|
||||
): Promise<VerifyPaymentResponse> {
|
||||
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();
|
||||
|
||||
if (!payment) {
|
||||
throw new Error("Payment verification failed or payment not found");
|
||||
}
|
||||
|
||||
if (json.success) {
|
||||
await prisma.payment.update({
|
||||
where: { id: payment.id },
|
||||
data: {
|
||||
paid: true,
|
||||
paidAt: new Date(),
|
||||
devices: {
|
||||
updateMany: {
|
||||
where: { paymentId: payment.id },
|
||||
data: { isActive: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (json.success === true) {
|
||||
await Promise.all([
|
||||
prisma.payment.update({
|
||||
where: {
|
||||
id: payment?.id,
|
||||
},
|
||||
data: {
|
||||
paid: true,
|
||||
paidAt: new Date(),
|
||||
devices: {
|
||||
updateMany: {
|
||||
where: {
|
||||
paymentId: payment?.id,
|
||||
},
|
||||
data: {
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
async function updateDevices(payment: PaymentWithDevices | null) {
|
||||
if (!payment) return;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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") {
|
||||
await processWalletPayment(user, payment, Number(data.absAmount));
|
||||
redirect("/payments");
|
||||
}
|
||||
|
||||
const res = await addDevicesToGroup({
|
||||
groupId: process.env.OMADA_GROUP_ID,
|
||||
siteId: process.env.OMADA_SITE_ID,
|
||||
newDevices: newDevices || [],
|
||||
});
|
||||
const verificationResult = await verifyExternalPayment(data, payment);
|
||||
await updateDevices(payment);
|
||||
|
||||
revalidatePath("/payment[paymentId]");
|
||||
console.log(res);
|
||||
return res;
|
||||
|
||||
return verificationResult;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error("Payment verification failed:", error);
|
||||
throw error; // Re-throw to handle at a higher level
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,10 @@
|
||||
import { DevicesTable } from "@/components/devices-table";
|
||||
import Filter from "@/components/filter";
|
||||
import Search from "@/components/search";
|
||||
import AddDeviceDialogForm from "@/components/user/add-device-dialog";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import { AArrowDown, AArrowUp } from "lucide-react";
|
||||
import React, { Suspense } from "react";
|
||||
|
||||
const sortfilterOptions = [
|
||||
{
|
||||
value: 'asc',
|
||||
label: 'Ascending',
|
||||
icon: <AArrowUp size={16} />,
|
||||
},
|
||||
{
|
||||
value: 'desc',
|
||||
label: 'Descending',
|
||||
icon: <AArrowDown size={16} />,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
export default async function Devices({
|
||||
@ -46,11 +33,7 @@ export default async function Devices({
|
||||
className=" border-b-2 pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||
>
|
||||
<Search />
|
||||
<Filter
|
||||
options={sortfilterOptions}
|
||||
defaultOption="asc"
|
||||
queryParamKey="sortBy"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<Suspense key={query} fallback={"loading...."}>
|
||||
<DevicesTable searchParams={searchParams} />
|
||||
|
@ -1,21 +1,8 @@
|
||||
import { DevicesTable } from "@/components/devices-table";
|
||||
import Filter from "@/components/filter";
|
||||
import Search from "@/components/search";
|
||||
import { AArrowDown, AArrowUp } from "lucide-react";
|
||||
import React, { Suspense } from "react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
|
||||
const sortfilterOptions = [
|
||||
{
|
||||
value: 'asc',
|
||||
label: 'Ascending',
|
||||
icon: <AArrowUp size={16} />,
|
||||
},
|
||||
{
|
||||
value: 'desc',
|
||||
label: 'Descending',
|
||||
icon: <AArrowDown size={16} />,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
export default async function ParentalControl({
|
||||
@ -42,11 +29,7 @@ export default async function ParentalControl({
|
||||
className=" border-b-2 pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||
>
|
||||
<Search />
|
||||
<Filter
|
||||
options={sortfilterOptions}
|
||||
defaultOption="asc"
|
||||
queryParamKey="sortBy"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<Suspense key={query} fallback={"loading...."}>
|
||||
<DevicesTable parentalControl={true} searchParams={searchParams} />
|
||||
|
@ -70,6 +70,11 @@ export async function GET(request: Request) {
|
||||
// Check if device has expired
|
||||
if (isAfter(currentDate, expiryDate)) {
|
||||
// Device has expired, block it
|
||||
// TODO: add a reason for blocking
|
||||
await blockDevice({
|
||||
macAddress: device.mac,
|
||||
type: "block",
|
||||
});
|
||||
await prisma.device.update({
|
||||
where: { id: device.id },
|
||||
data: {
|
||||
@ -79,10 +84,6 @@ export async function GET(request: Request) {
|
||||
});
|
||||
devicesBlocked++;
|
||||
}
|
||||
await blockDevice({
|
||||
macAddress: device.mac,
|
||||
type: "block",
|
||||
});
|
||||
}
|
||||
|
||||
if (hoursSinceLastRun < 24) {
|
||||
|
@ -11,6 +11,7 @@ export default function AddDevicesToCartButton({ device }: { device: Device }) {
|
||||
const devices = useAtomValue(deviceCartAtom)
|
||||
return (
|
||||
<Button
|
||||
className='w-full mt-2'
|
||||
disabled={devices.some((d) => d.id === device.id)}
|
||||
onClick={() => setDeviceCart((prev) => [...prev, device])}
|
||||
>
|
||||
|
@ -24,7 +24,7 @@ export function AccountPopover() {
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Button className="w-fit px-2" variant="outline">
|
||||
<UserIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { DeviceCartDrawer } from "@/components/device-cart";
|
||||
import { Wallet } from "@/components/wallet";
|
||||
|
||||
import { ModeToggle } from "@/components/theme-toggle";
|
||||
import { AppSidebar } from "@/components/ui/app-sidebar";
|
||||
|
||||
@ -20,7 +22,11 @@ export async function ApplicationLayout({
|
||||
headers: await headers()
|
||||
});
|
||||
const billFormula = await prisma.billFormula.findFirst();
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: session?.user?.id,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar role={session?.user?.role || "USER"} />
|
||||
@ -32,6 +38,7 @@ export async function ApplicationLayout({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet walletBalance={user?.walletBalance || 0} />
|
||||
<DeviceCartDrawer billFormula={billFormula || null} />
|
||||
<ModeToggle />
|
||||
<AccountPopover />
|
||||
|
@ -7,10 +7,11 @@ import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { TextShimmer } from "./ui/text-shimmer";
|
||||
|
||||
export default function BlockDeviceButton({ device }: { device: Device }) {
|
||||
export default function BlockDeviceDialog({ device }: { device: Device }) {
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
return (
|
||||
<Button
|
||||
className="w-full mt-2"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
49
components/device-card.tsx
Normal file
49
components/device-card.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import type { Device } from '@prisma/client'
|
||||
import Link from 'next/link'
|
||||
import AddDevicesToCartButton from './add-devices-to-cart-button'
|
||||
import BlockDeviceDialog from './block-device-dialog'
|
||||
import { Badge } from './ui/badge'
|
||||
|
||||
export default function DeviceCard({ device, parentalControl }: { device: Device, parentalControl?: boolean }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex text-sm shadow flex-col items-start p-2 border rounded border-dashed">
|
||||
<div className="font-semibold flex gap-2 mb-2">
|
||||
<Link
|
||||
className="font-medium hover:underline"
|
||||
href={`/devices/${device.id}`}
|
||||
>
|
||||
{device.name}
|
||||
</Link>
|
||||
<Badge variant={"outline"}>
|
||||
<span className="font-medium">
|
||||
{device.mac}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<span className="text-muted-foreground">
|
||||
Active until{" "}
|
||||
{new Date().toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
{device.blocked && (
|
||||
<div className="p-2 rounded border my-2 w-full">
|
||||
<span>Comment: </span>
|
||||
<p className="text-neutral-500">
|
||||
blocked because he was watching youtube
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!parentalControl ? (
|
||||
<AddDevicesToCartButton device={device} />
|
||||
) : (
|
||||
<BlockDeviceDialog device={device} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -79,7 +79,7 @@ export function DeviceCartDrawer({
|
||||
<DrawerTrigger asChild>
|
||||
<Button onClick={() => setIsOpen(!isOpen)} variant="outline">
|
||||
<MonitorSmartphone />
|
||||
Selected Devices {devices.length > 0 && `(${devices.length})`}
|
||||
{devices.length > 0 && `(${devices.length})`}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
@ -117,7 +117,7 @@ export function DeviceCartDrawer({
|
||||
setMonths(1)
|
||||
if (payment) {
|
||||
router.push(`/payments/${payment.id}`);
|
||||
setIsOpen(!isOpen);
|
||||
setTimeout(() => setIsOpen(!isOpen), 2000);
|
||||
} else {
|
||||
toast.error("Something went wrong.")
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ import prisma from "@/lib/db";
|
||||
import { headers } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import AddDevicesToCartButton from "./add-devices-to-cart-button";
|
||||
import BlockDeviceButton from "./block-device-button";
|
||||
import BlockDeviceButton from "./block-device-dialog";
|
||||
import DeviceCard from "./device-card";
|
||||
import Pagination from "./pagination";
|
||||
|
||||
export async function DevicesTable({
|
||||
@ -56,6 +57,7 @@ export async function DevicesTable({
|
||||
}
|
||||
},
|
||||
isActive: parentalControl ? parentalControl : undefined,
|
||||
blocked: parentalControl !== undefined ? undefined : false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -86,6 +88,8 @@ export async function DevicesTable({
|
||||
}
|
||||
},
|
||||
isActive: parentalControl,
|
||||
blocked: parentalControl !== undefined ? undefined : false,
|
||||
|
||||
},
|
||||
|
||||
skip: offset,
|
||||
@ -103,65 +107,82 @@ export async function DevicesTable({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table className="overflow-scroll">
|
||||
<TableCaption>Table of all devices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Device Name</TableHead>
|
||||
<TableHead>MAC Address</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="overflow-scroll">
|
||||
{devices.map((device) => (
|
||||
<TableRow key={device.id}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col items-start">
|
||||
<Link
|
||||
className="font-medium hover:underline"
|
||||
href={`/devices/${device.id}`}
|
||||
>
|
||||
{device.name}
|
||||
</Link>
|
||||
<span className="text-muted-foreground">
|
||||
Active until{" "}
|
||||
{new Date().toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{device.mac}</TableCell>
|
||||
<TableCell>
|
||||
{!parentalControl ? (
|
||||
<AddDevicesToCartButton device={device} />
|
||||
) : (
|
||||
<BlockDeviceButton device={device} />
|
||||
<div className="hidden sm:block">
|
||||
<Table className="overflow-scroll">
|
||||
<TableCaption>Table of all devices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Device Name</TableHead>
|
||||
<TableHead>MAC Address</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="overflow-scroll">
|
||||
{devices.map((device) => (
|
||||
<TableRow key={device.id}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col items-start">
|
||||
<Link
|
||||
className="font-medium hover:underline"
|
||||
href={`/devices/${device.id}`}
|
||||
>
|
||||
{device.name}
|
||||
</Link>
|
||||
<span className="text-muted-foreground">
|
||||
Active until{" "}
|
||||
{new Date().toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
{parentalControl && (
|
||||
<div className="p-2 rounded border my-2">
|
||||
<span>Comment: </span>
|
||||
<p className="text-neutral-500">
|
||||
blocked because he was watching youtube
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{device.mac}</TableCell>
|
||||
<TableCell>
|
||||
{!parentalControl ? (
|
||||
<AddDevicesToCartButton device={device} />
|
||||
) : (
|
||||
<BlockDeviceButton device={device} />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2}>
|
||||
{query.length > 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {devices.length} locations for "{query}
|
||||
"
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{totalDevices} devices
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2}>
|
||||
{query.length > 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {devices.length} locations for "{query}
|
||||
"
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{totalDevices} devices
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
<Pagination totalPages={totalPages} currentPage={page} />
|
||||
</TableFooter>
|
||||
</Table>
|
||||
<Pagination totalPages={totalPages} currentPage={page} />
|
||||
</div>
|
||||
<div className="sm:hidden my-4">
|
||||
{devices.map((device) => (
|
||||
<DeviceCard parentalControl={parentalControl} key={device.id} device={device} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import type { BillFormula, Prisma, User } from "@prisma/client";
|
||||
import { Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react";
|
||||
import { BadgeDollarSign, Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "./ui/button";
|
||||
@ -36,7 +36,8 @@ export default function DevicesToPay({
|
||||
const discountPercentage = billFormula?.discountPercentage ?? 75;
|
||||
// 100+(n−1)×75
|
||||
const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage;
|
||||
|
||||
const walletBalance = user?.walletBalance ?? 0;
|
||||
const isWalletPayVisible = walletBalance > total;
|
||||
|
||||
|
||||
return (
|
||||
@ -73,33 +74,59 @@ export default function DevicesToPay({
|
||||
{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
|
||||
disabled={verifying}
|
||||
onClick={async () => {
|
||||
setVerifying(true);
|
||||
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 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(total),
|
||||
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(total),
|
||||
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>
|
||||
|
@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRef, useTransition } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export default function Search({ disabled }: { disabled?: boolean }) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -31,30 +30,17 @@ export default function Search({ disabled }: { disabled?: boolean }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-2 items-center justify-between">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
className="w-fit"
|
||||
name="search"
|
||||
id="search"
|
||||
defaultValue={searchQuery ? searchQuery : ""}
|
||||
disabled={disabled}
|
||||
spellCheck={false}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
replace(pathname);
|
||||
}}
|
||||
>
|
||||
{isPending ? <Loader2 className="animate-spin" /> : "Reset"}
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder="Search..."
|
||||
type="search"
|
||||
className={cn("w-fit", isPending && "animate-pulse")}
|
||||
name="search"
|
||||
id="search"
|
||||
defaultValue={searchQuery ? searchQuery : ""}
|
||||
disabled={disabled}
|
||||
spellCheck={false}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
MonitorSpeaker,
|
||||
Smartphone,
|
||||
UsersRound,
|
||||
Wallet2Icon,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
@ -55,6 +56,11 @@ const data = {
|
||||
url: "/agreements",
|
||||
icon: <Handshake size={16} />,
|
||||
},
|
||||
{
|
||||
title: "Wallet",
|
||||
url: "/wallet",
|
||||
icon: <Wallet2Icon size={16} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
124
components/wallet.tsx
Normal file
124
components/wallet.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import {
|
||||
WalletDrawerOpenAtom,
|
||||
walletTopUpValue,
|
||||
} from "@/lib/atoms";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import type { TopupType } from "@/lib/types";
|
||||
import { useAtom, } from "jotai";
|
||||
import {
|
||||
CircleDollarSign,
|
||||
Loader2,
|
||||
Wallet2,
|
||||
} from "lucide-react";
|
||||
import { usePathname, } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import NumberInput from "./number-input";
|
||||
|
||||
|
||||
|
||||
export function Wallet({
|
||||
walletBalance,
|
||||
}: {
|
||||
walletBalance: number;
|
||||
}) {
|
||||
const session = authClient.useSession();
|
||||
const pathname = usePathname();
|
||||
const [amount, setAmount] = useAtom(walletTopUpValue);
|
||||
const [isOpen, setIsOpen] = useAtom(WalletDrawerOpenAtom);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
// const router = useRouter();
|
||||
|
||||
if (pathname === "/payment") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: TopupType = {
|
||||
userId: session?.data?.user.id ?? "",
|
||||
amount: Number.parseFloat(amount.toFixed(2)),
|
||||
paid: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button onClick={() => setIsOpen(!isOpen)} variant="outline">
|
||||
{walletBalance} MVR
|
||||
<Wallet2 />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Wallet</DrawerTitle>
|
||||
<DrawerDescription asChild>
|
||||
<div>
|
||||
Your wallet balance is{" "}
|
||||
<span className="font-semibold">
|
||||
{walletBalance.toFixed(2)}
|
||||
</span>{" "}
|
||||
</div>
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
|
||||
<div className="px-4 flex flex-col gap-4">
|
||||
<NumberInput
|
||||
label="Set amount to top up"
|
||||
value={amount}
|
||||
onChange={(value) => setAmount(value)}
|
||||
maxAllowed={5000}
|
||||
isDisabled={amount === 0}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log(data)
|
||||
setDisabled(true)
|
||||
// const payment = await createPayment(data)
|
||||
setDisabled(false)
|
||||
// setMonths(1)
|
||||
// if (payment) {
|
||||
// router.push(`/payments/${payment.id}`);
|
||||
// setIsOpen(!isOpen);
|
||||
// } else {
|
||||
// toast.error("Something went wrong.")
|
||||
// }
|
||||
}}
|
||||
className="w-full"
|
||||
disabled={amount === 0 || disabled}
|
||||
>
|
||||
{disabled ? (
|
||||
<>
|
||||
<Loader2 className="ml-2 animate-spin" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Go to payment
|
||||
<CircleDollarSign />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
@ -10,9 +10,12 @@ export const discountPercentageAtom = atom(75);
|
||||
export const numberOfDevicesAtom = atom(1);
|
||||
export const numberOfDaysAtom = atom(30);
|
||||
export const numberOfMonths = atom(1);
|
||||
export const walletTopUpValue = atom(1);
|
||||
export const formulaResultAtom = atom("");
|
||||
export const deviceCartAtom = atom<Device[]>([]);
|
||||
export const cartDrawerOpenAtom = atom(false);
|
||||
export const WalletDrawerOpenAtom = atom(false);
|
||||
|
||||
// Export the atoms with their store
|
||||
export const atoms = {
|
||||
initialPriceAtom,
|
||||
@ -23,4 +26,5 @@ export const atoms = {
|
||||
formulaResultAtom,
|
||||
deviceCartAtom,
|
||||
cartDrawerOpenAtom,
|
||||
walletTopUpValue,
|
||||
};
|
||||
|
@ -6,6 +6,12 @@ export type PaymentType = {
|
||||
paid: boolean;
|
||||
};
|
||||
|
||||
export type TopupType = {
|
||||
amount: number;
|
||||
userId: string;
|
||||
paid: boolean;
|
||||
};
|
||||
|
||||
interface IpAddress {
|
||||
ip: string;
|
||||
mask: number;
|
||||
|
2
prisma/migrations/20241224110841_add/migration.sql
Normal file
2
prisma/migrations/20241224110841_add/migration.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "user" ADD COLUMN "walletBalance" DOUBLE PRECISION NOT NULL DEFAULT 0;
|
2
prisma/migrations/20241224111353_add/migration.sql
Normal file
2
prisma/migrations/20241224111353_add/migration.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Device" ADD COLUMN "reasonForBlocking" TEXT;
|
11
prisma/migrations/20241224145258_add/migration.sql
Normal file
11
prisma/migrations/20241224145258_add/migration.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Topup" (
|
||||
"id" TEXT NOT NULL,
|
||||
"amount" DOUBLE PRECISION NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"paid" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Topup_pkey" PRIMARY KEY ("id")
|
||||
);
|
@ -35,6 +35,7 @@ model User {
|
||||
phoneNumberVerified Boolean @default(false)
|
||||
termsAccepted Boolean @default(false)
|
||||
policyAccepted Boolean @default(false)
|
||||
walletBalance Float @default(0)
|
||||
|
||||
devices Device[]
|
||||
|
||||
@ -110,19 +111,20 @@ model Island {
|
||||
}
|
||||
|
||||
model Device {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
mac String
|
||||
isActive Boolean @default(false)
|
||||
registered Boolean @default(false)
|
||||
blocked Boolean @default(false)
|
||||
expiryDate DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
User User? @relation(fields: [userId], references: [id])
|
||||
userId String?
|
||||
payment Payment? @relation(fields: [paymentId], references: [id])
|
||||
paymentId String?
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
mac String
|
||||
reasonForBlocking String?
|
||||
isActive Boolean @default(false)
|
||||
registered Boolean @default(false)
|
||||
blocked Boolean @default(false)
|
||||
expiryDate DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
User User? @relation(fields: [userId], references: [id])
|
||||
userId String?
|
||||
payment Payment? @relation(fields: [paymentId], references: [id])
|
||||
paymentId String?
|
||||
}
|
||||
|
||||
model Payment {
|
||||
@ -147,3 +149,12 @@ model BillFormula {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Topup {
|
||||
id String @id @default(cuid())
|
||||
amount Float
|
||||
userId String
|
||||
paid Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user