diff --git a/actions/payment.ts b/actions/payment.ts index c208751..3132773 100644 --- a/actions/payment.ts +++ b/actions/payment.ts @@ -1,31 +1,29 @@ "use server"; -import prisma from "@/lib/db"; import type { PaymentType } from "@/lib/types"; import { formatMacAddress } from "@/lib/utils"; -import type { Prisma } from "@prisma/client"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { addDevicesToGroup } from "./omada-actions"; export async function createPayment(data: PaymentType) { console.log("data", data); - const payment = await prisma.payment.create({ - data: { - amount: data.amount, - numberOfMonths: data.numberOfMonths, - paid: data.paid, - userId: data.userId, - devices: { - connect: data.deviceIds.map((id) => { - return { - id, - }; - }), - }, - }, - }); - redirect(`/payments/${payment.id}`); + // const payment = await prisma.payment.create({ + // data: { + // amount: data.amount, + // numberOfMonths: data.numberOfMonths, + // paid: data.paid, + // userId: data.userId, + // devices: { + // connect: data.deviceIds.map((id) => { + // return { + // id, + // }; + // }), + // }, + // }, + // }); + // redirect(`/payments/${payment.id}`); } type VerifyPaymentType = { @@ -38,12 +36,6 @@ type VerifyPaymentType = { type?: "TRANSFER" | "WALLET"; }; -type PaymentWithDevices = Prisma.PaymentGetPayload<{ - include: { - devices: true; - }; -}>; - class InsufficientFundsError extends Error { constructor() { super("Insufficient funds in wallet"); diff --git a/app/(dashboard)/devices-to-pay/page.tsx b/app/(dashboard)/devices-to-pay/page.tsx index c360947..5990051 100644 --- a/app/(dashboard)/devices-to-pay/page.tsx +++ b/app/(dashboard)/devices-to-pay/page.tsx @@ -1,12 +1,10 @@ -import DevicesForPayment from '@/components/devices-for-payment' -import prisma from '@/lib/db'; -import React from 'react' +import DevicesForPayment from "@/components/devices-for-payment"; +import React from "react"; export default async function DevicesToPay() { - const billFormula = await prisma.billFormula.findFirst(); - return ( -
- -
- ) + return ( +
+ +
+ ); } diff --git a/app/(dashboard)/devices/[deviceId]/page.tsx b/app/(dashboard)/devices/[deviceId]/page.tsx index 3a8a904..37739da 100644 --- a/app/(dashboard)/devices/[deviceId]/page.tsx +++ b/app/(dashboard)/devices/[deviceId]/page.tsx @@ -1,39 +1,34 @@ -import prisma from '@/lib/db' -import React from 'react' +import React from "react"; -export default async function DeviceDetails({ params }: { - params: Promise<{ deviceId: string }> +export default async function DeviceDetails({ + params, +}: { + params: Promise<{ deviceId: string }>; }) { - const deviceId = (await params)?.deviceId - const device = await prisma.device.findUnique({ - where: { - id: deviceId, - }, + const deviceId = (await params)?.deviceId; - }) - return ( -
-
-

- {device?.name} -

- {device?.mac} -
+ return null; + return ( +
+
+

{device?.name}

+ {device?.mac} +
-
- {/* */} - {/* + {/* */} + {/* */} -
- {/* +
+ {/* */} -
- ) + + ); } diff --git a/app/(dashboard)/devices/page.tsx b/app/(dashboard)/devices/page.tsx index 25878cb..dcdd1d0 100644 --- a/app/(dashboard)/devices/page.tsx +++ b/app/(dashboard)/devices/page.tsx @@ -23,8 +23,6 @@ export default async function Devices({

My Devices

-
{JSON.stringify(session, null, 2)}
-

- {session.data?.user?.phoneNumber} + {session.data?.user?.id_card}

+ ); + // <> + // + // + // + // + // + //
+ // + // 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 ( + + + + + + + New Device + + To add a new device, enter the device name and mac address below. + Click save when you are done. + + +
+
+
+
+ + + + {errors.name?.message} + +
- - - return ( - - - - - - - New Device - - To add a new device, enter the device name and mac address below. Click save when you are done. - - - -
-
-
- - - - {errors.name?.message} - -
- -
- - - - {errors.mac_address?.message} - -
-
-
- - - - -
-
- ); +
+ + + + {errors.mac_address?.message} + +
+
+
+ + + + +
+
+ ); } 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; + } +}