Enhance device management and user experience features

- Updated `package.json` to include the latest version of `@radix-ui/react-separator` and added `moment` for date handling.
- Modified `blockDevice` function in `omada-actions.ts` to include a `blockedBy` parameter, allowing differentiation between admin and parent actions.
- Refactored `payment.ts` to include expiry date handling for devices during payment processing.
- Improved `DevicesTable` and `ClickableRow` components to support admin functionalities and enhance device interaction.
- Updated `BlockDeviceDialog` to accept an `admin` prop, allowing for tailored blocking actions based on user role.
- Enhanced UI components for better consistency and responsiveness across the dashboard.

These changes improve the overall functionality and maintainability of the application, providing a better user experience in device management.
This commit is contained in:
i701 2025-01-01 23:48:56 +05:00
parent bdf3729b0d
commit 745f8d8fad
16 changed files with 378 additions and 213 deletions

View File

@ -112,8 +112,14 @@ export async function addDevicesToGroup({
export async function blockDevice({ export async function blockDevice({
macAddress, macAddress,
type, type,
reason reason,
}: { macAddress: string; type: "block" | "unblock", reason?: string }) { blockedBy = "PARENT",
}: {
macAddress: string;
type: "block" | "unblock";
reason?: string;
blockedBy?: "ADMIN" | "PARENT";
}) {
console.log("hello world asdasd"); console.log("hello world asdasd");
if (!macAddress) { if (!macAddress) {
throw new Error("macAddress is a required parameter"); throw new Error("macAddress is a required parameter");
@ -147,6 +153,7 @@ export async function blockDevice({
data: { data: {
reasonForBlocking: type === "block" ? reason : "", reasonForBlocking: type === "block" ? reason : "",
blocked: type === "block", blocked: type === "block",
blockedBy: blockedBy,
}, },
}); });
revalidatePath("/parental-control"); revalidatePath("/parental-control");

View File

@ -3,6 +3,7 @@
import prisma from "@/lib/db"; import prisma from "@/lib/db";
import type { PaymentType } from "@/lib/types"; import type { PaymentType } from "@/lib/types";
import { formatMacAddress } from "@/lib/utils"; import { formatMacAddress } from "@/lib/utils";
import type { Prisma } from "@prisma/client";
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"; import { addDevicesToGroup } from "./omada-actions";
@ -38,13 +39,11 @@ type VerifyPaymentType = {
type?: "TRANSFER" | "WALLET"; type?: "TRANSFER" | "WALLET";
}; };
type PaymentWithDevices = { type PaymentWithDevices = Prisma.PaymentGetPayload<{
id: string; include: {
devices: Array<{ devices: true;
name: string;
mac: string;
}>;
}; };
}>;
class InsufficientFundsError extends Error { class InsufficientFundsError extends Error {
constructor() { constructor() {
@ -67,6 +66,8 @@ async function processWalletPayment(
throw new InsufficientFundsError(); throw new InsufficientFundsError();
} }
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
await prisma.$transaction([ await prisma.$transaction([
prisma.payment.update({ prisma.payment.update({
where: { id: payment.id }, where: { id: payment.id },
@ -76,7 +77,7 @@ async function processWalletPayment(
devices: { devices: {
updateMany: { updateMany: {
where: { paymentId: payment.id }, where: { paymentId: payment.id },
data: { isActive: true }, data: { isActive: true, expiryDate: expiryDate },
}, },
}, },
}, },
@ -107,7 +108,7 @@ 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",
{ {
@ -118,12 +119,14 @@ async function verifyExternalPayment(
); );
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();
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
await prisma.payment.update({ await prisma.payment.update({
where: { id: payment.id }, where: { id: payment.id },
data: { data: {
@ -132,7 +135,7 @@ async function verifyExternalPayment(
devices: { devices: {
updateMany: { updateMany: {
where: { paymentId: payment.id }, where: { paymentId: payment.id },
data: { isActive: true }, data: { isActive: true, expiryDate: expiryDate },
}, },
}, },
}, },

View File

@ -2,9 +2,6 @@ import { DevicesTable } from "@/components/devices-table";
import Search from "@/components/search"; import Search from "@/components/search";
import { Suspense } from "react"; import { Suspense } from "react";
export default async function ParentalControl({ export default async function ParentalControl({
searchParams, searchParams,
}: { }: {
@ -19,9 +16,7 @@ export default async function ParentalControl({
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">
<h3 className="text-sarLinkOrange text-2xl"> <h3 className="text-sarLinkOrange text-2xl">Parental Control</h3>
Parental Control
</h3>
</div> </div>
<div <div
@ -29,10 +24,12 @@ export default async function ParentalControl({
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"
> >
<Search /> <Search />
</div> </div>
<Suspense key={query} fallback={"loading...."}> <Suspense key={query} fallback={"loading...."}>
<DevicesTable parentalControl={true} searchParams={searchParams} /> <DevicesTable
parentalControl={true}
searchParams={searchParams}
/>
</Suspense> </Suspense>
</div> </div>
); );

View File

@ -1,4 +1,21 @@
export default async function UserDevcies() { import { DevicesTable } from "@/components/devices-table";
import Search from "@/components/search";
import { Suspense } from "react";
export default async function UserDevices({
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
status: string;
}>;
}) {
const query = (await searchParams)?.query || "";
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">
@ -6,6 +23,17 @@ export default async function UserDevcies() {
User Devices User Devices
</h3> </h3>
</div> </div>
<div
id="user-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<Search />
</div>
<Suspense key={query} fallback={"loading...."}>
<DevicesTable parentalControl={true} searchParams={searchParams} />
</Suspense>
</div> </div>
); );
} }

BIN
bun.lockb

Binary file not shown.

View File

@ -36,6 +36,11 @@ export async function ApplicationLayout({
<div className="flex items-center gap-2 "> <div className="flex items-center gap-2 ">
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="mr-2 h-4" />
{session?.user.role === "ADMIN" && (
<span className="text-sm font-mono px-2 p-1 rounded-md bg-green-500/10 text-green-900">
Welcome back {session?.user.name}
</span>
)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -1,7 +1,7 @@
"use client" "use client";
import { blockDevice } from "@/actions/omada-actions" import { blockDevice } from "@/actions/omada-actions";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -9,71 +9,78 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod";
import type { Device, } from "@prisma/client" import type { Device } from "@prisma/client";
import { OctagonX } from "lucide-react" import { OctagonX } from "lucide-react";
import { useState } from "react" import { useState } from "react";
import { type SubmitHandler, useForm } from "react-hook-form" import { type SubmitHandler, useForm } from "react-hook-form";
import { toast } from "sonner" import { toast } from "sonner";
import { z } from "zod" import { z } from "zod";
import { Textarea } from "./ui/textarea" import { TextShimmer } from "./ui/text-shimmer";
import { TextShimmer } from "./ui/text-shimmer" import { Textarea } from "./ui/textarea";
const validationSchema = z.object({ const validationSchema = z.object({
reasonForBlocking: z.string().min(5, { message: "Reason is required" }), reasonForBlocking: z.string().min(5, { message: "Reason is required" }),
}) });
export default function BlockDeviceDialog({ device, type }: { device: Device, type: "block" | "unblock" }) { export default function BlockDeviceDialog({
const [disabled, setDisabled] = useState(false) device,
const [open, setOpen] = useState(false) type,
admin,
}: { device: Device; type: "block" | "unblock"; admin?: boolean }) {
const [disabled, setDisabled] = useState(false);
const [open, setOpen] = useState(false);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = useForm<z.infer<typeof validationSchema>>({ } = useForm<z.infer<typeof validationSchema>>({
resolver: zodResolver(validationSchema), resolver: zodResolver(validationSchema),
}) });
const onSubmit: SubmitHandler<z.infer<typeof validationSchema>> = (data) => { const onSubmit: SubmitHandler<z.infer<typeof validationSchema>> = (data) => {
setDisabled(true) setDisabled(true);
console.log(data) console.log(data);
toast.promise(blockDevice({ toast.promise(
blockDevice({
macAddress: device.mac, macAddress: device.mac,
type: type, type: type,
reason: data.reasonForBlocking, reason: data.reasonForBlocking,
blockedBy: "ADMIN",
// reason: data.reasonForBlocking, // reason: data.reasonForBlocking,
}), { }),
{
loading: "Blocking...", loading: "Blocking...",
success: () => { success: () => {
setDisabled(false) setDisabled(false);
setOpen((prev) => !prev) setOpen((prev) => !prev);
return "Blocked!" return "Blocked!";
}, },
error: (error) => { error: (error) => {
setDisabled(false) setDisabled(false);
return error || "Something went wrong" return error || "Something went wrong";
}, },
}) },
setDisabled(false) );
setDisabled(false);
} };
return ( return (
<div> <div>
{device.blocked ? ( {device.blocked ? (
<Button onClick={ <Button
() => { onClick={() => {
setDisabled(true); setDisabled(true);
toast.promise(blockDevice({ toast.promise(
blockDevice({
macAddress: device.mac, macAddress: device.mac,
type: "unblock", type: "unblock",
reason: '', reason: "",
}), { }),
{
loading: "unblockinig...", loading: "unblockinig...",
success: () => { success: () => {
setDisabled(false); setDisabled(false);
@ -83,18 +90,45 @@ export default function BlockDeviceDialog({ device, type }: { device: Device, ty
setDisabled(false); setDisabled(false);
return "Something went wrong"; return "Something went wrong";
}, },
}) },
} );
}> }}
{disabled ? ( >
<TextShimmer>
Unblocking {disabled ? <TextShimmer>Unblocking</TextShimmer> : "Unblock"}
</TextShimmer> </Button>
) : "Unblock"} ) : (
<>
{!admin ? (
<Button
variant={"destructive"}
onClick={() => {
setDisabled(true);
toast.promise(
blockDevice({
macAddress: device.mac,
type: "block",
reason: "",
blockedBy: "PARENT",
}),
{
loading: "blocking...",
success: () => {
setDisabled(false);
return "blocked!";
},
error: () => {
setDisabled(false);
return "Something went wrong";
},
},
);
}}
>
<OctagonX />
{disabled ? <TextShimmer>Blocking</TextShimmer> : "Block"}
</Button> </Button>
) : ( ) : (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button disabled={disabled} variant="destructive"> <Button disabled={disabled} variant="destructive">
@ -104,8 +138,9 @@ export default function BlockDeviceDialog({ device, type }: { device: Device, ty
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Please provide a reason for blocking this device.</DialogTitle> <DialogTitle>
Please provide a reason for blocking this device.
</DialogTitle>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
@ -113,14 +148,26 @@ export default function BlockDeviceDialog({ device, type }: { device: Device, ty
<Label htmlFor="reason" className="text-right"> <Label htmlFor="reason" className="text-right">
Reason for blocking Reason for blocking
</Label> </Label>
<Textarea rows={10} {...register("reasonForBlocking")} id="reasonForBlocking" className={cn("col-span-5", errors.reasonForBlocking && "ring-2 ring-red-500")} /> <Textarea
rows={10}
{...register("reasonForBlocking")}
id="reasonForBlocking"
className={cn(
"col-span-5",
errors.reasonForBlocking && "ring-2 ring-red-500",
)}
/>
<span className="text-sm text-red-500"> <span className="text-sm text-red-500">
{errors.reasonForBlocking?.message} {errors.reasonForBlocking?.message}
</span> </span>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant={"destructive"} disabled={disabled} type="submit"> <Button
variant={"destructive"}
disabled={disabled}
type="submit"
>
Block Block
</Button> </Button>
</DialogFooter> </DialogFooter>
@ -128,8 +175,8 @@ export default function BlockDeviceDialog({ device, type }: { device: Device, ty
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
</>
)}
</div> </div>
);
)
} }

View File

@ -10,7 +10,7 @@ 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 }: { device: Device, parentalControl?: boolean }) { export default function ClickableRow({ device, parentalControl, admin = false }: { device: Device, parentalControl?: boolean, admin?: boolean }) {
const [devices, setDeviceCart] = useAtom(deviceCartAtom) const [devices, setDeviceCart] = useAtom(deviceCartAtom)
@ -39,13 +39,13 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
</Link> </Link>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Active until{" "} Active until{" "}
{new Date().toLocaleDateString("en-US", { {new Date(device.expiryDate || "").toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
})} })}
</span> </span>
{(parentalControl && device.blocked) && ( {(device.blockedBy === "ADMIN" && device.blocked) && (
<div className="p-2 rounded border my-2"> <div className="p-2 rounded border my-2">
<span>Comment: </span> <span>Comment: </span>
<p className="text-neutral-500"> <p className="text-neutral-500">
@ -60,7 +60,7 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
{!parentalControl ? ( {!parentalControl ? (
<AddDevicesToCartButton device={device} /> <AddDevicesToCartButton device={device} />
) : ( ) : (
<BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} /> <BlockDeviceDialog admin={admin} type={device.blocked ? "unblock" : "block"} device={device} />
)} )}
</TableCell> </TableCell>
</TableRow > </TableRow >

View File

@ -53,11 +53,11 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
</span> </span>
)} )}
{device.blocked && ( {(device.blocked && device.blockedBy === "ADMIN") && (
<div className="p-2 rounded border my-2 w-full"> <div className="p-2 rounded border my-2 w-full">
<span className='uppercase text-red-500'>Blocked by admin </span> <span className='uppercase text-red-500'>Blocked by admin </span>
<p className="text-neutral-500"> <p className="text-neutral-500">
blocked because he was watching youtube {device?.reasonForBlocking}
</p> </p>
</div> </div>
)} )}
@ -67,7 +67,7 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
{!parentalControl ? ( {!parentalControl ? (
<AddDevicesToCartButton device={device} /> <AddDevicesToCartButton device={device} />
) : ( ) : (
<BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} /> <BlockDeviceDialog admin={false} type={device.blocked ? "unblock" : "block"} device={device} />
)} )}
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@ import Pagination from "./pagination";
export async function DevicesTable({ export async function DevicesTable({
searchParams, searchParams,
parentalControl parentalControl,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
query: string; query: string;
@ -29,12 +29,13 @@ export async function DevicesTable({
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: await headers() headers: await headers()
}) })
const isAdmin = session?.user.role === "ADMIN"
const query = (await searchParams)?.query || ""; const query = (await searchParams)?.query || "";
const page = (await searchParams)?.page; const page = (await searchParams)?.page;
const sortBy = (await searchParams)?.sortBy || "asc"; const sortBy = (await searchParams)?.sortBy || "asc";
const totalDevices = await prisma.device.count({ const totalDevices = await prisma.device.count({
where: { where: {
userId: session?.session.userId, userId: isAdmin ? undefined : session?.session.userId,
OR: [ OR: [
{ {
name: { name: {
@ -54,8 +55,8 @@ export async function DevicesTable({
paid: false paid: false
} }
}, },
isActive: parentalControl, isActive: isAdmin ? undefined : parentalControl,
blocked: parentalControl !== undefined ? undefined : false, blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false,
}, },
}); });
@ -65,7 +66,7 @@ export async function DevicesTable({
const devices = await prisma.device.findMany({ const devices = await prisma.device.findMany({
where: { where: {
userId: session?.session.userId, userId: isAdmin ? undefined : session?.session.userId,
OR: [ OR: [
{ {
name: { name: {
@ -85,8 +86,8 @@ export async function DevicesTable({
paid: false paid: false
} }
}, },
isActive: parentalControl, isActive: isAdmin ? undefined : parentalControl,
blocked: parentalControl !== undefined ? undefined : false, blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false,
}, },
skip: offset, skip: offset,
@ -153,7 +154,7 @@ export async function DevicesTable({
// )} // )}
// </TableCell> // </TableCell>
// </TableRow> // </TableRow>
<ClickableRow key={device.id} device={device} parentalControl={parentalControl} /> <ClickableRow admin={isAdmin} key={device.id} device={device} parentalControl={parentalControl} />
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
@ -180,7 +181,6 @@ export async function DevicesTable({
))} ))}
</div> </div>
</> </>
)} )}
</div> </div>
); );

View File

@ -13,11 +13,19 @@ import Link from "next/link";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Payment, Prisma } from "@prisma/client";
import { Calendar } from "lucide-react"; import { Calendar } from "lucide-react";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Pagination from "./pagination"; import Pagination from "./pagination";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Separator } from "./ui/separator"
type PaymentWithDevices = Prisma.PaymentGetPayload<{
include: {
devices: true;
};
}>
export async function PaymentsTable({ export async function PaymentsTable({
searchParams, searchParams,
@ -90,6 +98,7 @@ export async function PaymentsTable({
</div> </div>
) : ( ) : (
<> <>
<div className="hidden sm:block">
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption> <TableCaption>Table of all devices.</TableCaption>
<TableHeader> <TableHeader>
@ -126,7 +135,7 @@ export async function PaymentsTable({
{payment.paid ? "Paid" : "Unpaid"} {payment.paid ? "Paid" : "Unpaid"}
</Badge> </Badge>
</div> </div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full"> <div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3> <h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm"> <ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => ( {payment.devices.map((device) => (
@ -167,8 +176,65 @@ export async function PaymentsTable({
</TableFooter> </TableFooter>
</Table> </Table>
<Pagination totalPages={totalPages} currentPage={page} /> <Pagination totalPages={totalPages} currentPage={page} />
</div>
<div className="sm:hidden block">
{payments.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} />
))}
</div>
</> </>
)} )}
</div> </div>
); );
} }
function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) {
return (
<div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
<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", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
</div>
<div className="flex items-center gap-2 mt-2">
<Link className="font-medium hover:underline" href={`/payments/${payment.id}`}>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
<Badge className={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}>
{payment.paid ? "Paid" : "Unpaid"}
</Badge>
</div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => (
<li key={device.id} className="text-sm text-muted-foreground">
{device.name}
</li>
))}
</ol>
<div className="block sm:hidden">
<Separator className="my-2" />
<h3 className="text-sm font-medium">Duration</h3>
<span className="text-sm text-muted-foreground">
{payment.numberOfMonths} Months
</span>
<Separator className="my-2" />
<h3 className="text-sm font-medium">Amount</h3>
<span className="text-sm text-muted-foreground">
{payment.amount.toFixed(2)} MVR
</span>
</div>
</div>
</div>
)
}

View File

@ -55,7 +55,7 @@ export function Wallet({
<Drawer open={isOpen} onOpenChange={setIsOpen}> <Drawer open={isOpen} onOpenChange={setIsOpen}>
<DrawerTrigger asChild> <DrawerTrigger asChild>
<Button onClick={() => setIsOpen(!isOpen)} variant="outline"> <Button onClick={() => setIsOpen(!isOpen)} variant="outline">
{walletBalance} MVR {new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)} MVR
<Wallet2 /> <Wallet2 />
</Button> </Button>
</DrawerTrigger> </DrawerTrigger>
@ -67,7 +67,7 @@ export function Wallet({
<div> <div>
Your wallet balance is{" "} Your wallet balance is{" "}
<span className="font-semibold"> <span className="font-semibold">
{walletBalance.toFixed(2)} {new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)}
</span>{" "} </span>{" "}
</div> </div>
</DrawerDescription> </DrawerDescription>

View File

@ -24,7 +24,7 @@
"@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.61.4", "@tanstack/react-query": "^5.61.4",
@ -35,6 +35,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"jotai": "2.8.0", "jotai": "2.8.0",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"moment": "^2.30.1",
"motion": "^11.15.0", "motion": "^11.15.0",
"next": "15.1.2", "next": "15.1.2",
"next-themes": "^0.4.3", "next-themes": "^0.4.3",

View File

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "Blocker" AS ENUM ('ADMIN', 'PARENT');
-- AlterTable
ALTER TABLE "Device" ADD COLUMN "blockedBy" "Blocker" NOT NULL DEFAULT 'PARENT';

View File

@ -1,3 +1,3 @@
# Please do not edit this file manually # Please do not edit this file manually
# It should be added in your version-control system (i.e. Git) # It should be added in your version-control system (e.g., Git)
provider = "postgresql" provider = "postgresql"

View File

@ -110,6 +110,11 @@ model Island {
User User[] User User[]
} }
enum Blocker {
ADMIN
PARENT
}
model Device { model Device {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
@ -118,6 +123,7 @@ model Device {
isActive Boolean @default(false) isActive Boolean @default(false)
registered Boolean @default(false) registered Boolean @default(false)
blocked Boolean @default(false) blocked Boolean @default(false)
blockedBy Blocker @default(PARENT)
expiryDate DateTime? expiryDate DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt