mirror of
				https://github.com/i701/sarlink-portal.git
				synced 2025-11-04 06:26:59 +00:00 
			
		
		
		
	Implement new features and enhance existing components for improved user experience
- Added a new `bun.lockb` file for dependency management. - Updated `next.config.ts` to set output to "standalone" for better deployment options. - Removed `package-lock.json` to streamline package management. - Modified `package.json` to update dependencies, including `@prisma/client` and `sonner`, and adjusted build scripts for improved functionality. - Enhanced Tailwind CSS configuration to include new animations and color schemes. - Refactored various dashboard components to improve UI consistency, including adding a new `My Wallet` page and updating existing pages to use a unified styling approach. - Introduced a new `BlockDeviceDialog` component for managing device blocking with user-defined reasons. - Improved logging and error handling in payment verification and device management functions. These changes enhance the overall functionality, maintainability, and user experience of the application.
This commit is contained in:
		@@ -56,7 +56,7 @@ export async function addDevicesToGroup({
 | 
			
		||||
	try {
 | 
			
		||||
		// Fetch the existing group profiles
 | 
			
		||||
		const groupProfiles: OmadaResponse = await fetchOmadaGroupProfiles(siteId);
 | 
			
		||||
		console.log(groupProfiles);
 | 
			
		||||
		// console.log(groupProfiles);
 | 
			
		||||
		// Find the group profile with the specified groupId
 | 
			
		||||
		const groupProfile: GroupProfile | undefined =
 | 
			
		||||
			groupProfiles.result.data.find((profile) => profile.groupId === groupId);
 | 
			
		||||
@@ -70,7 +70,7 @@ export async function addDevicesToGroup({
 | 
			
		||||
			...(groupProfile.macAddressList || []),
 | 
			
		||||
			...newDevices,
 | 
			
		||||
		];
 | 
			
		||||
		console.log({ updatedMacAddressList });
 | 
			
		||||
		// console.log({ updatedMacAddressList });
 | 
			
		||||
		// Prepare the request payload
 | 
			
		||||
		const requestBody = {
 | 
			
		||||
			name: groupProfile.name,
 | 
			
		||||
@@ -112,7 +112,8 @@ export async function addDevicesToGroup({
 | 
			
		||||
export async function blockDevice({
 | 
			
		||||
	macAddress,
 | 
			
		||||
	type,
 | 
			
		||||
}: { macAddress: string; type: "block" | "unblock" }) {
 | 
			
		||||
	reason
 | 
			
		||||
}: { macAddress: string; type: "block" | "unblock", reason?: string }) {
 | 
			
		||||
	console.log("hello world asdasd");
 | 
			
		||||
	if (!macAddress) {
 | 
			
		||||
		throw new Error("macAddress is a required parameter");
 | 
			
		||||
@@ -144,6 +145,7 @@ export async function blockDevice({
 | 
			
		||||
				id: device?.id,
 | 
			
		||||
			},
 | 
			
		||||
			data: {
 | 
			
		||||
				reasonForBlocking: type === "block" ? reason : "",
 | 
			
		||||
				blocked: type === "block",
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -107,6 +107,7 @@ async function verifyExternalPayment(
 | 
			
		||||
	data: VerifyPaymentType,
 | 
			
		||||
	payment: PaymentWithDevices | null,
 | 
			
		||||
): Promise<VerifyPaymentResponse> {
 | 
			
		||||
	console.log('payment verify data ->', data)
 | 
			
		||||
	const response = await fetch(
 | 
			
		||||
		"https://verifypaymentsapi.baraveli.dev/verify-payment",
 | 
			
		||||
		{
 | 
			
		||||
@@ -117,7 +118,7 @@ async function verifyExternalPayment(
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	const json = await response.json();
 | 
			
		||||
 | 
			
		||||
	console.log(json)
 | 
			
		||||
	if (!payment) {
 | 
			
		||||
		throw new Error("Payment verification failed or payment not found");
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,12 @@ import React from 'react'
 | 
			
		||||
 | 
			
		||||
export default function Agreements() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>Agreements</div>
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
          Agreements
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@ export default async function Devices({
 | 
			
		||||
	const user = await getCurrentUser()
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
				<h3>
 | 
			
		||||
			<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
				<h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
					My Devices
 | 
			
		||||
				</h3>
 | 
			
		||||
				<AddDeviceDialogForm user_id={user?.id} />
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,8 @@ export default async function ParentalControl({
 | 
			
		||||
  const query = (await searchParams)?.query || "";
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3>
 | 
			
		||||
      <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
          Parental Control
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,13 @@ export default async function PaymentPage({
 | 
			
		||||
      devices: true,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const formula = await prisma.billFormula.findFirst();
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3>Payment</h3>
 | 
			
		||||
        <span className={cn("text-sm border px-4 py-2 rounded-md uppercase font-semibold", payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")}>
 | 
			
		||||
      <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
          Payment
 | 
			
		||||
        </h3>
 | 
			
		||||
        <span className={cn("text-sm border px-4 py-2 rounded-md uppercase font-semibold", payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-700")}>
 | 
			
		||||
          {payment?.paid ? "Paid" : "Pending"}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -42,7 +43,6 @@ export default async function PaymentPage({
 | 
			
		||||
      >
 | 
			
		||||
        <DevicesToPay
 | 
			
		||||
          user={user || undefined}
 | 
			
		||||
          billFormula={formula ?? undefined}
 | 
			
		||||
          payment={payment || undefined}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,10 @@ export default async function Devices({
 | 
			
		||||
  const query = (await searchParams)?.query || "";
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3>My Payments</h3>
 | 
			
		||||
      <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
          My Payments
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
export default async function UserDevcies() {
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h3 className="text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
			<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
				<h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
					User Devices
 | 
			
		||||
				</h3>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,11 @@ export default async function UserPayments() {
 | 
			
		||||
	await AdminAuthGuard();
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h3>User Payments</h3>
 | 
			
		||||
			<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
				<h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
					User Payments
 | 
			
		||||
				</h3>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,9 +38,11 @@ export default async function AdminUsers({
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h3 className="text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
			<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
				<h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
					Users
 | 
			
		||||
				</h3>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div
 | 
			
		||||
				id="user-table-filters"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								app/(dashboard)/wallet/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/(dashboard)/wallet/page.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export default function UserWallet() {
 | 
			
		||||
  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">
 | 
			
		||||
        <h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
          My Wallet
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -30,6 +30,7 @@ export async function ApplicationLayout({
 | 
			
		||||
	return (
 | 
			
		||||
		<SidebarProvider>
 | 
			
		||||
			<AppSidebar role={session?.user?.role || "USER"} />
 | 
			
		||||
			<DeviceCartDrawer billFormula={billFormula || null} />
 | 
			
		||||
			<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">
 | 
			
		||||
					<div className="flex items-center gap-2 ">
 | 
			
		||||
@@ -39,7 +40,6 @@ export async function ApplicationLayout({
 | 
			
		||||
 | 
			
		||||
					<div className="flex items-center gap-2">
 | 
			
		||||
						<Wallet walletBalance={user?.walletBalance || 0} />
 | 
			
		||||
						<DeviceCartDrawer billFormula={billFormula || null} />
 | 
			
		||||
						<ModeToggle />
 | 
			
		||||
						<AccountPopover />
 | 
			
		||||
					</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,135 @@
 | 
			
		||||
'use client'
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import { blockDevice } from "@/actions/omada-actions";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import type { Device } from "@prisma/client";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import { TextShimmer } from "./ui/text-shimmer";
 | 
			
		||||
import { blockDevice } from "@/actions/omada-actions"
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogFooter,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
} from "@/components/ui/dialog"
 | 
			
		||||
import { Label } from "@/components/ui/label"
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { zodResolver } from "@hookform/resolvers/zod"
 | 
			
		||||
import type { Device, } from "@prisma/client"
 | 
			
		||||
import { OctagonX } from "lucide-react"
 | 
			
		||||
import { useState } from "react"
 | 
			
		||||
import { type SubmitHandler, useForm } from "react-hook-form"
 | 
			
		||||
import { toast } from "sonner"
 | 
			
		||||
import { z } from "zod"
 | 
			
		||||
import { Textarea } from "./ui/textarea"
 | 
			
		||||
import { TextShimmer } from "./ui/text-shimmer"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const validationSchema = z.object({
 | 
			
		||||
  reasonForBlocking: z.string().min(5, { message: "Reason is required" }),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default function BlockDeviceDialog({ device, type }: { device: Device, type: "block" | "unblock" }) {
 | 
			
		||||
  const [disabled, setDisabled] = useState(false)
 | 
			
		||||
  const [open, setOpen] = useState(false)
 | 
			
		||||
  const {
 | 
			
		||||
    register,
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
    formState: { errors },
 | 
			
		||||
  } = useForm<z.infer<typeof validationSchema>>({
 | 
			
		||||
    resolver: zodResolver(validationSchema),
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const onSubmit: SubmitHandler<z.infer<typeof validationSchema>> = (data) => {
 | 
			
		||||
    setDisabled(true)
 | 
			
		||||
    console.log(data)
 | 
			
		||||
    toast.promise(blockDevice({
 | 
			
		||||
      macAddress: device.mac,
 | 
			
		||||
      type: type,
 | 
			
		||||
      reason: data.reasonForBlocking,
 | 
			
		||||
      // reason: data.reasonForBlocking,
 | 
			
		||||
    }), {
 | 
			
		||||
      loading: "Blocking...",
 | 
			
		||||
      success: () => {
 | 
			
		||||
        setDisabled(false)
 | 
			
		||||
        setOpen((prev) => !prev)
 | 
			
		||||
        return "Blocked!"
 | 
			
		||||
      },
 | 
			
		||||
      error: (error) => {
 | 
			
		||||
        setDisabled(false)
 | 
			
		||||
        return error || "Something went wrong"
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
    setDisabled(false)
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
export default function BlockDeviceDialog({ device }: { device: Device }) {
 | 
			
		||||
  const [disabled, setDisabled] = useState(false);
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      className="w-full mt-2"
 | 
			
		||||
      disabled={disabled}
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
    <div>
 | 
			
		||||
      {device.blocked ? (
 | 
			
		||||
        <Button onClick={
 | 
			
		||||
          () => {
 | 
			
		||||
            setDisabled(true);
 | 
			
		||||
        toast.promise(blockDevice({ macAddress: device.mac, type: device.blocked ? "unblock" : "block" }), {
 | 
			
		||||
          loading: device.blocked ? "Unblocking..." : "Blocking...",
 | 
			
		||||
            toast.promise(blockDevice({
 | 
			
		||||
              macAddress: device.mac,
 | 
			
		||||
              type: "unblock",
 | 
			
		||||
              reason: '',
 | 
			
		||||
            }), {
 | 
			
		||||
              loading: "unblockinig...",
 | 
			
		||||
              success: () => {
 | 
			
		||||
                setDisabled(false);
 | 
			
		||||
            return `Device ${device.name} successfully ${device.blocked ? "unblocked" : "blocked"
 | 
			
		||||
              }!`;
 | 
			
		||||
                return "Unblocked!";
 | 
			
		||||
              },
 | 
			
		||||
              error: () => {
 | 
			
		||||
                setDisabled(false);
 | 
			
		||||
                return "Something went wrong";
 | 
			
		||||
              },
 | 
			
		||||
        });
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {disabled ? <TextShimmer>{device.blocked ? "Unblocking..." : "Blocking..."}</TextShimmer> : (device?.blocked ? "Unblock" : "Block")}
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        }>
 | 
			
		||||
          {disabled ? (
 | 
			
		||||
            <TextShimmer>
 | 
			
		||||
              Unblocking
 | 
			
		||||
            </TextShimmer>
 | 
			
		||||
          ) : "Unblock"}
 | 
			
		||||
        </Button>
 | 
			
		||||
 | 
			
		||||
      ) : (
 | 
			
		||||
 | 
			
		||||
        <Dialog open={open} onOpenChange={setOpen}>
 | 
			
		||||
          <DialogTrigger asChild>
 | 
			
		||||
            <Button disabled={disabled} variant="destructive">
 | 
			
		||||
              <OctagonX />
 | 
			
		||||
              Block
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogTrigger>
 | 
			
		||||
          <DialogContent className="sm:max-w-[425px]">
 | 
			
		||||
            <DialogHeader>
 | 
			
		||||
              <DialogTitle>Please provide a reason for blocking this device.</DialogTitle>
 | 
			
		||||
 | 
			
		||||
            </DialogHeader>
 | 
			
		||||
            <form onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
              <div className="grid gap-4 py-4">
 | 
			
		||||
                <div className="flex flex-col items-start gap-1">
 | 
			
		||||
                  <Label htmlFor="reason" className="text-right">
 | 
			
		||||
                    Reason for blocking
 | 
			
		||||
                  </Label>
 | 
			
		||||
                  <Textarea rows={10} {...register("reasonForBlocking")} id="reasonForBlocking" className={cn("col-span-5", errors.reasonForBlocking && "ring-2 ring-red-500")} />
 | 
			
		||||
                  <span className="text-sm text-red-500">
 | 
			
		||||
                    {errors.reasonForBlocking?.message}
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <DialogFooter>
 | 
			
		||||
                <Button variant={"destructive"} disabled={disabled} type="submit">
 | 
			
		||||
                  Block
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DialogFooter>
 | 
			
		||||
            </form>
 | 
			
		||||
          </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -45,11 +45,11 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
 | 
			
		||||
              year: "numeric",
 | 
			
		||||
            })}
 | 
			
		||||
          </span>
 | 
			
		||||
          {parentalControl && (
 | 
			
		||||
          {(parentalControl && device.blocked) && (
 | 
			
		||||
            <div className="p-2 rounded border my-2">
 | 
			
		||||
              <span>Comment: </span>
 | 
			
		||||
              <p className="text-neutral-500">
 | 
			
		||||
                blocked because he was watching youtube
 | 
			
		||||
                {device?.reasonForBlocking}
 | 
			
		||||
              </p>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
@@ -60,7 +60,7 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
 | 
			
		||||
        {!parentalControl ? (
 | 
			
		||||
          <AddDevicesToCartButton device={device} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <BlockDeviceDialog device={device} />
 | 
			
		||||
          <BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} />
 | 
			
		||||
        )}
 | 
			
		||||
      </TableCell>
 | 
			
		||||
    </TableRow >
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
 | 
			
		||||
 | 
			
		||||
          {device.blocked && (
 | 
			
		||||
            <div className="p-2 rounded border my-2 w-full">
 | 
			
		||||
              <span>Comment: </span>
 | 
			
		||||
              <span className='uppercase text-red-500'>Blocked by admin </span>
 | 
			
		||||
              <p className="text-neutral-500">
 | 
			
		||||
                blocked because he was watching youtube
 | 
			
		||||
              </p>
 | 
			
		||||
@@ -67,7 +67,7 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
 | 
			
		||||
          {!parentalControl ? (
 | 
			
		||||
            <AddDevicesToCartButton device={device} />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <BlockDeviceDialog device={device} />
 | 
			
		||||
            <BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,6 @@ import { toast } from "sonner";
 | 
			
		||||
import NumberInput from "./number-input";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function DeviceCartDrawer({
 | 
			
		||||
  billFormula,
 | 
			
		||||
}: {
 | 
			
		||||
@@ -74,12 +73,14 @@ export function DeviceCartDrawer({
 | 
			
		||||
    paid: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (devices.length === 0) return null
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Drawer open={isOpen} onOpenChange={setIsOpen}>
 | 
			
		||||
        <DrawerTrigger asChild>
 | 
			
		||||
        <Button onClick={() => setIsOpen(!isOpen)} variant="outline">
 | 
			
		||||
          <Button size={"lg"} className="bg-sarLinkOrange absolute bottom-10  w-fit z-20 left-1/2 transform -translate-x-1/2" onClick={() => setIsOpen(!isOpen)} variant="outline">
 | 
			
		||||
            <MonitorSmartphone />
 | 
			
		||||
          {devices.length > 0 && `(${devices.length})`}
 | 
			
		||||
            Pay {devices.length > 0 && `(${devices.length})`} Device
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DrawerTrigger>
 | 
			
		||||
        <DrawerContent>
 | 
			
		||||
@@ -142,15 +143,18 @@ export function DeviceCartDrawer({
 | 
			
		||||
              <Button
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setDeviceCart([]);
 | 
			
		||||
                  setIsOpen(!isOpen);
 | 
			
		||||
                }}
 | 
			
		||||
                variant="outline"
 | 
			
		||||
              >
 | 
			
		||||
              Reset
 | 
			
		||||
                Clear Selection
 | 
			
		||||
              </Button>
 | 
			
		||||
            </DrawerFooter>
 | 
			
		||||
          </div>
 | 
			
		||||
        </DrawerContent>
 | 
			
		||||
      </Drawer>
 | 
			
		||||
    </>
 | 
			
		||||
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import {
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table";
 | 
			
		||||
import { formatDate } from "@/lib/utils";
 | 
			
		||||
import type { BillFormula, Prisma, User } from "@prisma/client";
 | 
			
		||||
import type { Prisma, User } from "@prisma/client";
 | 
			
		||||
import { BadgeDollarSign, Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
@@ -22,28 +22,24 @@ type PaymentWithDevices = Prisma.PaymentGetPayload<{
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export default function DevicesToPay({
 | 
			
		||||
  billFormula,
 | 
			
		||||
  payment,
 | 
			
		||||
  user
 | 
			
		||||
}: { billFormula?: BillFormula; payment?: PaymentWithDevices, user?: User }) {
 | 
			
		||||
}: { payment?: PaymentWithDevices, user?: User }) {
 | 
			
		||||
  const [verifying, setVerifying] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const devices = payment?.devices;
 | 
			
		||||
  if (devices?.length === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  const baseAmount = billFormula?.baseAmount ?? 100;
 | 
			
		||||
  const discountPercentage = billFormula?.discountPercentage ?? 75;
 | 
			
		||||
  // 100+(n−1)×75
 | 
			
		||||
  const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage;
 | 
			
		||||
  const walletBalance = user?.walletBalance ?? 0;
 | 
			
		||||
  const isWalletPayVisible = walletBalance > total;
 | 
			
		||||
  const isWalletPayVisible = walletBalance > (payment?.amount ?? 0);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full">
 | 
			
		||||
      <div className="p-2 flex flex-col gap-2">
 | 
			
		||||
        <h3 className="title-bg my-1 p-2 font-semibold text-lg">
 | 
			
		||||
        <h3 className="title-bg my-1 p-2 border border-dashed rounded-md font-semibold text-lg">
 | 
			
		||||
          {!payment?.paid ? "Devices to pay" : "Devices Paid"}
 | 
			
		||||
        </h3>
 | 
			
		||||
        <div className="flex flex-col gap-2">
 | 
			
		||||
@@ -85,7 +81,7 @@ export default function DevicesToPay({
 | 
			
		||||
                          paymentId: payment?.id,
 | 
			
		||||
                          benefName: user?.name ?? "",
 | 
			
		||||
                          accountNo: user?.accNo ?? "",
 | 
			
		||||
                          absAmount: String(total),
 | 
			
		||||
                          absAmount: String(payment?.amount),
 | 
			
		||||
                          time: formatDate(new Date(payment?.createdAt || "")),
 | 
			
		||||
                          type: "WALLET",
 | 
			
		||||
                        });
 | 
			
		||||
@@ -105,7 +101,7 @@ export default function DevicesToPay({
 | 
			
		||||
                        paymentId: payment?.id,
 | 
			
		||||
                        benefName: user?.name ?? "",
 | 
			
		||||
                        accountNo: user?.accNo ?? "",
 | 
			
		||||
                        absAmount: String(total),
 | 
			
		||||
                        absAmount: String(payment?.amount),
 | 
			
		||||
                        type: "TRANSFER",
 | 
			
		||||
                        time: formatDate(new Date(payment?.createdAt || "")),
 | 
			
		||||
                      });
 | 
			
		||||
@@ -140,7 +136,7 @@ export default function DevicesToPay({
 | 
			
		||||
          <TableFooter>
 | 
			
		||||
            <TableRow className="">
 | 
			
		||||
              <TableCell colSpan={1}>Total Due</TableCell>
 | 
			
		||||
              <TableCell className="text-right text-3xl font-bold">{total.toFixed(2)}</TableCell>
 | 
			
		||||
              <TableCell className="text-right text-3xl font-bold">{payment?.amount.toFixed(2)}</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableFooter>
 | 
			
		||||
        </Table>
 | 
			
		||||
 
 | 
			
		||||
@@ -104,7 +104,7 @@ export async function PaymentsTable({
 | 
			
		||||
              {payments.map((payment) => (
 | 
			
		||||
                <TableRow key={payment.id}>
 | 
			
		||||
                  <TableCell>
 | 
			
		||||
                    <div className={cn("flex flex-col items-start title-bg border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
 | 
			
		||||
                    <div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
 | 
			
		||||
                      <div className="flex items-center gap-2">
 | 
			
		||||
                        <Calendar size={16} opacity={0.5} />
 | 
			
		||||
                        <span className="text-muted-foreground">
 | 
			
		||||
 
 | 
			
		||||
@@ -35,8 +35,10 @@ export default function PriceCalculator() {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="border p-2 rounded-xl">
 | 
			
		||||
      <div className="flex flex-col justify-between items-start text-gray-500 title-bg p-2 mb-4">
 | 
			
		||||
        <h3 className="text-2xl font-semibold">Price Calculator</h3>
 | 
			
		||||
      <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
 | 
			
		||||
        <h3 className="text-sarLinkOrange text-2xl">
 | 
			
		||||
          Price Calculator
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
 | 
			
		||||
        {/* Initial Price Input */}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
 | 
			
		||||
 | 
			
		||||
const nextConfig: NextConfig = {
 | 
			
		||||
  /* config options here */
 | 
			
		||||
  output: "standalone",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default nextConfig;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10036
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10036
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										20
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								package.json
									
									
									
									
									
								
							@@ -4,7 +4,7 @@
 | 
			
		||||
	"private": true,
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"dev": "next dev --turbopack",
 | 
			
		||||
		"build": "next build",
 | 
			
		||||
		"build": "bunx prisma migrate deploy && bunx prisma generate && bunx prisma db push && next build",
 | 
			
		||||
		"start": "next start",
 | 
			
		||||
		"lint": "next lint"
 | 
			
		||||
	},
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@faker-js/faker": "^9.3.0",
 | 
			
		||||
		"@hookform/resolvers": "^3.9.1",
 | 
			
		||||
		"@prisma/client": "^5.22.0",
 | 
			
		||||
		"@prisma/client": "^6.1.0",
 | 
			
		||||
		"@radix-ui/react-alert-dialog": "^1.1.2",
 | 
			
		||||
		"@radix-ui/react-checkbox": "^1.1.2",
 | 
			
		||||
		"@radix-ui/react-collapsible": "^1.1.1",
 | 
			
		||||
@@ -33,33 +33,33 @@
 | 
			
		||||
		"clsx": "^2.1.1",
 | 
			
		||||
		"cmdk": "^1.0.0",
 | 
			
		||||
		"date-fns": "^4.1.0",
 | 
			
		||||
		"jotai": "^2.8.0",
 | 
			
		||||
		"jotai": "2.8.0",
 | 
			
		||||
		"lucide-react": "^0.460.0",
 | 
			
		||||
		"motion": "^11.15.0",
 | 
			
		||||
		"next": "15.1.2",
 | 
			
		||||
		"next-themes": "^0.4.3",
 | 
			
		||||
		"nextjs-toploader": "^3.7.15",
 | 
			
		||||
		"prisma": "^5.22.0",
 | 
			
		||||
		"prisma": "^6.1.0",
 | 
			
		||||
		"react": "19.0.0",
 | 
			
		||||
		"react-aria-components": "^1.5.0",
 | 
			
		||||
		"react-day-picker": "^8.10.1",
 | 
			
		||||
		"react-dom": "19.0.0",
 | 
			
		||||
		"react-hook-form": "^7.53.2",
 | 
			
		||||
		"react-phone-number-input": "^3.4.9",
 | 
			
		||||
		"sonner": "^1.7.0",
 | 
			
		||||
		"tailwind-merge": "^2.5.4",
 | 
			
		||||
		"sonner": "^1.7.1",
 | 
			
		||||
		"tailwind-merge": "^2.6.0",
 | 
			
		||||
		"tailwindcss-animate": "^1.0.7",
 | 
			
		||||
		"vaul": "^1.1.1",
 | 
			
		||||
		"zod": "^3.23.8"
 | 
			
		||||
		"vaul": "^1.1.2",
 | 
			
		||||
		"zod": "^3.24.1"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@types/node": "^22.10.2",
 | 
			
		||||
		"@types/react": "^19.0.2",
 | 
			
		||||
		"@types/react-dom": "^19.0.2",
 | 
			
		||||
		"eslint": "^8",
 | 
			
		||||
		"eslint": "^9.17.0",
 | 
			
		||||
		"eslint-config-next": "15.1.2",
 | 
			
		||||
		"postcss": "^8.4.49",
 | 
			
		||||
		"tailwindcss": "^3.4.1",
 | 
			
		||||
		"tailwindcss": "^3.4.17",
 | 
			
		||||
		"ts-node": "^10.9.2",
 | 
			
		||||
		"typescript": "^5.7.2"
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import type { Config } from "tailwindcss";
 | 
			
		||||
import tailwindcssAnimate from "tailwindcss-animate";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
	darkMode: ["class"],
 | 
			
		||||
@@ -10,6 +11,7 @@ export default {
 | 
			
		||||
	theme: {
 | 
			
		||||
		extend: {
 | 
			
		||||
			colors: {
 | 
			
		||||
				sarLinkOrange: "#f49b5b",
 | 
			
		||||
				background: "hsl(var(--background))",
 | 
			
		||||
				foreground: "hsl(var(--foreground))",
 | 
			
		||||
				card: {
 | 
			
		||||
@@ -72,5 +74,5 @@ export default {
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	plugins: [require("tailwindcss-animate")],
 | 
			
		||||
	plugins: [tailwindcssAnimate],
 | 
			
		||||
} satisfies Config;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user