mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-04-20 03:50:20 +00:00
refactor: add tryCatch utility for error handling, update device-related components and types, and clean up unused code in payment actions
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 13m55s
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 13m55s
This commit is contained in:
parent
dbdc1df7d5
commit
aa18484475
@ -1,31 +1,29 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
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";
|
||||||
|
|
||||||
export async function createPayment(data: PaymentType) {
|
export async function createPayment(data: PaymentType) {
|
||||||
console.log("data", data);
|
console.log("data", data);
|
||||||
const payment = await prisma.payment.create({
|
// const payment = await prisma.payment.create({
|
||||||
data: {
|
// data: {
|
||||||
amount: data.amount,
|
// amount: data.amount,
|
||||||
numberOfMonths: data.numberOfMonths,
|
// numberOfMonths: data.numberOfMonths,
|
||||||
paid: data.paid,
|
// paid: data.paid,
|
||||||
userId: data.userId,
|
// userId: data.userId,
|
||||||
devices: {
|
// devices: {
|
||||||
connect: data.deviceIds.map((id) => {
|
// connect: data.deviceIds.map((id) => {
|
||||||
return {
|
// return {
|
||||||
id,
|
// id,
|
||||||
};
|
// };
|
||||||
}),
|
// }),
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
redirect(`/payments/${payment.id}`);
|
// redirect(`/payments/${payment.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerifyPaymentType = {
|
type VerifyPaymentType = {
|
||||||
@ -38,12 +36,6 @@ type VerifyPaymentType = {
|
|||||||
type?: "TRANSFER" | "WALLET";
|
type?: "TRANSFER" | "WALLET";
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaymentWithDevices = Prisma.PaymentGetPayload<{
|
|
||||||
include: {
|
|
||||||
devices: true;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
class InsufficientFundsError extends Error {
|
class InsufficientFundsError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Insufficient funds in wallet");
|
super("Insufficient funds in wallet");
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import DevicesForPayment from '@/components/devices-for-payment'
|
import DevicesForPayment from "@/components/devices-for-payment";
|
||||||
import prisma from '@/lib/db';
|
import React from "react";
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default async function DevicesToPay() {
|
export default async function DevicesToPay() {
|
||||||
const billFormula = await prisma.billFormula.findFirst();
|
return (
|
||||||
return (
|
<div>
|
||||||
<div>
|
<DevicesForPayment />
|
||||||
<DevicesForPayment billFormula={billFormula ?? undefined} />
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,34 @@
|
|||||||
import prisma from '@/lib/db'
|
import React from "react";
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default async function DeviceDetails({ params }: {
|
export default async function DeviceDetails({
|
||||||
params: Promise<{ deviceId: string }>
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ deviceId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const deviceId = (await params)?.deviceId
|
const deviceId = (await params)?.deviceId;
|
||||||
const device = await prisma.device.findUnique({
|
|
||||||
where: {
|
|
||||||
id: deviceId,
|
|
||||||
},
|
|
||||||
|
|
||||||
})
|
return null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col justify-between items-start text-gray-500 title-bg py-4 px-2 mb-4">
|
<div className="flex flex-col justify-between items-start text-gray-500 title-bg py-4 px-2 mb-4">
|
||||||
<h3 className='text-2xl font-bold'>
|
<h3 className="text-2xl font-bold">{device?.name}</h3>
|
||||||
{device?.name}
|
<span>{device?.mac}</span>
|
||||||
</h3>
|
</div>
|
||||||
<span>{device?.mac}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="user-filters"
|
id="user-filters"
|
||||||
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 /> */}
|
||||||
{/* <Filter
|
{/* <Filter
|
||||||
options={sortfilterOptions}
|
options={sortfilterOptions}
|
||||||
defaultOption="asc"
|
defaultOption="asc"
|
||||||
queryParamKey="sortBy"
|
queryParamKey="sortBy"
|
||||||
/> */}
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
{/* <Suspense key={query} fallback={"loading...."}>
|
{/* <Suspense key={query} fallback={"loading...."}>
|
||||||
<DevicesTable searchParams={searchParams} />
|
<DevicesTable searchParams={searchParams} />
|
||||||
</Suspense> */}
|
</Suspense> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,6 @@ export default async function Devices({
|
|||||||
<h3 className="text-sarLinkOrange text-2xl">My Devices</h3>
|
<h3 className="text-sarLinkOrange text-2xl">My Devices</h3>
|
||||||
<AddDeviceDialogForm user_id={session?.user?.id} />
|
<AddDeviceDialogForm user_id={session?.user?.id} />
|
||||||
</div>
|
</div>
|
||||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="user-filters"
|
id="user-filters"
|
||||||
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"
|
||||||
|
12
next-auth.d.ts → app/next-auth.d.ts
vendored
12
next-auth.d.ts → app/next-auth.d.ts
vendored
@ -1,6 +1,5 @@
|
|||||||
import NextAuth, { DefaultSession } from "next-auth";
|
import NextAuth, { DefaultSession, type User } from "next-auth";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import type { User } from "./userTypes";
|
|
||||||
declare module "next-auth" {
|
declare module "next-auth" {
|
||||||
/**
|
/**
|
||||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||||
@ -13,6 +12,15 @@ declare module "next-auth" {
|
|||||||
image?: string | null;
|
image?: string | null;
|
||||||
user?: User & {
|
user?: User & {
|
||||||
expiry?: string;
|
expiry?: string;
|
||||||
|
id?: number;
|
||||||
|
username?: string;
|
||||||
|
user_permissions?: { id: number; name: string }[];
|
||||||
|
id_card?: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
last_login?: string;
|
||||||
|
date_joined?: string;
|
||||||
|
is_superuser?: boolean;
|
||||||
};
|
};
|
||||||
expires: ISODateString;
|
expires: ISODateString;
|
||||||
}
|
}
|
@ -34,7 +34,7 @@ export function AccountPopover() {
|
|||||||
{session.data?.user?.name}
|
{session.data?.user?.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{session.data?.user?.phoneNumber}
|
{session.data?.user?.id_card}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { headers } from "next/headers";
|
import { redirect } from "next/navigation";
|
||||||
import { AccountPopover } from "./account-popver";
|
import { AccountPopover } from "./account-popver";
|
||||||
|
|
||||||
export async function ApplicationLayout({
|
export async function ApplicationLayout({
|
||||||
@ -20,20 +20,22 @@ export async function ApplicationLayout({
|
|||||||
}: { children: React.ReactNode }) {
|
}: { children: React.ReactNode }) {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session) return redirect("/auth/signin");
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar role={"admin"} />
|
<AppSidebar />
|
||||||
{/* <DeviceCartDrawer billFormula={billFormula || null} /> */}
|
<DeviceCartDrawer />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4 z-10">
|
<header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4 z-10">
|
||||||
<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" && (
|
<div className="text-sm font-mono px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400">
|
||||||
<span 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 back {session?.user.name}
|
<span className="font-semibold">
|
||||||
|
{session?.user?.first_name} {session?.user?.last_name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -1,124 +1,120 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { deviceCartAtom } from "@/lib/atoms";
|
||||||
deviceCartAtom
|
|
||||||
} from "@/lib/atoms";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import {
|
import { MonitorSmartphone } from "lucide-react";
|
||||||
MonitorSmartphone
|
|
||||||
} from "lucide-react";
|
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
|
||||||
export function DeviceCartDrawer() {
|
export function DeviceCartDrawer() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const devices = useAtomValue(deviceCartAtom);
|
const devices = useAtomValue(deviceCartAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (pathname === "/payment" || pathname === "/devices-to-pay") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname === "/payment" || pathname === "/devices-to-pay") {
|
if (devices.length === 0) return null;
|
||||||
return null;
|
return (
|
||||||
}
|
<Button
|
||||||
|
size={"lg"}
|
||||||
|
className="bg-sarLinkOrange absolute bottom-10 w-fit z-20 left-1/2 transform -translate-x-1/2"
|
||||||
|
onClick={() => router.push("/devices-to-pay")}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<MonitorSmartphone />
|
||||||
|
Pay {devices.length > 0 && `(${devices.length})`} Device
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// <>
|
||||||
|
// <Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
// <DrawerTrigger asChild>
|
||||||
|
// <Button size={"lg"} className="bg-sarLinkOrange absolute bottom-10 w-fit z-20 left-1/2 transform -translate-x-1/2" onClick={() => setIsOpen(!isOpen)} variant="outline">
|
||||||
|
// <MonitorSmartphone />
|
||||||
|
// Pay {devices.length > 0 && `(${devices.length})`} Device
|
||||||
|
// </Button>
|
||||||
|
// </DrawerTrigger>
|
||||||
|
// <DrawerContent>
|
||||||
|
// <div className="mx-auto w-full max-w-sm">
|
||||||
|
// <DrawerHeader>
|
||||||
|
// <DrawerTitle>Selected Devices</DrawerTitle>
|
||||||
|
// <DrawerDescription>Selected devices pay.</DrawerDescription>
|
||||||
|
// </DrawerHeader>
|
||||||
|
// <div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto px-4 pb-4 gap-4">
|
||||||
|
// <pre>{JSON.stringify(isOpen, null, 2)}</pre>
|
||||||
|
// {devices.map((device) => (
|
||||||
|
// <DeviceCard key={device.id} device={device} />
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// <div className="px-4 flex flex-col gap-4">
|
||||||
|
// <NumberInput
|
||||||
|
// label="Set No of Months"
|
||||||
|
// value={months}
|
||||||
|
// onChange={(value) => setMonths(value)}
|
||||||
|
// maxAllowed={12}
|
||||||
|
// isDisabled={devices.length === 0}
|
||||||
|
// />
|
||||||
|
// {message && (
|
||||||
|
// <span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full">
|
||||||
|
// {message}
|
||||||
|
// </span>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// <DrawerFooter>
|
||||||
|
// <Button
|
||||||
|
// onClick={async () => {
|
||||||
|
// setDisabled(true);
|
||||||
|
// toast.promise(
|
||||||
|
// createPayment(data).then((result) => {
|
||||||
|
// if (result.success) {
|
||||||
|
// setDeviceCart([]);
|
||||||
|
// setMonths(1);
|
||||||
|
// setDisabled(false);
|
||||||
|
// if (isOpen) router.push(`/payments/${result.paymentId}`);
|
||||||
|
// setIsOpen(!isOpen);
|
||||||
|
// return "Payment created!";
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// {
|
||||||
|
// loading: "Processing payment...",
|
||||||
|
// success: "Payment created!",
|
||||||
|
// error: (err) => err.message || "Something went wrong.",
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
// className="w-full"
|
||||||
|
// disabled={devices.length === 0 || disabled}
|
||||||
|
// >
|
||||||
|
// {disabled ? (
|
||||||
|
// <>
|
||||||
|
// <Loader2 className="ml-2 animate-spin" />
|
||||||
|
// </>
|
||||||
|
// ) : (
|
||||||
|
// <>
|
||||||
|
// Go to payment
|
||||||
|
// <CircleDollarSign />
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
// </Button>
|
||||||
|
// <DrawerClose asChild>
|
||||||
|
// <Button variant="outline">Cancel</Button>
|
||||||
|
// </DrawerClose>
|
||||||
|
// <Button
|
||||||
|
// onClick={() => {
|
||||||
|
// setDeviceCart([]);
|
||||||
|
// setIsOpen(!isOpen);
|
||||||
|
// }}
|
||||||
|
// variant="outline"
|
||||||
|
// >
|
||||||
|
// Clear Selection
|
||||||
|
// </Button>
|
||||||
|
// </DrawerFooter>
|
||||||
|
// </div>
|
||||||
|
// </DrawerContent>
|
||||||
|
// </Drawer>
|
||||||
|
// </>
|
||||||
|
|
||||||
|
// );
|
||||||
if (devices.length === 0) return null
|
|
||||||
return <Button size={"lg"} className="bg-sarLinkOrange absolute bottom-10 w-fit z-20 left-1/2 transform -translate-x-1/2" onClick={() => router.push("/devices-to-pay")} variant="outline">
|
|
||||||
<MonitorSmartphone />
|
|
||||||
Pay {devices.length > 0 && `(${devices.length})`} Device
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
// <>
|
|
||||||
// <Drawer open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
// <DrawerTrigger asChild>
|
|
||||||
// <Button size={"lg"} className="bg-sarLinkOrange absolute bottom-10 w-fit z-20 left-1/2 transform -translate-x-1/2" onClick={() => setIsOpen(!isOpen)} variant="outline">
|
|
||||||
// <MonitorSmartphone />
|
|
||||||
// Pay {devices.length > 0 && `(${devices.length})`} Device
|
|
||||||
// </Button>
|
|
||||||
// </DrawerTrigger>
|
|
||||||
// <DrawerContent>
|
|
||||||
// <div className="mx-auto w-full max-w-sm">
|
|
||||||
// <DrawerHeader>
|
|
||||||
// <DrawerTitle>Selected Devices</DrawerTitle>
|
|
||||||
// <DrawerDescription>Selected devices pay.</DrawerDescription>
|
|
||||||
// </DrawerHeader>
|
|
||||||
// <div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto px-4 pb-4 gap-4">
|
|
||||||
// <pre>{JSON.stringify(isOpen, null, 2)}</pre>
|
|
||||||
// {devices.map((device) => (
|
|
||||||
// <DeviceCard key={device.id} device={device} />
|
|
||||||
// ))}
|
|
||||||
// </div>
|
|
||||||
// <div className="px-4 flex flex-col gap-4">
|
|
||||||
// <NumberInput
|
|
||||||
// label="Set No of Months"
|
|
||||||
// value={months}
|
|
||||||
// onChange={(value) => setMonths(value)}
|
|
||||||
// maxAllowed={12}
|
|
||||||
// isDisabled={devices.length === 0}
|
|
||||||
// />
|
|
||||||
// {message && (
|
|
||||||
// <span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full">
|
|
||||||
// {message}
|
|
||||||
// </span>
|
|
||||||
// )}
|
|
||||||
// </div>
|
|
||||||
// <DrawerFooter>
|
|
||||||
// <Button
|
|
||||||
// onClick={async () => {
|
|
||||||
// setDisabled(true);
|
|
||||||
// toast.promise(
|
|
||||||
// createPayment(data).then((result) => {
|
|
||||||
// if (result.success) {
|
|
||||||
// setDeviceCart([]);
|
|
||||||
// setMonths(1);
|
|
||||||
// setDisabled(false);
|
|
||||||
// if (isOpen) router.push(`/payments/${result.paymentId}`);
|
|
||||||
// setIsOpen(!isOpen);
|
|
||||||
// return "Payment created!";
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// {
|
|
||||||
// loading: "Processing payment...",
|
|
||||||
// success: "Payment created!",
|
|
||||||
// error: (err) => err.message || "Something went wrong.",
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// }}
|
|
||||||
// className="w-full"
|
|
||||||
// disabled={devices.length === 0 || disabled}
|
|
||||||
// >
|
|
||||||
// {disabled ? (
|
|
||||||
// <>
|
|
||||||
// <Loader2 className="ml-2 animate-spin" />
|
|
||||||
// </>
|
|
||||||
// ) : (
|
|
||||||
// <>
|
|
||||||
// Go to payment
|
|
||||||
// <CircleDollarSign />
|
|
||||||
// </>
|
|
||||||
// )}
|
|
||||||
// </Button>
|
|
||||||
// <DrawerClose asChild>
|
|
||||||
// <Button variant="outline">Cancel</Button>
|
|
||||||
// </DrawerClose>
|
|
||||||
// <Button
|
|
||||||
// onClick={() => {
|
|
||||||
// setDeviceCart([]);
|
|
||||||
// setIsOpen(!isOpen);
|
|
||||||
// }}
|
|
||||||
// variant="outline"
|
|
||||||
// >
|
|
||||||
// Clear Selection
|
|
||||||
// </Button>
|
|
||||||
// </DrawerFooter>
|
|
||||||
// </div>
|
|
||||||
// </DrawerContent>
|
|
||||||
// </Drawer>
|
|
||||||
// </>
|
|
||||||
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,107 +1,93 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createPayment } from "@/actions/payment";
|
import { createPayment } from "@/actions/payment";
|
||||||
import DeviceCard from "@/components/device-card";
|
import DeviceCard from "@/components/device-card";
|
||||||
import NumberInput from "@/components/number-input";
|
import NumberInput from "@/components/number-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { deviceCartAtom, numberOfMonths } from "@/lib/atoms";
|
||||||
deviceCartAtom,
|
|
||||||
numberOfMonths
|
|
||||||
} from "@/lib/atoms";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import type { PaymentType } from "@/lib/types";
|
import type { PaymentType } from "@/lib/types";
|
||||||
import type { BillFormula } from "@prisma/client";
|
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import {
|
import { CircleDollarSign, Loader2 } from "lucide-react";
|
||||||
CircleDollarSign,
|
import { useSession } from "next-auth/react";
|
||||||
Loader2
|
|
||||||
} from "lucide-react";
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
export default function DevicesForPayment({
|
export default function DevicesForPayment() {
|
||||||
billFormula,
|
const baseAmount = 100;
|
||||||
}: {
|
const discountPercentage = 75;
|
||||||
billFormula?: BillFormula;
|
const session = useSession();
|
||||||
}) {
|
const pathname = usePathname();
|
||||||
const baseAmount = billFormula?.baseAmount || 100;
|
const devices = useAtomValue(deviceCartAtom);
|
||||||
const discountPercentage = billFormula?.discountPercentage || 75;
|
const setDeviceCart = useSetAtom(deviceCartAtom);
|
||||||
const session = authClient.useSession();
|
const [months, setMonths] = useAtom(numberOfMonths);
|
||||||
const pathname = usePathname();
|
const [message, setMessage] = useState("");
|
||||||
const devices = useAtomValue(deviceCartAtom);
|
const [disabled, setDisabled] = useState(false);
|
||||||
const setDeviceCart = useSetAtom(deviceCartAtom);
|
const [total, setTotal] = useState(0);
|
||||||
const [months, setMonths] = useAtom(numberOfMonths);
|
useEffect(() => {
|
||||||
const [message, setMessage] = useState("");
|
if (months === 7) {
|
||||||
const [disabled, setDisabled] = useState(false);
|
setMessage("You will get 1 month free.");
|
||||||
const [total, setTotal] = useState(0);
|
} else if (months === 12) {
|
||||||
useEffect(() => {
|
setMessage("You will get 2 months free.");
|
||||||
if (months === 7) {
|
} else {
|
||||||
setMessage("You will get 1 month free.");
|
setMessage("");
|
||||||
} else if (months === 12) {
|
}
|
||||||
setMessage("You will get 2 months free.");
|
setTotal(baseAmount + (devices.length + 1 - 1) * discountPercentage);
|
||||||
} else {
|
}, [months, devices.length]);
|
||||||
setMessage("");
|
|
||||||
}
|
|
||||||
setTotal(baseAmount + ((devices.length + 1) - 1) * discountPercentage);
|
|
||||||
}, [months, devices.length, baseAmount, discountPercentage]);
|
|
||||||
|
|
||||||
if (pathname === "/payment") {
|
if (pathname === "/payment") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: PaymentType = {
|
const data: PaymentType = {
|
||||||
numberOfMonths: months,
|
numberOfMonths: months,
|
||||||
userId: session?.data?.user.id ?? "",
|
userId: session?.data?.user?.id ?? "",
|
||||||
deviceIds: devices.map((device) => device.id),
|
deviceIds: devices.map((device) => device.id),
|
||||||
amount: Number.parseFloat(total.toFixed(2)),
|
amount: Number.parseFloat(total.toFixed(2)),
|
||||||
paid: false,
|
paid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-lg mx-auto space-y-4 px-4">
|
<div className="max-w-lg mx-auto space-y-4 px-4">
|
||||||
<div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto pb-4 gap-4">
|
<div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto pb-4 gap-4">
|
||||||
{devices.map((device) => (
|
{devices.map((device) => (
|
||||||
<DeviceCard key={device.id} device={device} />
|
<DeviceCard key={device.id} device={device} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Set No of Months"
|
label="Set No of Months"
|
||||||
value={months}
|
value={months}
|
||||||
onChange={(value: number) => setMonths(value)}
|
onChange={(value: number) => setMonths(value)}
|
||||||
maxAllowed={12}
|
maxAllowed={12}
|
||||||
isDisabled={devices.length === 0}
|
isDisabled={devices.length === 0}
|
||||||
/>
|
/>
|
||||||
{message && (
|
{message && (
|
||||||
<span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full">
|
<span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full">
|
||||||
{message}
|
{message}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setDisabled(true);
|
setDisabled(true);
|
||||||
await createPayment(data);
|
await createPayment(data);
|
||||||
setDeviceCart([]);
|
setDeviceCart([]);
|
||||||
setMonths(1);
|
setMonths(1);
|
||||||
setDisabled(false);
|
setDisabled(false);
|
||||||
|
}}
|
||||||
}}
|
className="w-full"
|
||||||
className="w-full"
|
disabled={devices.length === 0 || disabled}
|
||||||
disabled={devices.length === 0 || disabled}
|
>
|
||||||
>
|
{disabled ? (
|
||||||
{disabled ? (
|
<>
|
||||||
<>
|
<Loader2 className="ml-2 animate-spin" />
|
||||||
<Loader2 className="ml-2 animate-spin" />
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
Go to payment
|
||||||
Go to payment
|
<CircleDollarSign />
|
||||||
<CircleDollarSign />
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
|
);
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { getDevices } from "@/queries/devices";
|
||||||
|
import { tryCatch } from "@/utils/tryCatch";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import ClickableRow from "./clickable-row";
|
import ClickableRow from "./clickable-row";
|
||||||
import DeviceCard from "./device-card";
|
import DeviceCard from "./device-card";
|
||||||
@ -26,86 +28,17 @@ export async function DevicesTable({
|
|||||||
parentalControl?: boolean;
|
parentalControl?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
const isAdmin = session?.user;
|
const isAdmin = session?.user?.is_superuser;
|
||||||
const query = (await searchParams)?.query || "";
|
const query = (await searchParams)?.query || "";
|
||||||
const page = (await searchParams)?.page;
|
|
||||||
const sortBy = (await searchParams)?.sortBy || "asc";
|
|
||||||
// const totalDevices = await prisma.device.count({
|
|
||||||
// where: {
|
|
||||||
// userId: isAdmin ? undefined : session?.session.userId,
|
|
||||||
// OR: [
|
|
||||||
// {
|
|
||||||
// name: {
|
|
||||||
// contains: query || "",
|
|
||||||
// mode: "insensitive",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// mac: {
|
|
||||||
// contains: query || "",
|
|
||||||
// mode: "insensitive",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// NOT: {
|
|
||||||
// payments: {
|
|
||||||
// some: {
|
|
||||||
// paid: false,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// isActive: isAdmin ? undefined : parentalControl,
|
|
||||||
// blocked: isAdmin
|
|
||||||
// ? undefined
|
|
||||||
// : parentalControl !== undefined
|
|
||||||
// ? undefined
|
|
||||||
// : false,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const totalPages = Math.ceil(totalDevices / 10);
|
const [error, devices] = await tryCatch(getDevices({ query: query }));
|
||||||
const limit = 10;
|
if (error) {
|
||||||
const offset = (Number(page) - 1) * limit || 0;
|
return <pre>{JSON.stringify(error, null, 2)}</pre>;
|
||||||
|
}
|
||||||
// const devices = await prisma.device.findMany({
|
const { meta, links, data } = devices;
|
||||||
// where: {
|
|
||||||
// userId: session?.session.userId,
|
|
||||||
// OR: [
|
|
||||||
// {
|
|
||||||
// name: {
|
|
||||||
// contains: query || "",
|
|
||||||
// mode: "insensitive",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// mac: {
|
|
||||||
// contains: query || "",
|
|
||||||
// mode: "insensitive",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// NOT: {
|
|
||||||
// payments: {
|
|
||||||
// some: {
|
|
||||||
// paid: false,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// isActive: parentalControl,
|
|
||||||
// blocked: parentalControl !== undefined ? undefined : false,
|
|
||||||
// },
|
|
||||||
|
|
||||||
// skip: offset,
|
|
||||||
// take: limit,
|
|
||||||
// orderBy: {
|
|
||||||
// name: `${sortBy}` as "asc" | "desc",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
return null;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{devices.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
|
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
|
||||||
<h3>No devices yet.</h3>
|
<h3>No devices yet.</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -122,7 +55,7 @@ export async function DevicesTable({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="overflow-scroll">
|
<TableBody className="overflow-scroll">
|
||||||
{devices.map((device) => (
|
{data.map((device) => (
|
||||||
// <TableRow key={device.id}>
|
// <TableRow key={device.id}>
|
||||||
// <TableCell>
|
// <TableCell>
|
||||||
// <div className="flex flex-col items-start">
|
// <div className="flex flex-col items-start">
|
||||||
@ -173,21 +106,25 @@ export async function DevicesTable({
|
|||||||
<TableCell colSpan={2}>
|
<TableCell colSpan={2}>
|
||||||
{query.length > 0 && (
|
{query.length > 0 && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Showing {devices.length} locations for "{query}
|
Showing {meta.total} locations for "{query}
|
||||||
"
|
"
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground">
|
<TableCell className="text-muted-foreground">
|
||||||
{totalDevices} devices
|
{meta.total} devices
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableFooter>
|
</TableFooter>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination totalPages={totalPages} currentPage={page} />
|
<Pagination
|
||||||
|
totalPages={meta.total / meta.per_page}
|
||||||
|
currentPage={meta.current_page}
|
||||||
|
/>
|
||||||
|
<pre>{JSON.stringify(meta, null, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden my-4">
|
<div className="sm:hidden my-4">
|
||||||
{devices.map((device) => (
|
{data.map((device) => (
|
||||||
<DeviceCard
|
<DeviceCard
|
||||||
parentalControl={parentalControl}
|
parentalControl={parentalControl}
|
||||||
key={device.id}
|
key={device.id}
|
||||||
|
@ -8,25 +8,15 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import prisma from "@/lib/db";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { auth } from "@/app/auth";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Prisma } from "@prisma/client";
|
|
||||||
import { Calendar } from "lucide-react";
|
import { Calendar } from "lucide-react";
|
||||||
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";
|
import { Separator } from "./ui/separator";
|
||||||
|
|
||||||
type PaymentWithDevices = Prisma.PaymentGetPayload<{
|
|
||||||
include: {
|
|
||||||
devices: true;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export async function PaymentsTable({
|
export async function PaymentsTable({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@ -36,60 +26,61 @@ export async function PaymentsTable({
|
|||||||
sortBy: string;
|
sortBy: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const session = await auth.api.getSession({
|
// const session = await auth.api.getSession({
|
||||||
headers: await headers(),
|
// headers: await headers(),
|
||||||
});
|
// });
|
||||||
const query = (await searchParams)?.query || "";
|
// const query = (await searchParams)?.query || "";
|
||||||
const page = (await searchParams)?.page;
|
// const page = (await searchParams)?.page;
|
||||||
const totalPayments = await prisma.payment.count({
|
// const totalPayments = await prisma.payment.count({
|
||||||
where: {
|
// where: {
|
||||||
userId: session?.session.userId,
|
// userId: session?.session.userId,
|
||||||
OR: [
|
// OR: [
|
||||||
{
|
// {
|
||||||
devices: {
|
// devices: {
|
||||||
every: {
|
// every: {
|
||||||
name: {
|
// name: {
|
||||||
contains: query || "",
|
// contains: query || "",
|
||||||
mode: "insensitive",
|
// mode: "insensitive",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const totalPages = Math.ceil(totalPayments / 10);
|
// const totalPages = Math.ceil(totalPayments / 10);
|
||||||
const limit = 10;
|
// const limit = 10;
|
||||||
const offset = (Number(page) - 1) * limit || 0;
|
// const offset = (Number(page) - 1) * limit || 0;
|
||||||
|
|
||||||
const payments = await prisma.payment.findMany({
|
// const payments = await prisma.payment.findMany({
|
||||||
where: {
|
// where: {
|
||||||
userId: session?.session.userId,
|
// userId: session?.session.userId,
|
||||||
OR: [
|
// OR: [
|
||||||
{
|
// {
|
||||||
devices: {
|
// devices: {
|
||||||
every: {
|
// every: {
|
||||||
name: {
|
// name: {
|
||||||
contains: query || "",
|
// contains: query || "",
|
||||||
mode: "insensitive",
|
// mode: "insensitive",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
include: {
|
// include: {
|
||||||
devices: true,
|
// devices: true,
|
||||||
},
|
// },
|
||||||
|
|
||||||
skip: offset,
|
// skip: offset,
|
||||||
take: limit,
|
// take: limit,
|
||||||
orderBy: {
|
// orderBy: {
|
||||||
createdAt: "desc",
|
// createdAt: "desc",
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
return null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{payments.length === 0 ? (
|
{payments.length === 0 ? (
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
Wallet2Icon,
|
Wallet2Icon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { authOptions } from "@/app/auth";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
@ -27,76 +28,130 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const data = {
|
type Permission = {
|
||||||
navMain: [
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Categories = {
|
||||||
|
id: string;
|
||||||
|
children: (
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
perm_identifier: string;
|
||||||
|
icon: React.JSX.Element;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
icon: React.JSX.Element;
|
||||||
|
perm_identifier?: undefined;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export async function AppSidebar({
|
||||||
|
role,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Sidebar>) {
|
||||||
|
const categories = [
|
||||||
{
|
{
|
||||||
title: "MENU",
|
id: "MENU",
|
||||||
url: "#",
|
url: "#",
|
||||||
requiredRoles: ["ADMIN", "USER"],
|
children: [
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
title: "Devices",
|
title: "Devices",
|
||||||
url: "/devices",
|
link: "/devices",
|
||||||
|
perm_identifier: "device",
|
||||||
icon: <Smartphone size={16} />,
|
icon: <Smartphone size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Payments",
|
title: "Payments",
|
||||||
url: "/payments",
|
link: "/payments",
|
||||||
icon: <CreditCard size={16} />,
|
icon: <CreditCard size={16} />,
|
||||||
|
perm_identifier: "payment",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Parental Control",
|
title: "Parental Control",
|
||||||
url: "/parental-control",
|
link: "/parental-control",
|
||||||
icon: <CreditCard size={16} />,
|
icon: <CreditCard size={16} />,
|
||||||
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Agreements",
|
title: "Agreements",
|
||||||
url: "/agreements",
|
link: "/agreements",
|
||||||
icon: <Handshake size={16} />,
|
icon: <Handshake size={16} />,
|
||||||
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Wallet",
|
title: "Wallet",
|
||||||
url: "/wallet",
|
link: "/wallet",
|
||||||
icon: <Wallet2Icon size={16} />,
|
icon: <Wallet2Icon size={16} />,
|
||||||
|
perm_identifier: "wallet",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "ADMIN CONTROL",
|
id: "ADMIN CONTROL",
|
||||||
url: "#",
|
url: "#",
|
||||||
requiredRoles: ["ADMIN"],
|
children: [
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
title: "Users",
|
title: "Users",
|
||||||
url: "/users",
|
link: "/users",
|
||||||
icon: <UsersRound size={16} />,
|
icon: <UsersRound size={16} />,
|
||||||
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "User Devices",
|
title: "User Devices",
|
||||||
url: "/user-devices",
|
link: "/user-devices",
|
||||||
icon: <MonitorSpeaker size={16} />,
|
icon: <MonitorSpeaker size={16} />,
|
||||||
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "User Payments",
|
title: "User Payments",
|
||||||
url: "/user-payments",
|
link: "/user-payments",
|
||||||
icon: <Coins size={16} />,
|
icon: <Coins size={16} />,
|
||||||
|
perm_identifier: "payment",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Price Calculator",
|
title: "Price Calculator",
|
||||||
url: "/price-calculator",
|
link: "/price-calculator",
|
||||||
icon: <Calculator size={16} />,
|
icon: <Calculator size={16} />,
|
||||||
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
const filteredCategories = categories.map((category) => {
|
||||||
|
const filteredChildren = category.children.filter((child) => {
|
||||||
|
const permIdentifier = child.perm_identifier;
|
||||||
|
return session?.user?.user_permissions?.some((permission: Permission) => {
|
||||||
|
const permissionParts = permission.name.split(" ");
|
||||||
|
const modelNameFromPermission = permissionParts.slice(2).join(" ");
|
||||||
|
return modelNameFromPermission === permIdentifier;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...category, children: filteredChildren };
|
||||||
|
});
|
||||||
|
const filteredCategoriesWithChildren = filteredCategories.filter(
|
||||||
|
(category) => category.children.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let CATEGORIES: Categories;
|
||||||
|
if (session?.user?.is_superuser) {
|
||||||
|
CATEGORIES = categories;
|
||||||
|
} else {
|
||||||
|
CATEGORIES = filteredCategoriesWithChildren;
|
||||||
|
}
|
||||||
|
|
||||||
export function AppSidebar({
|
|
||||||
role,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Sidebar> & { role: string }) {
|
|
||||||
return (
|
return (
|
||||||
<Sidebar {...props} className="z-50">
|
<Sidebar {...props} className="z-50">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@ -105,53 +160,46 @@ export function AppSidebar({
|
|||||||
</h4>
|
</h4>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent className="gap-0">
|
<SidebarContent className="gap-0">
|
||||||
{data.navMain
|
{CATEGORIES.map((item) => {
|
||||||
.filter(
|
return (
|
||||||
(item) =>
|
<Collapsible
|
||||||
!item.requiredRoles || item.requiredRoles.includes(role || ""),
|
key={item.id}
|
||||||
)
|
title={item.id}
|
||||||
.map((item) => {
|
defaultOpen
|
||||||
if (item.requiredRoles?.includes(role)) {
|
className="group/collapsible"
|
||||||
return (
|
>
|
||||||
<Collapsible
|
<SidebarGroup>
|
||||||
key={item.title}
|
<SidebarGroupLabel
|
||||||
title={item.title}
|
asChild
|
||||||
defaultOpen
|
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
className="group/collapsible"
|
|
||||||
>
|
>
|
||||||
<SidebarGroup>
|
<CollapsibleTrigger>
|
||||||
<SidebarGroupLabel
|
{item.id}{" "}
|
||||||
asChild
|
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||||
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
</CollapsibleTrigger>
|
||||||
>
|
</SidebarGroupLabel>
|
||||||
<CollapsibleTrigger>
|
<CollapsibleContent>
|
||||||
{item.title}{" "}
|
<SidebarGroupContent>
|
||||||
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
<SidebarMenu>
|
||||||
</CollapsibleTrigger>
|
{item.children.map((item) => (
|
||||||
</SidebarGroupLabel>
|
<SidebarMenuItem key={item.title}>
|
||||||
<CollapsibleContent>
|
<SidebarMenuButton className="py-6" asChild>
|
||||||
<SidebarGroupContent>
|
<Link className="text-md" href={item.link}>
|
||||||
<SidebarMenu>
|
{item.icon}
|
||||||
{item.items.map((item) => (
|
<span className="opacity-70 ml-2">
|
||||||
<SidebarMenuItem key={item.title}>
|
{item.title}
|
||||||
<SidebarMenuButton className="py-6" asChild>
|
</span>
|
||||||
<Link className="text-md" href={item.url}>
|
</Link>
|
||||||
{item.icon}
|
</SidebarMenuButton>
|
||||||
<span className="opacity-70 ml-2">
|
</SidebarMenuItem>
|
||||||
{item.title}
|
))}
|
||||||
</span>
|
</SidebarMenu>
|
||||||
</Link>
|
</SidebarGroupContent>
|
||||||
</SidebarMenuButton>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarGroup>
|
||||||
))}
|
</Collapsible>
|
||||||
</SidebarMenu>
|
);
|
||||||
</SidebarGroupContent>
|
})}
|
||||||
</CollapsibleContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AddDevice } from "@/actions/user-actions";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { addDevice } from "@/queries/devices";
|
||||||
|
import { tryCatch } from "@/utils/tryCatch";
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Loader2, Plus } from "lucide-react";
|
import { Loader2, Plus } from "lucide-react";
|
||||||
@ -23,112 +24,112 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
|
export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(2, { message: "Name is required." }),
|
||||||
|
mac_address: z
|
||||||
|
.string()
|
||||||
|
.min(2, { message: "MAC Address is required." })
|
||||||
|
.regex(
|
||||||
|
/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/,
|
||||||
|
"Please enter a valid MAC address",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
const formSchema = z.object({
|
const [disabled, setDisabled] = useState(false);
|
||||||
name: z.string().min(2, { message: "Name is required." }),
|
const [open, setOpen] = useState(false);
|
||||||
mac_address: z
|
const {
|
||||||
.string()
|
register,
|
||||||
.min(2, { message: "MAC Address is required." })
|
handleSubmit,
|
||||||
.regex(
|
formState: { errors },
|
||||||
/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/,
|
} = useForm<z.infer<typeof formSchema>>({
|
||||||
"Please enter a valid MAC address",
|
resolver: zodResolver(formSchema),
|
||||||
),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const [disabled, setDisabled] = useState(false);
|
if (!user_id) {
|
||||||
const [open, setOpen] = useState(false);
|
return null;
|
||||||
const {
|
}
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<z.infer<typeof formSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user_id) {
|
const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (data) => {
|
||||||
return null
|
console.log(data);
|
||||||
}
|
setDisabled(true);
|
||||||
|
const [error, response] = await tryCatch(
|
||||||
|
addDevice({
|
||||||
|
mac: data.mac_address,
|
||||||
|
name: data.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message || "Something went wrong.");
|
||||||
|
setDisabled(false);
|
||||||
|
} else {
|
||||||
|
setOpen(false);
|
||||||
|
setDisabled(false);
|
||||||
|
toast.success("Device successfully added!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => {
|
return (
|
||||||
console.log(data);
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
setDisabled(true)
|
<DialogTrigger asChild>
|
||||||
toast.promise(AddDevice({ mac_address: data.mac_address, name: data.name, user_id: user_id }), {
|
<Button
|
||||||
loading: 'Adding new device...',
|
className="gap-2 items-center"
|
||||||
success: () => {
|
disabled={disabled}
|
||||||
setDisabled(false)
|
variant="default"
|
||||||
setOpen((prev) => !prev)
|
>
|
||||||
return 'Device successfully added!'
|
Add Device
|
||||||
},
|
<Plus size={16} />
|
||||||
error: (error) => {
|
</Button>
|
||||||
setDisabled(false)
|
</DialogTrigger>
|
||||||
return error || 'Something went wrong.'
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
},
|
<DialogHeader>
|
||||||
})
|
<DialogTitle>New Device</DialogTitle>
|
||||||
};
|
<DialogDescription>
|
||||||
|
To add a new device, enter the device name and mac address below.
|
||||||
|
Click save when you are done.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="device_name" className="text-right">
|
||||||
|
Device Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="eg: Iphone X"
|
||||||
|
type="text"
|
||||||
|
{...register("name")}
|
||||||
|
id="device_name"
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
<span className="text-red-500 text-sm">
|
||||||
|
{errors.name?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="address" className="text-right">
|
||||||
return (
|
Mac Address
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
</Label>
|
||||||
<DialogTrigger asChild>
|
<Input
|
||||||
<Button
|
placeholder="Mac address of your device"
|
||||||
className="gap-2 items-center"
|
{...register("mac_address")}
|
||||||
disabled={disabled}
|
id="mac_address"
|
||||||
variant="default"
|
className="col-span-3"
|
||||||
>
|
/>
|
||||||
Add Device
|
<span className="text-red-500 text-sm">
|
||||||
<Plus size={16} />
|
{errors.mac_address?.message}
|
||||||
</Button>
|
</span>
|
||||||
</DialogTrigger>
|
</div>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
</div>
|
||||||
<DialogHeader>
|
</div>
|
||||||
<DialogTitle>New Device</DialogTitle>
|
<DialogFooter>
|
||||||
<DialogDescription>
|
<Button disabled={disabled} type="submit">
|
||||||
To add a new device, enter the device name and mac address below. Click save when you are done.
|
{disabled ? <Loader2 className="animate-spin" /> : "Save"}
|
||||||
</DialogDescription>
|
</Button>
|
||||||
</DialogHeader>
|
</DialogFooter>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
</form>
|
||||||
<div className="grid gap-4 py-4">
|
</DialogContent>
|
||||||
<div className="flex flex-col gap-2">
|
</Dialog>
|
||||||
<div>
|
);
|
||||||
<Label htmlFor="device_name" className="text-right">
|
|
||||||
Device Name
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="eg: Iphone X"
|
|
||||||
type="text"
|
|
||||||
{...register("name")}
|
|
||||||
id="device_name"
|
|
||||||
className="col-span-3"
|
|
||||||
/>
|
|
||||||
<span className="text-red-500 text-sm">
|
|
||||||
{errors.name?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="address" className="text-right">
|
|
||||||
Mac Address
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Mac address of your device"
|
|
||||||
{...register("mac_address")}
|
|
||||||
id="mac_address"
|
|
||||||
className="col-span-3"
|
|
||||||
/>
|
|
||||||
<span className="text-red-500 text-sm">
|
|
||||||
{errors.mac_address?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button disabled={disabled} type="submit">
|
|
||||||
{disabled ? <Loader2 className="animate-spin" /> : "Save"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -29,3 +29,24 @@ export interface Island {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mac: string;
|
||||||
|
reason_for_blocking: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
registered: boolean;
|
||||||
|
blocked: boolean;
|
||||||
|
blocked_by: string;
|
||||||
|
expiry_date: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
user: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Api400Error {
|
||||||
|
data: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
61
queries/devices.ts
Normal file
61
queries/devices.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { authOptions } from "@/app/auth";
|
||||||
|
import type { Api400Error, ApiResponse, Device } from "@/lib/backend-types";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
type GetDevicesProps = {
|
||||||
|
query?: string;
|
||||||
|
page?: number;
|
||||||
|
sortBy?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
export async function getDevices({ query }: GetDevicesProps) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
const respose = await fetch(
|
||||||
|
`${process.env.SARLINK_API_BASE_URL}/api/devices/?name=${query}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Token ${session?.apiToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = (await respose.json()) as ApiResponse<Device>;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addDevice({
|
||||||
|
name,
|
||||||
|
mac,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
mac: string;
|
||||||
|
}) {
|
||||||
|
type SingleDevice = Pick<Device, "name" | "mac">;
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.SARLINK_API_BASE_URL}/api/devices/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Token ${session?.apiToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
mac: mac,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
// Throw an error with the message from the API
|
||||||
|
throw new Error(errorData.message || "Something went wrong.");
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as SingleDevice;
|
||||||
|
revalidatePath("/devices");
|
||||||
|
return data;
|
||||||
|
}
|
8
utils/tryCatch.ts
Normal file
8
utils/tryCatch.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export async function tryCatch<T, E = Error>(promise: T | Promise<T>) {
|
||||||
|
try {
|
||||||
|
const data = await promise;
|
||||||
|
return [null, data] as const;
|
||||||
|
} catch (error) {
|
||||||
|
return [error as E, null] as const;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user