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

View File

@ -3,6 +3,7 @@
import prisma from "@/lib/db";
import type { PaymentType } from "@/lib/types";
import { formatMacAddress } from "@/lib/utils";
import type { Prisma } from "@prisma/client";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { addDevicesToGroup } from "./omada-actions";
@ -38,13 +39,11 @@ type VerifyPaymentType = {
type?: "TRANSFER" | "WALLET";
};
type PaymentWithDevices = {
id: string;
devices: Array<{
name: string;
mac: string;
}>;
};
type PaymentWithDevices = Prisma.PaymentGetPayload<{
include: {
devices: true;
};
}>;
class InsufficientFundsError extends Error {
constructor() {
@ -67,6 +66,8 @@ async function processWalletPayment(
throw new InsufficientFundsError();
}
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
await prisma.$transaction([
prisma.payment.update({
where: { id: payment.id },
@ -76,7 +77,7 @@ async function processWalletPayment(
devices: {
updateMany: {
where: { paymentId: payment.id },
data: { isActive: true },
data: { isActive: true, expiryDate: expiryDate },
},
},
},
@ -107,7 +108,7 @@ async function verifyExternalPayment(
data: VerifyPaymentType,
payment: PaymentWithDevices | null,
): Promise<VerifyPaymentResponse> {
console.log('payment verify data ->', data)
console.log("payment verify data ->", data);
const response = await fetch(
"https://verifypaymentsapi.baraveli.dev/verify-payment",
{
@ -118,12 +119,14 @@ async function verifyExternalPayment(
);
const json = await response.json();
console.log(json)
console.log(json);
if (!payment) {
throw new Error("Payment verification failed or payment not found");
}
if (json.success) {
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + payment.numberOfMonths);
await prisma.payment.update({
where: { id: payment.id },
data: {
@ -132,7 +135,7 @@ async function verifyExternalPayment(
devices: {
updateMany: {
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 { Suspense } from "react";
export default async function ParentalControl({
searchParams,
}: {
@ -19,9 +16,7 @@ export default async function ParentalControl({
return (
<div>
<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">
Parental Control
</h3>
<h3 className="text-sarLinkOrange text-2xl">Parental Control</h3>
</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"
>
<Search />
</div>
<Suspense key={query} fallback={"loading...."}>
<DevicesTable parentalControl={true} searchParams={searchParams} />
<DevicesTable
parentalControl={true}
searchParams={searchParams}
/>
</Suspense>
</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 (
<div>
<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
</h3>
</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>
);
}

BIN
bun.lockb

Binary file not shown.

View File

@ -36,6 +36,11 @@ export async function ApplicationLayout({
<div className="flex items-center gap-2 ">
<SidebarTrigger className="-ml-1" />
<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 className="flex items-center gap-2">

View File

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

View File

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

View File

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

View File

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

View File

@ -13,11 +13,19 @@ import Link from "next/link";
import { auth } from "@/lib/auth";
import { cn } from "@/lib/utils";
import type { Payment, Prisma } from "@prisma/client";
import { Calendar } from "lucide-react";
import { headers } from "next/headers";
import Pagination from "./pagination";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { Separator } from "./ui/separator"
type PaymentWithDevices = Prisma.PaymentGetPayload<{
include: {
devices: true;
};
}>
export async function PaymentsTable({
searchParams,
@ -90,85 +98,143 @@ export async function PaymentsTable({
</div>
) : (
<>
<Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Duration</TableHead>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Duration</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{payments.map((payment) => (
<TableRow key={payment.id}>
<TableCell>
<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">
{new Date(payment.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
</div>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{payments.map((payment) => (
<TableRow key={payment.id}>
<TableCell>
<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">
{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 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>
</div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full">
<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>
</div>
</TableCell>
<TableCell className="font-medium" >
{payment.numberOfMonths} Months
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>
{query.length > 0 && (
<p className="text-sm text-muted-foreground">
Showing {payments.length} locations for &quot;{query}
&quot;
</p>
)}
</TableCell>
<TableCell className="font-medium">
{payment.numberOfMonths} Months
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.amount.toFixed(2)}
</span>
MVR
<TableCell className="text-muted-foreground">
{totalPayments} payments
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>
{query.length > 0 && (
<p className="text-sm text-muted-foreground">
Showing {payments.length} locations for &quot;{query}
&quot;
</p>
)}
</TableCell>
<TableCell className="text-muted-foreground">
{totalPayments} payments
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination totalPages={totalPages} currentPage={page} />
</TableFooter>
</Table>
<Pagination totalPages={totalPages} currentPage={page} />
</div>
<div className="sm:hidden block">
{payments.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} />
))}
</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}>
<DrawerTrigger asChild>
<Button onClick={() => setIsOpen(!isOpen)} variant="outline">
{walletBalance} MVR
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)} MVR
<Wallet2 />
</Button>
</DrawerTrigger>
@ -67,7 +67,7 @@ export function Wallet({
<div>
Your wallet balance is{" "}
<span className="font-semibold">
{walletBalance.toFixed(2)}
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)}
</span>{" "}
</div>
</DrawerDescription>

View File

@ -24,7 +24,7 @@
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.1",
"@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-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.61.4",
@ -35,6 +35,7 @@
"date-fns": "^4.1.0",
"jotai": "2.8.0",
"lucide-react": "^0.460.0",
"moment": "^2.30.1",
"motion": "^11.15.0",
"next": "15.1.2",
"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
# 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"

View File

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