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

This commit is contained in:
i701 2025-04-05 16:07:11 +05:00
parent dbdc1df7d5
commit aa18484475
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
16 changed files with 641 additions and 599 deletions

View File

@ -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");

View File

@ -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 billFormula={billFormula ?? undefined} /> <DevicesForPayment />
</div> </div>
) );
} }

View File

@ -1,22 +1,17 @@
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}
</h3>
<span>{device?.mac}</span> <span>{device?.mac}</span>
</div> </div>
@ -35,5 +30,5 @@ export default async function DeviceDetails({ params }: {
<DevicesTable searchParams={searchParams} /> <DevicesTable searchParams={searchParams} />
</Suspense> */} </Suspense> */}
</div> </div>
) );
} }

View File

@ -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"

View File

@ -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;
} }

View File

@ -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

View File

@ -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">

View File

@ -1,34 +1,32 @@
"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") { if (pathname === "/payment" || pathname === "/devices-to-pay") {
return null; return null;
} }
if (devices.length === 0) return null;
return (
if (devices.length === 0) return null <Button
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"> 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 /> <MonitorSmartphone />
Pay {devices.length > 0 && `(${devices.length})`} Device Pay {devices.length > 0 && `(${devices.length})`} Device
</Button> </Button>
);
// <> // <>
// <Drawer open={isOpen} onOpenChange={setIsOpen}> // <Drawer open={isOpen} onOpenChange={setIsOpen}>
@ -120,5 +118,3 @@ export function DeviceCartDrawer() {
// ); // );
} }

View File

@ -1,32 +1,20 @@
"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 baseAmount = billFormula?.baseAmount || 100;
const discountPercentage = billFormula?.discountPercentage || 75;
const session = authClient.useSession();
const pathname = usePathname(); const pathname = usePathname();
const devices = useAtomValue(deviceCartAtom); const devices = useAtomValue(deviceCartAtom);
const setDeviceCart = useSetAtom(deviceCartAtom); const setDeviceCart = useSetAtom(deviceCartAtom);
@ -42,8 +30,8 @@ export default function DevicesForPayment({
} else { } else {
setMessage(""); setMessage("");
} }
setTotal(baseAmount + ((devices.length + 1) - 1) * discountPercentage); setTotal(baseAmount + (devices.length + 1 - 1) * discountPercentage);
}, [months, devices.length, baseAmount, discountPercentage]); }, [months, devices.length]);
if (pathname === "/payment") { if (pathname === "/payment") {
return null; return null;
@ -51,7 +39,7 @@ export default function DevicesForPayment({
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,
@ -85,7 +73,6 @@ export default function DevicesForPayment({
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}
@ -101,7 +88,6 @@ export default function DevicesForPayment({
</> </>
)} )}
</Button> </Button>
</div> </div>
) );
} }

View File

@ -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 &quot;{query} Showing {meta.total} locations for &quot;{query}
&quot; &quot;
</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}

View File

@ -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 ? (

View File

@ -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,17 +160,11 @@ export function AppSidebar({
</h4> </h4>
</SidebarHeader> </SidebarHeader>
<SidebarContent className="gap-0"> <SidebarContent className="gap-0">
{data.navMain {CATEGORIES.map((item) => {
.filter(
(item) =>
!item.requiredRoles || item.requiredRoles.includes(role || ""),
)
.map((item) => {
if (item.requiredRoles?.includes(role)) {
return ( return (
<Collapsible <Collapsible
key={item.title} key={item.id}
title={item.title} title={item.id}
defaultOpen defaultOpen
className="group/collapsible" className="group/collapsible"
> >
@ -125,17 +174,17 @@ export function AppSidebar({
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
> >
<CollapsibleTrigger> <CollapsibleTrigger>
{item.title}{" "} {item.id}{" "}
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" /> <ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
</CollapsibleTrigger> </CollapsibleTrigger>
</SidebarGroupLabel> </SidebarGroupLabel>
<CollapsibleContent> <CollapsibleContent>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{item.items.map((item) => ( {item.children.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton className="py-6" asChild> <SidebarMenuButton className="py-6" asChild>
<Link className="text-md" href={item.url}> <Link className="text-md" href={item.link}>
{item.icon} {item.icon}
<span className="opacity-70 ml-2"> <span className="opacity-70 ml-2">
{item.title} {item.title}
@ -150,7 +199,6 @@ export function AppSidebar({
</SidebarGroup> </SidebarGroup>
</Collapsible> </Collapsible>
); );
}
})} })}
</SidebarContent> </SidebarContent>
<SidebarRail /> <SidebarRail />

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import { AddDevice } from "@/actions/user-actions";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -14,6 +13,8 @@ import {
} 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,7 +24,6 @@ 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({ const formSchema = z.object({
name: z.string().min(2, { message: "Name is required." }), name: z.string().min(2, { message: "Name is required." }),
mac_address: z mac_address: z
@ -46,28 +46,28 @@ export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
}); });
if (!user_id) { if (!user_id) {
return null return null;
} }
const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => { const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (data) => {
console.log(data); console.log(data);
setDisabled(true) setDisabled(true);
toast.promise(AddDevice({ mac_address: data.mac_address, name: data.name, user_id: user_id }), { const [error, response] = await tryCatch(
loading: 'Adding new device...', addDevice({
success: () => { mac: data.mac_address,
setDisabled(false) name: data.name,
setOpen((prev) => !prev) }),
return 'Device successfully added!' );
}, if (error) {
error: (error) => { toast.error(error.message || "Something went wrong.");
setDisabled(false) setDisabled(false);
return error || 'Something went wrong.' } else {
}, setOpen(false);
}) setDisabled(false);
toast.success("Device successfully added!");
}
}; };
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -84,7 +84,8 @@ export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
<DialogHeader> <DialogHeader>
<DialogTitle>New Device</DialogTitle> <DialogTitle>New Device</DialogTitle>
<DialogDescription> <DialogDescription>
To add a new device, enter the device name and mac address below. Click save when you are done. To add a new device, enter the device name and mac address below.
Click save when you are done.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>

View File

@ -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
View 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
View 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;
}
}