mirror of
				https://github.com/i701/sarlink-portal.git
				synced 2025-11-04 06:26:59 +00:00 
			
		
		
		
	Enhance user management and payment processing features
- Updated `package.json` to include a new script for launching Prisma Studio. - Modified `signup` function in `auth-actions.ts` to include account number in user data. - Refactored `createPayment` function in `payment.ts` to improve error handling and return structured responses. - Updated UI components in the dashboard to improve layout and responsiveness, including changes to `UserDevices` and `UserPayments` pages. - Introduced new `AdminDevicesTable` and `UsersPaymentsTable` components for better admin functionalities. - Enhanced `DeviceCartDrawer` to provide user feedback during payment processing. - Added account number input to the signup form and updated validation schema accordingly. - Updated Prisma schema to include a new `ninja_user_id` field for user management. These changes improve the overall functionality, maintainability, and user experience of the application, particularly in user management and payment processing.
This commit is contained in:
		@@ -116,6 +116,7 @@ export async function signup(_actionState: ActionState, formData: FormData) {
 | 
			
		||||
			id_card: parsedData.data.id_card,
 | 
			
		||||
			dob: new Date(parsedData.data.dob),
 | 
			
		||||
			role: "USER",
 | 
			
		||||
			accNo: parsedData.data.accNo,
 | 
			
		||||
			phoneNumber: NUMBER_WITH_COUNTRY_CODE,
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -9,24 +9,28 @@ 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,
 | 
			
		||||
					};
 | 
			
		||||
				}),
 | 
			
		||||
	try {
 | 
			
		||||
		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,
 | 
			
		||||
						};
 | 
			
		||||
					}),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
	revalidatePath("/devices");
 | 
			
		||||
	return payment;
 | 
			
		||||
		});
 | 
			
		||||
		return { success: true, paymentId: payment.id };
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error("Error creating payment:", error);
 | 
			
		||||
		return { success: false, error: "Failed to create payment" };
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type VerifyPaymentType = {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ export async function VerifyUser(userId: string) {
 | 
			
		||||
			verified: true,
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
	await CreateClient({
 | 
			
		||||
	const ninjaClient = await CreateClient({
 | 
			
		||||
		group_settings_id: "",
 | 
			
		||||
		address1: "",
 | 
			
		||||
		city: user.atoll?.name || "",
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ export default async function LoginPage() {
 | 
			
		||||
		return redirect("/devices");
 | 
			
		||||
	}
 | 
			
		||||
	return (
 | 
			
		||||
		<div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
 | 
			
		||||
		<div className="dark:bg-black w-full h-screen flex items-center justify-center font-sans">
 | 
			
		||||
			<div className="flex flex-col items-center justify-center w-full h-full ">
 | 
			
		||||
				<Image alt="Sar Link Logo" src="/logo.png" width={100} height={100} />
 | 
			
		||||
				<div className="mt-4 flex flex-col items-center justify-center">
 | 
			
		||||
 
 | 
			
		||||
@@ -29,8 +29,8 @@ export default async function SignupPage({
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
 | 
			
		||||
			<div className="flex flex-col items-center justify-center w-full h-full ">
 | 
			
		||||
		<div className="dark:bg-black w-full flex items-center justify-center font-sans">
 | 
			
		||||
			<div className="flex flex-col items-center justify-center w-full h-full py-4">
 | 
			
		||||
				<Image
 | 
			
		||||
					priority
 | 
			
		||||
					alt="Sar Link Logo"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { DevicesTable } from "@/components/devices-table";
 | 
			
		||||
import { AdminDevicesTable } from "@/components/admin/admin-devices-table";
 | 
			
		||||
import Search from "@/components/search";
 | 
			
		||||
import { Suspense } from "react";
 | 
			
		||||
 | 
			
		||||
@@ -32,7 +32,7 @@ export default async function UserDevices({
 | 
			
		||||
 | 
			
		||||
			</div>
 | 
			
		||||
			<Suspense key={query} fallback={"loading...."}>
 | 
			
		||||
				<DevicesTable parentalControl={true} searchParams={searchParams} />
 | 
			
		||||
				<AdminDevicesTable parentalControl={true} searchParams={searchParams} />
 | 
			
		||||
			</Suspense>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,20 @@
 | 
			
		||||
import { UsersPaymentsTable } from "@/components/admin/user-payments-table";
 | 
			
		||||
import { AdminAuthGuard } from "@/lib/auth-guard";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import React, { Suspense } from "react";
 | 
			
		||||
 | 
			
		||||
export default async function UserPayments() {
 | 
			
		||||
export default async function UserPayments({
 | 
			
		||||
	searchParams,
 | 
			
		||||
}: {
 | 
			
		||||
	searchParams: Promise<{
 | 
			
		||||
		query: string;
 | 
			
		||||
		page: number;
 | 
			
		||||
		sortBy: string;
 | 
			
		||||
		status: string;
 | 
			
		||||
	}>;
 | 
			
		||||
}) {
 | 
			
		||||
	await AdminAuthGuard();
 | 
			
		||||
	const query = (await searchParams)?.query || "";
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
@@ -10,6 +22,9 @@ export default async function UserPayments() {
 | 
			
		||||
					User Payments
 | 
			
		||||
				</h3>
 | 
			
		||||
			</div>
 | 
			
		||||
			<Suspense key={query} fallback={"loading...."}>
 | 
			
		||||
				<UsersPaymentsTable searchParams={searchParams} />
 | 
			
		||||
			</Suspense>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ export default function RootLayout({
 | 
			
		||||
}>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <html lang="en" suppressHydrationWarning>
 | 
			
		||||
      <body className={`${barlow.variable} antialiased font-sans`}>
 | 
			
		||||
      <body className={`${barlow.variable} antialiased font-sans bg-gray-100`}>
 | 
			
		||||
        <Provider>
 | 
			
		||||
          <NextTopLoader color="#f49d1b" showSpinner={false} zIndex={9999} />
 | 
			
		||||
          <Toaster richColors />
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										187
									
								
								components/admin/admin-devices-table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								components/admin/admin-devices-table.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
import {
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCaption,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableFooter,
 | 
			
		||||
  TableHead,
 | 
			
		||||
  TableHeader,
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table";
 | 
			
		||||
import { auth } from "@/lib/auth";
 | 
			
		||||
import prisma from "@/lib/db";
 | 
			
		||||
import { headers } from "next/headers";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import BlockDeviceDialog from "../block-device-dialog";
 | 
			
		||||
import ClickableRow from "../clickable-row";
 | 
			
		||||
import DeviceCard from "../device-card";
 | 
			
		||||
import Pagination from "../pagination";
 | 
			
		||||
 | 
			
		||||
export async function AdminDevicesTable({
 | 
			
		||||
  searchParams,
 | 
			
		||||
  parentalControl,
 | 
			
		||||
}: {
 | 
			
		||||
  searchParams: Promise<{
 | 
			
		||||
    query: string;
 | 
			
		||||
    page: number;
 | 
			
		||||
    sortBy: string;
 | 
			
		||||
  }>;
 | 
			
		||||
  parentalControl?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  const session = await auth.api.getSession({
 | 
			
		||||
    headers: await headers()
 | 
			
		||||
  })
 | 
			
		||||
  const isAdmin = session?.user.role === "ADMIN"
 | 
			
		||||
  const query = (await searchParams)?.query || "";
 | 
			
		||||
  const page = (await searchParams)?.page;
 | 
			
		||||
  const sortBy = (await searchParams)?.sortBy || "asc";
 | 
			
		||||
  const totalDevices = await prisma.device.count({
 | 
			
		||||
    where: {
 | 
			
		||||
      OR: [
 | 
			
		||||
        {
 | 
			
		||||
          name: {
 | 
			
		||||
            contains: query || "",
 | 
			
		||||
            mode: "insensitive",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          mac: {
 | 
			
		||||
            contains: query || "",
 | 
			
		||||
            mode: "insensitive",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const totalPages = Math.ceil(totalDevices / 10);
 | 
			
		||||
  const limit = 10;
 | 
			
		||||
  const offset = (Number(page) - 1) * limit || 0;
 | 
			
		||||
 | 
			
		||||
  const devices = await prisma.device.findMany({
 | 
			
		||||
    where: {
 | 
			
		||||
      OR: [
 | 
			
		||||
        {
 | 
			
		||||
          name: {
 | 
			
		||||
            contains: query || "",
 | 
			
		||||
            mode: "insensitive",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          mac: {
 | 
			
		||||
            contains: query || "",
 | 
			
		||||
            mode: "insensitive",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    skip: offset,
 | 
			
		||||
    take: limit,
 | 
			
		||||
    orderBy: {
 | 
			
		||||
      name: `${sortBy}` as "asc" | "desc",
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      {devices.length === 0 ? (
 | 
			
		||||
        <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
 | 
			
		||||
          <h3>No devices yet.</h3>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          <div className="hidden sm:block">
 | 
			
		||||
            <Table className="overflow-scroll">
 | 
			
		||||
              <TableCaption>Table of all devices.</TableCaption>
 | 
			
		||||
              <TableHeader>
 | 
			
		||||
                <TableRow>
 | 
			
		||||
                  <TableHead>Device Name</TableHead>
 | 
			
		||||
                  <TableHead>MAC Address</TableHead>
 | 
			
		||||
                  <TableHead>isActive</TableHead>
 | 
			
		||||
                  <TableHead>blocked</TableHead>
 | 
			
		||||
                  <TableHead>blockedBy</TableHead>
 | 
			
		||||
                  <TableHead>expiryDate</TableHead>
 | 
			
		||||
                </TableRow>
 | 
			
		||||
              </TableHeader>
 | 
			
		||||
              <TableBody className="overflow-scroll">
 | 
			
		||||
                {devices.map((device) => (
 | 
			
		||||
                  <TableRow key={device.id}>
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      <div className="flex flex-col items-start">
 | 
			
		||||
                        <Link
 | 
			
		||||
                          className="font-medium hover:underline"
 | 
			
		||||
                          href={`/devices/${device.id}`}
 | 
			
		||||
                        >
 | 
			
		||||
                          {device.name}
 | 
			
		||||
                        </Link>
 | 
			
		||||
                        <span className="text-muted-foreground">
 | 
			
		||||
                          Active until{" "}
 | 
			
		||||
                          {new Date().toLocaleDateString("en-US", {
 | 
			
		||||
                            month: "short",
 | 
			
		||||
                            day: "2-digit",
 | 
			
		||||
                            year: "numeric",
 | 
			
		||||
                          })}
 | 
			
		||||
                        </span>
 | 
			
		||||
                        {device.blocked && (
 | 
			
		||||
                          <div className="p-2 rounded border my-2">
 | 
			
		||||
                            <span>Comment: </span>
 | 
			
		||||
                            <p className="text-neutral-500">
 | 
			
		||||
                              blocked because he was watching youtube
 | 
			
		||||
                            </p>
 | 
			
		||||
                          </div>
 | 
			
		||||
                        )}
 | 
			
		||||
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                    <TableCell className="font-medium">{device.mac}</TableCell>
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      {device.isActive ? "Active" : "Inactive"}
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      {device.blocked ? "Blocked" : "Not Blocked"}
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      {device.blockedBy ? device.blockedBy : "Not Blocked"}
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      {new Date().toLocaleDateString("en-US", {
 | 
			
		||||
                        month: "short",
 | 
			
		||||
                        day: "2-digit",
 | 
			
		||||
                        year: "numeric",
 | 
			
		||||
                      })}
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      <BlockDeviceDialog admin={isAdmin} type={device.blocked ? "unblock" : "block"} device={device} />
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                  </TableRow>
 | 
			
		||||
                ))}
 | 
			
		||||
              </TableBody>
 | 
			
		||||
              <TableFooter>
 | 
			
		||||
                <TableRow>
 | 
			
		||||
                  <TableCell colSpan={5}>
 | 
			
		||||
                    {query.length > 0 && (
 | 
			
		||||
                      <p className="text-sm text-muted-foreground">
 | 
			
		||||
                        Showing {devices.length} locations for "{query}
 | 
			
		||||
                        "
 | 
			
		||||
                      </p>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </TableCell>
 | 
			
		||||
                  <TableCell className="text-muted-foreground">
 | 
			
		||||
                    {totalDevices} devices
 | 
			
		||||
                  </TableCell>
 | 
			
		||||
                </TableRow>
 | 
			
		||||
              </TableFooter>
 | 
			
		||||
            </Table>
 | 
			
		||||
            <Pagination totalPages={totalPages} currentPage={page} />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="sm:hidden my-4">
 | 
			
		||||
            {devices.map((device) => (
 | 
			
		||||
              <DeviceCard parentalControl={parentalControl} key={device.id} device={device} />
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										222
									
								
								components/admin/user-payments-table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								components/admin/user-payments-table.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,222 @@
 | 
			
		||||
import Pagination from "@/components/pagination";
 | 
			
		||||
import { Badge } from "@/components/ui/badge";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCaption,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableFooter,
 | 
			
		||||
  TableHead,
 | 
			
		||||
  TableHeader,
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table";
 | 
			
		||||
import prisma from "@/lib/db";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
 | 
			
		||||
export async function UsersPaymentsTable({
 | 
			
		||||
  searchParams,
 | 
			
		||||
}: {
 | 
			
		||||
  searchParams: Promise<{
 | 
			
		||||
    query: string;
 | 
			
		||||
    page: number;
 | 
			
		||||
    sortBy: string;
 | 
			
		||||
    status: string;
 | 
			
		||||
  }>;
 | 
			
		||||
}) {
 | 
			
		||||
  const query = (await searchParams)?.query || "";
 | 
			
		||||
  const page = (await searchParams)?.page;
 | 
			
		||||
  const sortBy = (await searchParams)?.sortBy || "asc";
 | 
			
		||||
  const totalPayments = await prisma.payment.count({
 | 
			
		||||
    where: {
 | 
			
		||||
      OR: [
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            name: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            phoneNumber: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            address: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            id_card: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const totalPages = Math.ceil(totalPayments / 10);
 | 
			
		||||
  const limit = 10;
 | 
			
		||||
  const offset = (Number(page) - 1) * limit || 0;
 | 
			
		||||
 | 
			
		||||
  const payments = await prisma.payment.findMany({
 | 
			
		||||
    where: {
 | 
			
		||||
      OR: [
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            name: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            phoneNumber: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            address: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          user: {
 | 
			
		||||
            id_card: {
 | 
			
		||||
              contains: query || "",
 | 
			
		||||
              mode: "insensitive",
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    include: {
 | 
			
		||||
      user: true,
 | 
			
		||||
      devices: true,
 | 
			
		||||
    },
 | 
			
		||||
    skip: offset,
 | 
			
		||||
    take: limit,
 | 
			
		||||
    orderBy: {
 | 
			
		||||
      id: `${sortBy}` as "asc" | "desc",
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // const users = await prisma.user.findMany({
 | 
			
		||||
  // 	where: {
 | 
			
		||||
  // 		role: "USER",
 | 
			
		||||
  // 	},
 | 
			
		||||
  // 	include: {
 | 
			
		||||
  // 		atoll: true,
 | 
			
		||||
  // 		island: true,
 | 
			
		||||
  // 	},
 | 
			
		||||
  // });
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      {payments.length === 0 ? (
 | 
			
		||||
        <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
 | 
			
		||||
          <h3>No Users yet.</h3>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          <Table className="overflow-scroll">
 | 
			
		||||
            <TableCaption>Table of all users.</TableCaption>
 | 
			
		||||
            <TableHeader>
 | 
			
		||||
              <TableRow>
 | 
			
		||||
                <TableHead>Name</TableHead>
 | 
			
		||||
                <TableHead>ID Card</TableHead>
 | 
			
		||||
                <TableHead>Atoll</TableHead>
 | 
			
		||||
                <TableHead>Island</TableHead>
 | 
			
		||||
                <TableHead>House Name</TableHead>
 | 
			
		||||
                <TableHead>Status</TableHead>
 | 
			
		||||
                <TableHead>Dob</TableHead>
 | 
			
		||||
                <TableHead>Phone Number</TableHead>
 | 
			
		||||
                <TableHead>Action</TableHead>
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            </TableHeader>
 | 
			
		||||
            <TableBody className="overflow-scroll">
 | 
			
		||||
              {payments.map((payment) => (
 | 
			
		||||
                <TableRow
 | 
			
		||||
                  className={`${payment.paid && "title-bg dark:bg-black"}`}
 | 
			
		||||
                  key={payment.id}
 | 
			
		||||
                >
 | 
			
		||||
                  <TableCell className="font-medium">{payment.user.name}</TableCell>
 | 
			
		||||
                  <TableCell className="font-medium">{payment.user.id_card}</TableCell>
 | 
			
		||||
                  <TableCell>{payment.user?.name}</TableCell>
 | 
			
		||||
                  <TableCell>{payment.user?.name}</TableCell>
 | 
			
		||||
                  <TableCell>{payment.id}</TableCell>
 | 
			
		||||
 | 
			
		||||
                  <TableCell>
 | 
			
		||||
                    {payment.paid ? (
 | 
			
		||||
                      <Badge
 | 
			
		||||
                        variant="outline"
 | 
			
		||||
                        className="bg-lime-100 text-black"
 | 
			
		||||
                      >
 | 
			
		||||
                        Verified
 | 
			
		||||
                      </Badge>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <Badge
 | 
			
		||||
                        variant="outline"
 | 
			
		||||
                        className="bg-yellow-100 text-black"
 | 
			
		||||
                      >
 | 
			
		||||
                        Unverified
 | 
			
		||||
                      </Badge>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </TableCell>
 | 
			
		||||
                  <TableCell>
 | 
			
		||||
                    {new Date(payment.paidAt ?? "").toLocaleDateString("en-US", {
 | 
			
		||||
                      month: "short",
 | 
			
		||||
                      day: "2-digit",
 | 
			
		||||
                      year: "numeric",
 | 
			
		||||
                    })}
 | 
			
		||||
                  </TableCell>
 | 
			
		||||
 | 
			
		||||
                  <TableCell>{payment.id}</TableCell>
 | 
			
		||||
                  <TableCell>
 | 
			
		||||
                    <Link href={`/payments/${payment.id}/verify`}>
 | 
			
		||||
                      <Button>
 | 
			
		||||
                        Details
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </TableCell>
 | 
			
		||||
                </TableRow>
 | 
			
		||||
              ))}
 | 
			
		||||
            </TableBody>
 | 
			
		||||
            <TableFooter>
 | 
			
		||||
              <TableRow>
 | 
			
		||||
                <TableCell colSpan={8}>
 | 
			
		||||
                  {query.length > 0 && (
 | 
			
		||||
                    <p className="text-sm text-muted-foreground">
 | 
			
		||||
                      Showing {payments.length} locations for "{query}
 | 
			
		||||
                      "
 | 
			
		||||
                    </p>
 | 
			
		||||
                  )}
 | 
			
		||||
                </TableCell>
 | 
			
		||||
                <TableCell className="text-muted-foreground">
 | 
			
		||||
                  {totalPayments} payments
 | 
			
		||||
                </TableCell>
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            </TableFooter>
 | 
			
		||||
          </Table>
 | 
			
		||||
          <Pagination totalPages={totalPages} currentPage={page} />
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -37,7 +37,7 @@ export async function ApplicationLayout({
 | 
			
		||||
						<SidebarTrigger className="-ml-1" />
 | 
			
		||||
						<Separator orientation="vertical" className="mr-2 h-4" />
 | 
			
		||||
						{session?.user.role === "ADMIN" && (
 | 
			
		||||
							<span className="text-sm font-mono px-2 p-1 rounded-md bg-green-500/10 text-green-900">
 | 
			
		||||
							<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 {session?.user.name}
 | 
			
		||||
							</span>
 | 
			
		||||
						)}
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
 | 
			
		||||
	return (
 | 
			
		||||
		<form
 | 
			
		||||
			action={action}
 | 
			
		||||
			className="max-w-xs mt-4 w-full bg-white dark:bg-transparent dark:border-2 shadow rounded-lg mx-auto"
 | 
			
		||||
			className="max-w-xs mt-2 w-full bg-white dark:bg-transparent dark:border-2 shadow rounded-lg mx-auto"
 | 
			
		||||
		>
 | 
			
		||||
			<div className="py-2 px-4 my-2 space-y-2">
 | 
			
		||||
				<div>
 | 
			
		||||
@@ -208,7 +208,28 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
 | 
			
		||||
						</span>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<label htmlFor="accNo" className="text-sm">
 | 
			
		||||
						Account Number
 | 
			
		||||
					</label>
 | 
			
		||||
 | 
			
		||||
					<Input
 | 
			
		||||
						className={cn(
 | 
			
		||||
							"text-base",
 | 
			
		||||
							actionState.errors?.fieldErrors.accNo && "border-2 border-red-500",
 | 
			
		||||
						)}
 | 
			
		||||
						name="accNo"
 | 
			
		||||
						type="number"
 | 
			
		||||
						disabled={isPending}
 | 
			
		||||
						defaultValue={(actionState.payload?.get("accNo") || "") as string}
 | 
			
		||||
						placeholder="Account no"
 | 
			
		||||
					/>
 | 
			
		||||
					{actionState.errors?.fieldErrors.accNo && (
 | 
			
		||||
						<span className="text-sm inline-block text-red-500">
 | 
			
		||||
							{actionState.errors?.fieldErrors.accNo}
 | 
			
		||||
						</span>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<label htmlFor="phone_number" className="text-sm">
 | 
			
		||||
						Phone Number
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,7 @@ export function DeviceCartDrawer({
 | 
			
		||||
              <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} />
 | 
			
		||||
              ))}
 | 
			
		||||
@@ -111,17 +112,24 @@ export function DeviceCartDrawer({
 | 
			
		||||
            <DrawerFooter>
 | 
			
		||||
              <Button
 | 
			
		||||
                onClick={async () => {
 | 
			
		||||
                  setDisabled(true)
 | 
			
		||||
                  const payment = await createPayment(data)
 | 
			
		||||
                  setDisabled(false)
 | 
			
		||||
                  setDeviceCart([])
 | 
			
		||||
                  setMonths(1)
 | 
			
		||||
                  if (payment) {
 | 
			
		||||
                    router.push(`/payments/${payment.id}`);
 | 
			
		||||
                    setTimeout(() => setIsOpen(!isOpen), 2000);
 | 
			
		||||
                  } else {
 | 
			
		||||
                    toast.error("Something went wrong.")
 | 
			
		||||
                  }
 | 
			
		||||
                  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}
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ export async function DevicesTable({
 | 
			
		||||
 | 
			
		||||
	const devices = await prisma.device.findMany({
 | 
			
		||||
		where: {
 | 
			
		||||
			userId: isAdmin ? undefined : session?.session.userId,
 | 
			
		||||
			userId: session?.session.userId,
 | 
			
		||||
			OR: [
 | 
			
		||||
				{
 | 
			
		||||
					name: {
 | 
			
		||||
@@ -84,10 +84,10 @@ export async function DevicesTable({
 | 
			
		||||
			NOT: {
 | 
			
		||||
				payment: {
 | 
			
		||||
					paid: false
 | 
			
		||||
				}
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			isActive: isAdmin ? undefined : parentalControl,
 | 
			
		||||
			blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false,
 | 
			
		||||
			isActive: parentalControl,
 | 
			
		||||
			blocked: parentalControl !== undefined ? undefined : false,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		skip: offset,
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,10 @@ export default function DevicesToPay({
 | 
			
		||||
              <TableCell>Total Devices</TableCell>
 | 
			
		||||
              <TableCell className="text-right text-xl">{devices?.length}</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableCell>Duration</TableCell>
 | 
			
		||||
              <TableCell className="text-right text-xl">{payment?.numberOfMonths} Months</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableBody>
 | 
			
		||||
          <TableFooter>
 | 
			
		||||
            <TableRow className="">
 | 
			
		||||
 
 | 
			
		||||
@@ -26,4 +26,5 @@ export const signUpFormSchema = z.object({
 | 
			
		||||
			required_error: "You must accept the privacy policy",
 | 
			
		||||
		})
 | 
			
		||||
		.transform((val) => val === "on"),
 | 
			
		||||
	accNo: z.string().min(2, { message: "Account number is required." }),
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,8 @@
 | 
			
		||||
		"dev": "next dev --turbopack",
 | 
			
		||||
		"build": "bunx prisma migrate deploy && bunx prisma generate && bunx prisma db push && next build",
 | 
			
		||||
		"start": "next start",
 | 
			
		||||
		"lint": "next lint"
 | 
			
		||||
		"lint": "next lint",
 | 
			
		||||
		"studio": "bunx prisma studio"
 | 
			
		||||
	},
 | 
			
		||||
	"prisma": {
 | 
			
		||||
		"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								prisma/migrations/20250106070523_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								prisma/migrations/20250106070523_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
-- AlterTable
 | 
			
		||||
ALTER TABLE "user" ADD COLUMN     "ninja_user_id" TEXT;
 | 
			
		||||
@@ -36,8 +36,8 @@ model User {
 | 
			
		||||
  termsAccepted       Boolean  @default(false)
 | 
			
		||||
  policyAccepted      Boolean  @default(false)
 | 
			
		||||
  walletBalance       Float    @default(0)
 | 
			
		||||
 | 
			
		||||
  devices Device[]
 | 
			
		||||
  ninja_user_id       String?
 | 
			
		||||
  devices             Device[]
 | 
			
		||||
 | 
			
		||||
  role     String?
 | 
			
		||||
  lang     String?
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user