diff --git a/components/device-cart.tsx b/components/device-cart.tsx
index 2a00151..dd6b050 100644
--- a/components/device-cart.tsx
+++ b/components/device-cart.tsx
@@ -1,124 +1,120 @@
"use client";
import { Button } from "@/components/ui/button";
-import {
- deviceCartAtom
-} from "@/lib/atoms";
-import { authClient } from "@/lib/auth-client";
+import { deviceCartAtom } from "@/lib/atoms";
import { useAtomValue } from "jotai";
-import {
- MonitorSmartphone
-} from "lucide-react";
+import { MonitorSmartphone } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
-
export function DeviceCartDrawer() {
- const pathname = usePathname();
- const devices = useAtomValue(deviceCartAtom);
- const router = useRouter();
+ const pathname = usePathname();
+ const devices = useAtomValue(deviceCartAtom);
+ const router = useRouter();
+ if (pathname === "/payment" || pathname === "/devices-to-pay") {
+ return null;
+ }
- if (pathname === "/payment" || pathname === "/devices-to-pay") {
- return null;
- }
+ if (devices.length === 0) return null;
+ return (
+
+ );
+ // <>
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // Selected Devices
+ // Selected devices pay.
+ //
+ //
+ //
{JSON.stringify(isOpen, null, 2)}
+ // {devices.map((device) => (
+ //
+ // ))}
+ //
+ //
+ // setMonths(value)}
+ // maxAllowed={12}
+ // isDisabled={devices.length === 0}
+ // />
+ // {message && (
+ //
+ // {message}
+ //
+ // )}
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // >
-
- if (devices.length === 0) return null
- return
-
- // <>
- //
- //
- //
- //
- //
- //
- //
- // Selected Devices
- // Selected devices pay.
- //
- //
- //
{JSON.stringify(isOpen, null, 2)}
- // {devices.map((device) => (
- //
- // ))}
- //
- //
- // setMonths(value)}
- // maxAllowed={12}
- // isDisabled={devices.length === 0}
- // />
- // {message && (
- //
- // {message}
- //
- // )}
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- // >
-
- // );
+ // );
}
-
-
diff --git a/components/devices-for-payment.tsx b/components/devices-for-payment.tsx
index 1a578be..3826cfd 100644
--- a/components/devices-for-payment.tsx
+++ b/components/devices-for-payment.tsx
@@ -1,107 +1,93 @@
-
"use client";
import { createPayment } from "@/actions/payment";
import DeviceCard from "@/components/device-card";
import NumberInput from "@/components/number-input";
import { Button } from "@/components/ui/button";
-import {
- deviceCartAtom,
- numberOfMonths
-} from "@/lib/atoms";
-import { authClient } from "@/lib/auth-client";
+import { deviceCartAtom, numberOfMonths } from "@/lib/atoms";
import type { PaymentType } from "@/lib/types";
-import type { BillFormula } from "@prisma/client";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
-import {
- CircleDollarSign,
- Loader2
-} from "lucide-react";
+import { CircleDollarSign, Loader2 } from "lucide-react";
+import { useSession } from "next-auth/react";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
-export default function DevicesForPayment({
- billFormula,
-}: {
- billFormula?: BillFormula;
-}) {
- const baseAmount = billFormula?.baseAmount || 100;
- const discountPercentage = billFormula?.discountPercentage || 75;
- const session = authClient.useSession();
- const pathname = usePathname();
- const devices = useAtomValue(deviceCartAtom);
- const setDeviceCart = useSetAtom(deviceCartAtom);
- const [months, setMonths] = useAtom(numberOfMonths);
- const [message, setMessage] = useState("");
- const [disabled, setDisabled] = useState(false);
- const [total, setTotal] = useState(0);
- useEffect(() => {
- if (months === 7) {
- setMessage("You will get 1 month free.");
- } else if (months === 12) {
- setMessage("You will get 2 months free.");
- } else {
- setMessage("");
- }
- setTotal(baseAmount + ((devices.length + 1) - 1) * discountPercentage);
- }, [months, devices.length, baseAmount, discountPercentage]);
+export default function DevicesForPayment() {
+ const baseAmount = 100;
+ const discountPercentage = 75;
+ const session = useSession();
+ const pathname = usePathname();
+ const devices = useAtomValue(deviceCartAtom);
+ const setDeviceCart = useSetAtom(deviceCartAtom);
+ const [months, setMonths] = useAtom(numberOfMonths);
+ const [message, setMessage] = useState("");
+ const [disabled, setDisabled] = useState(false);
+ const [total, setTotal] = useState(0);
+ useEffect(() => {
+ if (months === 7) {
+ setMessage("You will get 1 month free.");
+ } else if (months === 12) {
+ setMessage("You will get 2 months free.");
+ } else {
+ setMessage("");
+ }
+ setTotal(baseAmount + (devices.length + 1 - 1) * discountPercentage);
+ }, [months, devices.length]);
- if (pathname === "/payment") {
- return null;
- }
+ if (pathname === "/payment") {
+ return null;
+ }
- const data: PaymentType = {
- numberOfMonths: months,
- userId: session?.data?.user.id ?? "",
- deviceIds: devices.map((device) => device.id),
- amount: Number.parseFloat(total.toFixed(2)),
- paid: false,
- };
+ const data: PaymentType = {
+ numberOfMonths: months,
+ userId: session?.data?.user?.id ?? "",
+ deviceIds: devices.map((device) => device.id),
+ amount: Number.parseFloat(total.toFixed(2)),
+ paid: false,
+ };
- return (
-
-
- {devices.map((device) => (
-
- ))}
-
-
- setMonths(value)}
- maxAllowed={12}
- isDisabled={devices.length === 0}
- />
- {message && (
-
- {message}
-
- )}
-
-
-
-
- )
+ return (
+
+
+ {devices.map((device) => (
+
+ ))}
+
+
+ setMonths(value)}
+ maxAllowed={12}
+ isDisabled={devices.length === 0}
+ />
+ {message && (
+
+ {message}
+
+ )}
+
+
+
+ );
}
diff --git a/components/devices-table.tsx b/components/devices-table.tsx
index 3c84fec..9ea73a3 100644
--- a/components/devices-table.tsx
+++ b/components/devices-table.tsx
@@ -9,6 +9,8 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
+import { getDevices } from "@/queries/devices";
+import { tryCatch } from "@/utils/tryCatch";
import { getServerSession } from "next-auth";
import ClickableRow from "./clickable-row";
import DeviceCard from "./device-card";
@@ -26,86 +28,17 @@ export async function DevicesTable({
parentalControl?: boolean;
}) {
const session = await getServerSession(authOptions);
- const isAdmin = session?.user;
+ const isAdmin = session?.user?.is_superuser;
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 limit = 10;
- const offset = (Number(page) - 1) * limit || 0;
-
- // const devices = await prisma.device.findMany({
- // 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;
+ const [error, devices] = await tryCatch(getDevices({ query: query }));
+ if (error) {
+ return
{JSON.stringify(error, null, 2)}
;
+ }
+ const { meta, links, data } = devices;
return (
- {devices.length === 0 ? (
+ {data.length === 0 ? (
No devices yet.
@@ -122,7 +55,7 @@ export async function DevicesTable({
- {devices.map((device) => (
+ {data.map((device) => (
//
//
//
@@ -173,21 +106,25 @@ export async function DevicesTable({
{query.length > 0 && (
- Showing {devices.length} locations for "{query}
+ Showing {meta.total} locations for "{query}
"
)}
- {totalDevices} devices
+ {meta.total} devices
-
+
+
{JSON.stringify(meta, null, 2)}
- {devices.map((device) => (
+ {data.map((device) => (
;
-
export async function PaymentsTable({
searchParams,
}: {
@@ -36,60 +26,61 @@ export async function PaymentsTable({
sortBy: string;
}>;
}) {
- const session = await auth.api.getSession({
- headers: await headers(),
- });
- const query = (await searchParams)?.query || "";
- const page = (await searchParams)?.page;
- const totalPayments = await prisma.payment.count({
- where: {
- userId: session?.session.userId,
- OR: [
- {
- devices: {
- every: {
- name: {
- contains: query || "",
- mode: "insensitive",
- },
- },
- },
- },
- ],
- },
- });
+ // const session = await auth.api.getSession({
+ // headers: await headers(),
+ // });
+ // const query = (await searchParams)?.query || "";
+ // const page = (await searchParams)?.page;
+ // const totalPayments = await prisma.payment.count({
+ // where: {
+ // userId: session?.session.userId,
+ // OR: [
+ // {
+ // devices: {
+ // every: {
+ // name: {
+ // contains: query || "",
+ // mode: "insensitive",
+ // },
+ // },
+ // },
+ // },
+ // ],
+ // },
+ // });
- const totalPages = Math.ceil(totalPayments / 10);
- const limit = 10;
- const offset = (Number(page) - 1) * limit || 0;
+ // const totalPages = Math.ceil(totalPayments / 10);
+ // const limit = 10;
+ // const offset = (Number(page) - 1) * limit || 0;
- const payments = await prisma.payment.findMany({
- where: {
- userId: session?.session.userId,
- OR: [
- {
- devices: {
- every: {
- name: {
- contains: query || "",
- mode: "insensitive",
- },
- },
- },
- },
- ],
- },
- include: {
- devices: true,
- },
+ // const payments = await prisma.payment.findMany({
+ // where: {
+ // userId: session?.session.userId,
+ // OR: [
+ // {
+ // devices: {
+ // every: {
+ // name: {
+ // contains: query || "",
+ // mode: "insensitive",
+ // },
+ // },
+ // },
+ // },
+ // ],
+ // },
+ // include: {
+ // devices: true,
+ // },
- skip: offset,
- take: limit,
- orderBy: {
- createdAt: "desc",
- },
- });
+ // skip: offset,
+ // take: limit,
+ // orderBy: {
+ // createdAt: "desc",
+ // },
+ // });
+ return null;
return (
{payments.length === 0 ? (
diff --git a/components/ui/app-sidebar.tsx b/components/ui/app-sidebar.tsx
index c373dda..f521d41 100644
--- a/components/ui/app-sidebar.tsx
+++ b/components/ui/app-sidebar.tsx
@@ -10,6 +10,7 @@ import {
Wallet2Icon,
} from "lucide-react";
+import { authOptions } from "@/app/auth";
import {
Collapsible,
CollapsibleContent,
@@ -27,76 +28,130 @@ import {
SidebarMenuItem,
SidebarRail,
} from "@/components/ui/sidebar";
+import { getServerSession } from "next-auth";
import Link from "next/link";
-const data = {
- navMain: [
+type Permission = {
+ 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
) {
+ const categories = [
{
- title: "MENU",
+ id: "MENU",
url: "#",
- requiredRoles: ["ADMIN", "USER"],
- items: [
+ children: [
{
title: "Devices",
- url: "/devices",
+ link: "/devices",
+ perm_identifier: "device",
icon: ,
},
{
title: "Payments",
- url: "/payments",
+ link: "/payments",
icon: ,
+ perm_identifier: "payment",
},
{
title: "Parental Control",
- url: "/parental-control",
+ link: "/parental-control",
icon: ,
+ perm_identifier: "device",
},
{
title: "Agreements",
- url: "/agreements",
+ link: "/agreements",
icon: ,
+ perm_identifier: "device",
},
{
title: "Wallet",
- url: "/wallet",
+ link: "/wallet",
icon: ,
+ perm_identifier: "wallet",
},
],
},
{
- title: "ADMIN CONTROL",
+ id: "ADMIN CONTROL",
url: "#",
- requiredRoles: ["ADMIN"],
- items: [
+ children: [
{
title: "Users",
- url: "/users",
+ link: "/users",
icon: ,
+ perm_identifier: "device",
},
{
title: "User Devices",
- url: "/user-devices",
+ link: "/user-devices",
icon: ,
+ perm_identifier: "device",
},
{
title: "User Payments",
- url: "/user-payments",
+ link: "/user-payments",
icon: ,
+ perm_identifier: "payment",
},
{
title: "Price Calculator",
- url: "/price-calculator",
+ link: "/price-calculator",
icon: ,
+ 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 & { role: string }) {
return (
@@ -105,53 +160,46 @@ export function AppSidebar({
- {data.navMain
- .filter(
- (item) =>
- !item.requiredRoles || item.requiredRoles.includes(role || ""),
- )
- .map((item) => {
- if (item.requiredRoles?.includes(role)) {
- return (
- {
+ return (
+
+
+
-
-
-
- {item.title}{" "}
-
-
-
-
-
-
- {item.items.map((item) => (
-
-
-
- {item.icon}
-
- {item.title}
-
-
-
-
- ))}
-
-
-
-
-
- );
- }
- })}
+
+ {item.id}{" "}
+
+
+
+
+
+
+ {item.children.map((item) => (
+
+
+
+ {item.icon}
+
+ {item.title}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+ })}
diff --git a/components/user/add-device-dialog.tsx b/components/user/add-device-dialog.tsx
index fb3ea56..ebdb459 100644
--- a/components/user/add-device-dialog.tsx
+++ b/components/user/add-device-dialog.tsx
@@ -1,19 +1,20 @@
"use client";
-import { AddDevice } from "@/actions/user-actions";
import { Button } from "@/components/ui/button";
import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { addDevice } from "@/queries/devices";
+import { tryCatch } from "@/utils/tryCatch";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, Plus } from "lucide-react";
@@ -23,112 +24,112 @@ import { toast } from "sonner";
import { z } from "zod";
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({
- 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 [disabled, setDisabled] = useState(false);
+ const [open, setOpen] = useState(false);
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm>({
+ resolver: zodResolver(formSchema),
+ });
- const [disabled, setDisabled] = useState(false);
- const [open, setOpen] = useState(false);
- const {
- register,
- handleSubmit,
- formState: { errors },
- } = useForm>({
- resolver: zodResolver(formSchema),
- });
+ if (!user_id) {
+ return null;
+ }
- if (!user_id) {
- return null
- }
+ const onSubmit: SubmitHandler> = async (data) => {
+ 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> = (data) => {
- console.log(data);
- setDisabled(true)
- toast.promise(AddDevice({ mac_address: data.mac_address, name: data.name, user_id: user_id }), {
- loading: 'Adding new device...',
- success: () => {
- setDisabled(false)
- setOpen((prev) => !prev)
- return 'Device successfully added!'
- },
- error: (error) => {
- setDisabled(false)
- return error || 'Something went wrong.'
- },
- })
- };
+ return (
+
+ );
}
diff --git a/lib/backend-types.ts b/lib/backend-types.ts
index 21fc409..e4b73f1 100644
--- a/lib/backend-types.ts
+++ b/lib/backend-types.ts
@@ -29,3 +29,24 @@ export interface Island {
createdAt: 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;
+ };
+}
diff --git a/queries/devices.ts b/queries/devices.ts
new file mode 100644
index 0000000..782e535
--- /dev/null
+++ b/queries/devices.ts
@@ -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;
+ return data;
+}
+
+export async function addDevice({
+ name,
+ mac,
+}: {
+ name: string;
+ mac: string;
+}) {
+ type SingleDevice = Pick;
+ 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;
+}
diff --git a/utils/tryCatch.ts b/utils/tryCatch.ts
new file mode 100644
index 0000000..77e978d
--- /dev/null
+++ b/utils/tryCatch.ts
@@ -0,0 +1,8 @@
+export async function tryCatch(promise: T | Promise) {
+ try {
+ const data = await promise;
+ return [null, data] as const;
+ } catch (error) {
+ return [error as E, null] as const;
+ }
+}