mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-21 18:42:00 +00:00
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:
parent
bdf3729b0d
commit
745f8d8fad
@ -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");
|
||||
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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 >
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 "{query}
|
||||
"
|
||||
</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 "{query}
|
||||
"
|
||||
</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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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",
|
||||
|
5
prisma/migrations/20250101160105_add/migration.sql
Normal file
5
prisma/migrations/20250101160105_add/migration.sql
Normal 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';
|
@ -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"
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user