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

This commit is contained in:
2025-04-05 23:25:17 +05:00
parent aa18484475
commit 9e2a2f430e
15 changed files with 596 additions and 423 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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