mirror of
				https://github.com/i701/sarlink-portal.git
				synced 2025-11-04 06:26:59 +00:00 
			
		
		
		
	Add payment processing and device management features
- Introduced createPayment action for handling payment creation. - Added PaymentsTable component for displaying payment records with pagination. - Implemented new PaymentPage for viewing individual payment details and associated devices. - Refactored DeviceCartDrawer to integrate payment creation and device selection. - Enhanced DevicesToPay component to display devices based on payment status. - Updated PriceCalculator component for better user input handling. - Introduced NumberInput component for consistent number input across forms. - Modified Prisma schema to include new fields for payments and devices. - Improved overall user experience with responsive design adjustments and new UI elements.
This commit is contained in:
		
							
								
								
									
										26
									
								
								actions/payment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								actions/payment.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					"use server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import prisma from "@/lib/db";
 | 
				
			||||||
 | 
					import type { PaymentType } from "@/lib/types";
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function createPayment(data: PaymentType) {
 | 
				
			||||||
 | 
						console.log("hi", 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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,24 +0,0 @@
 | 
				
			|||||||
import DevicesToPay from '@/components/devices-to-pay';
 | 
					 | 
				
			||||||
import prisma from '@/lib/db';
 | 
					 | 
				
			||||||
import React from 'react'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default async function PaymentPage() {
 | 
					 | 
				
			||||||
  const formula = await prisma.billFormula.findFirst();
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
      <div className="flex justify-between items-center border-b-2 text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
					 | 
				
			||||||
        <h3>
 | 
					 | 
				
			||||||
          Payment
 | 
					 | 
				
			||||||
        </h3>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        id="user-filters"
 | 
					 | 
				
			||||||
        className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <DevicesToPay billFormula={formula ?? undefined} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										37
									
								
								app/(dashboard)/payments/[paymentId]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/(dashboard)/payments/[paymentId]/page.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import DevicesToPay from "@/components/devices-to-pay";
 | 
				
			||||||
 | 
					import { hasSession } from "@/lib/auth-guard";
 | 
				
			||||||
 | 
					import prisma from "@/lib/db";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function PaymentPage({
 | 
				
			||||||
 | 
					  params,
 | 
				
			||||||
 | 
					}: { params: Promise<{ paymentId: string }> }) {
 | 
				
			||||||
 | 
					  const paymentId = (await params).paymentId;
 | 
				
			||||||
 | 
					  const payment = await prisma.payment.findUnique({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      id: paymentId,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    include: {
 | 
				
			||||||
 | 
					      devices: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  await hasSession();
 | 
				
			||||||
 | 
					  const formula = await prisma.billFormula.findFirst();
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <div className="flex justify-between items-center border-b-2 text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
				
			||||||
 | 
					        <h3>Payment</h3>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        id="user-filters"
 | 
				
			||||||
 | 
					        className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <DevicesToPay
 | 
				
			||||||
 | 
					          billFormula={formula ?? undefined}
 | 
				
			||||||
 | 
					          payment={payment || undefined}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,14 +1,33 @@
 | 
				
			|||||||
"use client";
 | 
					import { PaymentsTable } from "@/components/payments-table";
 | 
				
			||||||
import { authClient } from "@/lib/auth-client";
 | 
					import Search from "@/components/search";
 | 
				
			||||||
import React from "react";
 | 
					import { Suspense } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function MyPayments() {
 | 
					export default async function Devices({
 | 
				
			||||||
	const session = authClient.useSession();
 | 
					  searchParams,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  searchParams: Promise<{
 | 
				
			||||||
 | 
					    query: string;
 | 
				
			||||||
 | 
					    page: number;
 | 
				
			||||||
 | 
					    sortBy: string;
 | 
				
			||||||
 | 
					    status: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const query = (await searchParams)?.query || "";
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <div className="flex justify-between items-center border-b-2 text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
 | 
				
			||||||
 | 
					        <h3>My Payments</h3>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
					      <div
 | 
				
			||||||
		<div>
 | 
					        id="user-filters"
 | 
				
			||||||
			<h3>Client session</h3>
 | 
					        className=" border-b-2 pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
 | 
				
			||||||
			<pre>{JSON.stringify(session.data, null, 2)}</pre>
 | 
					      >
 | 
				
			||||||
		</div>
 | 
					        <Search />
 | 
				
			||||||
	);
 | 
					      </div>
 | 
				
			||||||
 | 
					      <Suspense key={query} fallback={"loading...."}>
 | 
				
			||||||
 | 
					        <PaymentsTable searchParams={searchParams} />
 | 
				
			||||||
 | 
					      </Suspense>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,125 +1,8 @@
 | 
				
			|||||||
"use client";
 | 
					import PriceCalculator from '@/components/price-calculator'
 | 
				
			||||||
import {
 | 
					import React from 'react'
 | 
				
			||||||
  discountPercentageAtom,
 | 
					 | 
				
			||||||
  formulaResultAtom,
 | 
					 | 
				
			||||||
  initialPriceAtom,
 | 
					 | 
				
			||||||
  numberOfDaysAtom,
 | 
					 | 
				
			||||||
  numberOfDevicesAtom,
 | 
					 | 
				
			||||||
} from "@/lib/atoms";
 | 
					 | 
				
			||||||
import { useAtom } from "jotai";
 | 
					 | 
				
			||||||
import { Minus, Plus } from "lucide-react";
 | 
					 | 
				
			||||||
import { useEffect } from "react";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  Button,
 | 
					 | 
				
			||||||
  Group,
 | 
					 | 
				
			||||||
  Input,
 | 
					 | 
				
			||||||
  Label,
 | 
					 | 
				
			||||||
  NumberField,
 | 
					 | 
				
			||||||
} from "react-aria-components";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function PriceCalculator() {
 | 
					 | 
				
			||||||
  const [initialPrice, setInitialPrice] = useAtom(initialPriceAtom);
 | 
					 | 
				
			||||||
  const [discountPercentage, setDiscountPercentage] = useAtom(
 | 
					 | 
				
			||||||
    discountPercentageAtom,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  const [numberOfDevices, setNumberOfDevices] = useAtom(numberOfDevicesAtom);
 | 
					 | 
				
			||||||
  const [numberOfDays, setNumberOfDays] = useAtom(numberOfDaysAtom);
 | 
					 | 
				
			||||||
  const [formulaResult, setFormulaResult] = useAtom(formulaResultAtom);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const basePrice = initialPrice + (numberOfDevices - 1) * discountPercentage;
 | 
					 | 
				
			||||||
    setFormulaResult(
 | 
					 | 
				
			||||||
      `Price for ${numberOfDevices} device(s) over ${numberOfDays} day(s): MVR ${basePrice.toFixed(2)}`,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }, [
 | 
					 | 
				
			||||||
    initialPrice,
 | 
					 | 
				
			||||||
    discountPercentage,
 | 
					 | 
				
			||||||
    numberOfDevices,
 | 
					 | 
				
			||||||
    numberOfDays,
 | 
					 | 
				
			||||||
    setFormulaResult,
 | 
					 | 
				
			||||||
  ]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Pricing() {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="border p-2 rounded-xl">
 | 
					    <PriceCalculator />
 | 
				
			||||||
      <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>
 | 
					 | 
				
			||||||
      <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
 | 
					 | 
				
			||||||
        {/* Initial Price Input */}
 | 
					 | 
				
			||||||
        <NumberInput
 | 
					 | 
				
			||||||
          label="Initial Price"
 | 
					 | 
				
			||||||
          value={initialPrice}
 | 
					 | 
				
			||||||
          onChange={(value) => setInitialPrice(value)}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        {/* Number of Devices Input */}
 | 
					 | 
				
			||||||
        <NumberInput
 | 
					 | 
				
			||||||
          label="Number of Devices"
 | 
					 | 
				
			||||||
          value={numberOfDevices}
 | 
					 | 
				
			||||||
          onChange={(value) => setNumberOfDevices(value)}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        {/* Number of Days Input */}
 | 
					 | 
				
			||||||
        <NumberInput
 | 
					 | 
				
			||||||
          label="Number of Days"
 | 
					 | 
				
			||||||
          value={numberOfDays}
 | 
					 | 
				
			||||||
          onChange={(value) => setNumberOfDays(value)}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {/* Discount Percentage Input */}
 | 
					 | 
				
			||||||
        <NumberInput
 | 
					 | 
				
			||||||
          label="Discount Percentage"
 | 
					 | 
				
			||||||
          value={discountPercentage}
 | 
					 | 
				
			||||||
          onChange={(value) => setDiscountPercentage(value)}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div className="mt-4">
 | 
					 | 
				
			||||||
        <div className="title-bg relative rounded-lg border border-input shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 [&:has(input:is(:disabled))_*]:pointer-events-none">
 | 
					 | 
				
			||||||
          <label
 | 
					 | 
				
			||||||
            htmlFor=""
 | 
					 | 
				
			||||||
            className="block px-3 pt-2 text-md font-medium text-foreground"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            Total
 | 
					 | 
				
			||||||
          </label>
 | 
					 | 
				
			||||||
          <input
 | 
					 | 
				
			||||||
            className="flex font-mono font-semibold h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
 | 
					 | 
				
			||||||
            value={formulaResult}
 | 
					 | 
				
			||||||
            readOnly
 | 
					 | 
				
			||||||
            placeholder={"Result"}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Dependencies: pnpm install lucide-react react-aria-components
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function NumberInput({
 | 
					 | 
				
			||||||
  label,
 | 
					 | 
				
			||||||
  value,
 | 
					 | 
				
			||||||
  onChange,
 | 
					 | 
				
			||||||
}: { label: string; value: number; onChange: (value: number) => void }) {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <NumberField value={value} minValue={0} onChange={onChange}>
 | 
					 | 
				
			||||||
      <div className="space-y-2">
 | 
					 | 
				
			||||||
        <Label className="text-sm font-medium text-foreground">{label}</Label>
 | 
					 | 
				
			||||||
        <Group className="relative inline-flex h-9 w-full items-center overflow-hidden whitespace-nowrap rounded-lg border border-input text-sm shadow-sm shadow-black/5 transition-shadow data-[focus-within]:border-ring data-[disabled]:opacity-50 data-[focus-within]:outline-none data-[focus-within]:ring-[3px] data-[focus-within]:ring-ring/20">
 | 
					 | 
				
			||||||
          <Button
 | 
					 | 
				
			||||||
            slot="decrement"
 | 
					 | 
				
			||||||
            className="-ms-px flex aspect-square h-[inherit] items-center justify-center rounded-s-lg border border-input bg-background text-sm text-muted-foreground/80 transition-shadow hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Minus size={16} strokeWidth={2} aria-hidden="true" />
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
          <Input className="w-full grow bg-background px-3 py-2 text-center tabular-nums text-foreground focus:outline-none" />
 | 
					 | 
				
			||||||
          <Button
 | 
					 | 
				
			||||||
            slot="increment"
 | 
					 | 
				
			||||||
            className="-me-px flex aspect-square h-[inherit] items-center justify-center rounded-e-lg border border-input bg-background text-sm text-muted-foreground/80 transition-shadow hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Plus size={16} strokeWidth={2} aria-hidden="true" />
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
        </Group>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </NumberField>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								app/favicon.ico
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/favicon.ico
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB  | 
@@ -16,12 +16,12 @@ export default function AddDevicesToCartButton({ device }: { device: Device }) {
 | 
				
			|||||||
    >
 | 
					    >
 | 
				
			||||||
      {devices.some((d) => d.id === device.id) ? (
 | 
					      {devices.some((d) => d.id === device.id) ? (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          Added
 | 
					          Selected
 | 
				
			||||||
          <CheckCheck />
 | 
					          <CheckCheck />
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
      ) : (
 | 
					      ) : (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          Add to cart
 | 
					          Select device
 | 
				
			||||||
          <BadgePlus />
 | 
					          <BadgePlus />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import {
 | 
				
			|||||||
	SidebarTrigger,
 | 
						SidebarTrigger,
 | 
				
			||||||
} from "@/components/ui/sidebar";
 | 
					} from "@/components/ui/sidebar";
 | 
				
			||||||
import { auth } from "@/lib/auth";
 | 
					import { auth } from "@/lib/auth";
 | 
				
			||||||
 | 
					import prisma from "@/lib/db";
 | 
				
			||||||
import { headers } from "next/headers";
 | 
					import { headers } from "next/headers";
 | 
				
			||||||
import { AccountPopover } from "./account-popver";
 | 
					import { AccountPopover } from "./account-popver";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,6 +19,7 @@ export async function ApplicationLayout({
 | 
				
			|||||||
	const session = await auth.api.getSession({
 | 
						const session = await auth.api.getSession({
 | 
				
			||||||
		headers: await headers()
 | 
							headers: await headers()
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
						const billFormula = await prisma.billFormula.findFirst();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<SidebarProvider>
 | 
							<SidebarProvider>
 | 
				
			||||||
@@ -30,7 +32,7 @@ export async function ApplicationLayout({
 | 
				
			|||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<div className="flex items-center gap-2">
 | 
										<div className="flex items-center gap-2">
 | 
				
			||||||
						<DeviceCartDrawer />
 | 
											<DeviceCartDrawer billFormula={billFormula || null} />
 | 
				
			||||||
						<ModeToggle />
 | 
											<ModeToggle />
 | 
				
			||||||
						<AccountPopover />
 | 
											<AccountPopover />
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
"use client"
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as React from "react"
 | 
					import { createPayment } from "@/actions/payment";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
import { Button } from "@/components/ui/button"
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Drawer,
 | 
					  Drawer,
 | 
				
			||||||
  DrawerClose,
 | 
					  DrawerClose,
 | 
				
			||||||
@@ -12,98 +11,178 @@ import {
 | 
				
			|||||||
  DrawerHeader,
 | 
					  DrawerHeader,
 | 
				
			||||||
  DrawerTitle,
 | 
					  DrawerTitle,
 | 
				
			||||||
  DrawerTrigger,
 | 
					  DrawerTrigger,
 | 
				
			||||||
} from "@/components/ui/drawer"
 | 
					} from "@/components/ui/drawer";
 | 
				
			||||||
import { cartDrawerOpenAtom, deviceCartAtom } from "@/lib/atoms"
 | 
					import {
 | 
				
			||||||
import type { Device } from "@prisma/client"
 | 
					  cartDrawerOpenAtom,
 | 
				
			||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai"
 | 
					  deviceCartAtom,
 | 
				
			||||||
import { CircleDollarSign, ShoppingCart, Trash2 } from "lucide-react"
 | 
					  numberOfMonths,
 | 
				
			||||||
import Link from "next/link"
 | 
					} from "@/lib/atoms";
 | 
				
			||||||
import { usePathname } from "next/navigation"
 | 
					import { authClient } from "@/lib/auth-client";
 | 
				
			||||||
 | 
					import type { PaymentType } from "@/lib/types";
 | 
				
			||||||
 | 
					import type { BillFormula, Device } from "@prisma/client";
 | 
				
			||||||
 | 
					import { useAtom, useAtomValue, useSetAtom } from "jotai";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  CircleDollarSign,
 | 
				
			||||||
 | 
					  Loader2,
 | 
				
			||||||
 | 
					  MonitorSmartphone,
 | 
				
			||||||
 | 
					  Trash2,
 | 
				
			||||||
 | 
					} from "lucide-react";
 | 
				
			||||||
 | 
					import { usePathname, useRouter } from "next/navigation";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import NumberInput from "./number-input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function DeviceCartDrawer() {
 | 
					
 | 
				
			||||||
  const pathname = usePathname()
 | 
					export function DeviceCartDrawer({
 | 
				
			||||||
  const devices = useAtomValue(deviceCartAtom)
 | 
					  billFormula,
 | 
				
			||||||
  const setDeviceCart = useSetAtom(deviceCartAtom)
 | 
					}: {
 | 
				
			||||||
  const [isOpen, setIsOpen] = useAtom(cartDrawerOpenAtom)
 | 
					  billFormula: BillFormula | null;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  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 [isOpen, setIsOpen] = useAtom(cartDrawerOpenAtom);
 | 
				
			||||||
 | 
					  const [message, setMessage] = useState("");
 | 
				
			||||||
 | 
					  const [disabled, setDisabled] = useState(false);
 | 
				
			||||||
 | 
					  const [total, setTotal] = useState(0);
 | 
				
			||||||
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					  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) * discountPercentage);
 | 
				
			||||||
 | 
					  }, [months, devices.length, baseAmount, discountPercentage]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (pathname === "/payment") {
 | 
					  if (pathname === "/payment") {
 | 
				
			||||||
    return null
 | 
					    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,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Drawer open={isOpen} onOpenChange={setIsOpen}>
 | 
					    <Drawer open={isOpen} onOpenChange={setIsOpen}>
 | 
				
			||||||
      <DrawerTrigger asChild>
 | 
					      <DrawerTrigger asChild>
 | 
				
			||||||
        <Button
 | 
					        <Button onClick={() => setIsOpen(!isOpen)} variant="outline">
 | 
				
			||||||
          onClick={() => setIsOpen(!isOpen)}
 | 
					          <MonitorSmartphone />
 | 
				
			||||||
          variant="outline">
 | 
					          Selected Devices {devices.length > 0 && `(${devices.length})`}
 | 
				
			||||||
          <ShoppingCart />
 | 
					 | 
				
			||||||
          Cart {devices.length > 0 && `(${devices.length})`}
 | 
					 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </DrawerTrigger>
 | 
					      </DrawerTrigger>
 | 
				
			||||||
      <DrawerContent>
 | 
					      <DrawerContent>
 | 
				
			||||||
        <div className="mx-auto w-full max-w-sm">
 | 
					        <div className="mx-auto w-full max-w-sm">
 | 
				
			||||||
          <DrawerHeader>
 | 
					          <DrawerHeader>
 | 
				
			||||||
            <DrawerTitle>Cart Devices</DrawerTitle>
 | 
					            <DrawerTitle>Selected Devices</DrawerTitle>
 | 
				
			||||||
            <DrawerDescription>Devices in your cart to pay.</DrawerDescription>
 | 
					            <DrawerDescription>Selected devices pay.</DrawerDescription>
 | 
				
			||||||
          </DrawerHeader>
 | 
					          </DrawerHeader>
 | 
				
			||||||
          <div className="flex max-h-[calc(100svh-200px)] flex-col overflow-auto px-4 pb-4 gap-4">
 | 
					          <div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto px-4 pb-4 gap-4">
 | 
				
			||||||
            {devices.map((device) => (
 | 
					            {devices.map((device) => (
 | 
				
			||||||
              <DeviceCard key={device.id} device={device} />
 | 
					              <DeviceCard key={device.id} device={device} />
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="px-4 flex flex-col gap-4">
 | 
				
			||||||
 | 
					            <NumberInput
 | 
				
			||||||
 | 
					              label="Set No of Months"
 | 
				
			||||||
 | 
					              value={months}
 | 
				
			||||||
 | 
					              onChange={(value) => setMonths(value)}
 | 
				
			||||||
 | 
					              maxAllowed={12}
 | 
				
			||||||
 | 
					              isDisabled={devices.length === 0}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            {message && (
 | 
				
			||||||
 | 
					              <span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full">
 | 
				
			||||||
 | 
					                {message}
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
          <DrawerFooter>
 | 
					          <DrawerFooter>
 | 
				
			||||||
            <Link aria-disabled={devices.length === 0} href={devices.length === 0 ? "#" : "/payment"}>
 | 
					            <Button
 | 
				
			||||||
              <Button
 | 
					              onClick={async () => {
 | 
				
			||||||
                onClick={() => {
 | 
					                setDisabled(true)
 | 
				
			||||||
                  setIsOpen(!isOpen)
 | 
					                const payment = await createPayment(data)
 | 
				
			||||||
                }}
 | 
					                setDisabled(false)
 | 
				
			||||||
                className="w-full" disabled={devices.length === 0}>
 | 
					                setDeviceCart([])
 | 
				
			||||||
                Go to payment
 | 
					                setMonths(1)
 | 
				
			||||||
                <CircleDollarSign />
 | 
					                if (payment) {
 | 
				
			||||||
              </Button>
 | 
					                  router.push(`/payments/${payment.id}`);
 | 
				
			||||||
            </Link>
 | 
					                  setIsOpen(!isOpen);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  toast.error("Something went wrong.")
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              className="w-full"
 | 
				
			||||||
 | 
					              disabled={devices.length === 0 || disabled}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {disabled ? (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <Loader2 className="ml-2 animate-spin" />
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  Go to payment
 | 
				
			||||||
 | 
					                  <CircleDollarSign />
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
            <DrawerClose asChild>
 | 
					            <DrawerClose asChild>
 | 
				
			||||||
              <Button variant="outline">Cancel</Button>
 | 
					              <Button variant="outline">Cancel</Button>
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            </DrawerClose>
 | 
					            </DrawerClose>
 | 
				
			||||||
            <Button
 | 
					            <Button
 | 
				
			||||||
              onClick={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                setDeviceCart([])
 | 
					                setDeviceCart([]);
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
              variant="outline">Reset Cart</Button>
 | 
					              variant="outline"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Reset
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
          </DrawerFooter>
 | 
					          </DrawerFooter>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </DrawerContent>
 | 
					      </DrawerContent>
 | 
				
			||||||
    </Drawer>
 | 
					    </Drawer>
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
function DeviceCard({ device }: { device: Device }) {
 | 
					function DeviceCard({ device }: { device: Device }) {
 | 
				
			||||||
  const setDeviceCart = useSetAtom(deviceCartAtom)
 | 
					  const setDeviceCart = useSetAtom(deviceCartAtom);
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="relative flex h-full w-full items-center pr-4 justify-between rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 [&:has(input:is(:disabled))_*]:pointer-events-none">
 | 
					    <div className="relative flex h-full w-full items-center pr-4 justify-between rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20">
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
 | 
					        <label
 | 
				
			||||||
        <label htmlFor="input-33" className="block px-3 pt-2 text-xs font-medium text-foreground">
 | 
					          htmlFor="input-33"
 | 
				
			||||||
 | 
					          className="block px-3 pt-2 text-xs font-medium text-foreground"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          {device.name}
 | 
					          {device.name}
 | 
				
			||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
          className="flex h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
 | 
					          className="flex h-10 opacity-50 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
 | 
				
			||||||
          value={device.mac}
 | 
					          value={device.mac}
 | 
				
			||||||
          readOnly
 | 
					          readOnly
 | 
				
			||||||
 | 
					          disabled
 | 
				
			||||||
          placeholder={"MAC Address"}
 | 
					          placeholder={"MAC Address"}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Button
 | 
					      <Button
 | 
				
			||||||
        onClick={() => {
 | 
					        onClick={() => {
 | 
				
			||||||
          setDeviceCart((prev) => prev.filter((d) => d.id !== device.id))
 | 
					          setDeviceCart((prev) => prev.filter((d) => d.id !== device.id));
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        variant={"destructive"}>
 | 
					        variant={"destructive"}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        Remove
 | 
					        Remove
 | 
				
			||||||
        <Trash2 />
 | 
					        <Trash2 />
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import {
 | 
				
			|||||||
	TableRow,
 | 
						TableRow,
 | 
				
			||||||
} from "@/components/ui/table";
 | 
					} from "@/components/ui/table";
 | 
				
			||||||
import prisma from "@/lib/db";
 | 
					import prisma from "@/lib/db";
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
import AddDevicesToCartButton from "./add-devices-to-cart-button";
 | 
					import AddDevicesToCartButton from "./add-devices-to-cart-button";
 | 
				
			||||||
import Pagination from "./pagination";
 | 
					import Pagination from "./pagination";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,6 +41,11 @@ export async function DevicesTable({
 | 
				
			|||||||
					},
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			],
 | 
								],
 | 
				
			||||||
 | 
								NOT: {
 | 
				
			||||||
 | 
									payment: {
 | 
				
			||||||
 | 
										paid: false
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -63,6 +69,11 @@ export async function DevicesTable({
 | 
				
			|||||||
					},
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			],
 | 
								],
 | 
				
			||||||
 | 
								NOT: {
 | 
				
			||||||
 | 
									payment: {
 | 
				
			||||||
 | 
										paid: false
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		skip: offset,
 | 
							skip: offset,
 | 
				
			||||||
@@ -92,7 +103,24 @@ export async function DevicesTable({
 | 
				
			|||||||
						<TableBody className="overflow-scroll">
 | 
											<TableBody className="overflow-scroll">
 | 
				
			||||||
							{devices.map((device) => (
 | 
												{devices.map((device) => (
 | 
				
			||||||
								<TableRow key={device.id}>
 | 
													<TableRow key={device.id}>
 | 
				
			||||||
									<TableCell className="font-medium">{device.name}</TableCell>
 | 
														<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>
 | 
				
			||||||
 | 
															</div>
 | 
				
			||||||
 | 
														</TableCell>
 | 
				
			||||||
									<TableCell className="font-medium">{device.mac}</TableCell>
 | 
														<TableCell className="font-medium">{device.mac}</TableCell>
 | 
				
			||||||
									<TableCell>
 | 
														<TableCell>
 | 
				
			||||||
										<AddDevicesToCartButton device={device} />
 | 
															<AddDevicesToCartButton device={device} />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
'use client'
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Table,
 | 
					  Table,
 | 
				
			||||||
  TableBody,
 | 
					  TableBody,
 | 
				
			||||||
@@ -7,26 +6,33 @@ import {
 | 
				
			|||||||
  TableFooter,
 | 
					  TableFooter,
 | 
				
			||||||
  TableRow,
 | 
					  TableRow,
 | 
				
			||||||
} from "@/components/ui/table"
 | 
					} from "@/components/ui/table"
 | 
				
			||||||
import { deviceCartAtom } from '@/lib/atoms'
 | 
					import type { BillFormula, Prisma } from "@prisma/client"
 | 
				
			||||||
import type { BillFormula } from "@prisma/client"
 | 
					 | 
				
			||||||
import { useAtomValue } from 'jotai'
 | 
					 | 
				
			||||||
import React from 'react'
 | 
					import React from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function DevicesToPay({ billFormula }: { billFormula?: BillFormula }) {
 | 
					
 | 
				
			||||||
  const devices = useAtomValue(deviceCartAtom)
 | 
					type PaymentWithDevices = Prisma.PaymentGetPayload<{
 | 
				
			||||||
  if (devices.length === 0) {
 | 
					  include: {
 | 
				
			||||||
 | 
					    devices: true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function DevicesToPay({ billFormula, payment }: { billFormula?: BillFormula, payment?: PaymentWithDevices }) {
 | 
				
			||||||
 | 
					  const devices = payment?.devices
 | 
				
			||||||
 | 
					  if (devices?.length === 0) {
 | 
				
			||||||
    return null
 | 
					    return null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const baseAmount = billFormula?.baseAmount ?? 100
 | 
					  const baseAmount = billFormula?.baseAmount ?? 100
 | 
				
			||||||
  const discountPercentage = billFormula?.discountPercentage ?? 75
 | 
					  const discountPercentage = billFormula?.discountPercentage ?? 75
 | 
				
			||||||
  // 100+(n−1)×75
 | 
					  // 100+(n−1)×75
 | 
				
			||||||
  const total = baseAmount + (devices.length - 1) * discountPercentage
 | 
					  const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='w-full'>
 | 
					    <div className='w-full'>
 | 
				
			||||||
      <div className='p-2 flex flex-col gap-2'>
 | 
					      <div className='p-2 flex flex-col gap-2'>
 | 
				
			||||||
        <h3 className='title-bg my-1 font-semibold text-lg'>Devices to pay</h3>
 | 
					        <h3 className='title-bg my-1 font-semibold text-lg'>
 | 
				
			||||||
 | 
					          {!payment?.paid ? 'Devices to pay' : 'Devices Paid'}
 | 
				
			||||||
 | 
					        </h3>
 | 
				
			||||||
        <div className="flex flex-col gap-2">
 | 
					        <div className="flex flex-col gap-2">
 | 
				
			||||||
          {devices.map((device) => (
 | 
					          {devices?.map((device) => (
 | 
				
			||||||
            <div key={device.id} className="bg-muted border rounded p-2 flex gap-2 items-center">
 | 
					            <div key={device.id} className="bg-muted border rounded p-2 flex gap-2 items-center">
 | 
				
			||||||
              <div className="flex flex-col">
 | 
					              <div className="flex flex-col">
 | 
				
			||||||
                <div className="text-sm font-medium">{device.name}</div>
 | 
					                <div className="text-sm font-medium">{device.name}</div>
 | 
				
			||||||
@@ -44,7 +50,7 @@ export default function DevicesToPay({ billFormula }: { billFormula?: BillFormul
 | 
				
			|||||||
          <TableBody>
 | 
					          <TableBody>
 | 
				
			||||||
            <TableRow>
 | 
					            <TableRow>
 | 
				
			||||||
              <TableCell>Total Devices</TableCell>
 | 
					              <TableCell>Total Devices</TableCell>
 | 
				
			||||||
              <TableCell className="text-right">{devices.length}</TableCell>
 | 
					              <TableCell className="text-right">{devices?.length}</TableCell>
 | 
				
			||||||
            </TableRow>
 | 
					            </TableRow>
 | 
				
			||||||
          </TableBody>
 | 
					          </TableBody>
 | 
				
			||||||
          <TableFooter>
 | 
					          <TableFooter>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										50
									
								
								components/number-input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								components/number-input.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					import { Minus, Plus } from "lucide-react";
 | 
				
			||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Group,
 | 
				
			||||||
 | 
					  Input,
 | 
				
			||||||
 | 
					  Label,
 | 
				
			||||||
 | 
					  NumberField,
 | 
				
			||||||
 | 
					} from "react-aria-components";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function NumberInput({
 | 
				
			||||||
 | 
					  maxAllowed,
 | 
				
			||||||
 | 
					  label,
 | 
				
			||||||
 | 
					  value,
 | 
				
			||||||
 | 
					  onChange,
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  isDisabled,
 | 
				
			||||||
 | 
					}: { maxAllowed?: number, label: string; value: number; onChange: (value: number) => void, className?: string, isDisabled?: boolean }) {
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (maxAllowed) {
 | 
				
			||||||
 | 
					      if (value > maxAllowed) {
 | 
				
			||||||
 | 
					        onChange(maxAllowed);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [maxAllowed, value, onChange]);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <NumberField isDisabled={isDisabled} className={cn(className)} value={value} minValue={0} onChange={onChange}>
 | 
				
			||||||
 | 
					      <div className="space-y-2">
 | 
				
			||||||
 | 
					        <Label className="text-sm font-medium text-foreground">{label}</Label>
 | 
				
			||||||
 | 
					        <Group className="relative inline-flex h-9 w-full items-center overflow-hidden whitespace-nowrap rounded-lg border border-input text-sm shadow-sm shadow-black/5 transition-shadow data-[focus-within]:border-ring data-[disabled]:opacity-50 data-[focus-within]:outline-none data-[focus-within]:ring-[3px] data-[focus-within]:ring-ring/20">
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            slot="decrement"
 | 
				
			||||||
 | 
					            className="-ms-px flex aspect-square h-[inherit] items-center justify-center rounded-s-lg border border-input bg-background text-sm text-muted-foreground/80 transition-shadow hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Minus size={16} strokeWidth={2} aria-hidden="true" />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					          <Input className="w-full grow bg-background px-3 py-2 text-center tabular-nums text-foreground focus:outline-none" />
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            slot="increment"
 | 
				
			||||||
 | 
					            className="-me-px flex aspect-square h-[inherit] items-center justify-center rounded-e-lg border border-input bg-background text-sm text-muted-foreground/80 transition-shadow hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Plus size={16} strokeWidth={2} aria-hidden="true" />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </Group>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </NumberField>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										166
									
								
								components/payments-table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								components/payments-table.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Table,
 | 
				
			||||||
 | 
					  TableBody,
 | 
				
			||||||
 | 
					  TableCaption,
 | 
				
			||||||
 | 
					  TableCell,
 | 
				
			||||||
 | 
					  TableFooter,
 | 
				
			||||||
 | 
					  TableHead,
 | 
				
			||||||
 | 
					  TableHeader,
 | 
				
			||||||
 | 
					  TableRow,
 | 
				
			||||||
 | 
					} from "@/components/ui/table";
 | 
				
			||||||
 | 
					import prisma from "@/lib/db";
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Calendar } from "lucide-react";
 | 
				
			||||||
 | 
					import Pagination from "./pagination";
 | 
				
			||||||
 | 
					import { Badge } from "./ui/badge";
 | 
				
			||||||
 | 
					import { Button } from "./ui/button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function PaymentsTable({
 | 
				
			||||||
 | 
					  searchParams,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  searchParams: Promise<{
 | 
				
			||||||
 | 
					    query: string;
 | 
				
			||||||
 | 
					    page: number;
 | 
				
			||||||
 | 
					    sortBy: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const query = (await searchParams)?.query || "";
 | 
				
			||||||
 | 
					  const page = (await searchParams)?.page;
 | 
				
			||||||
 | 
					  const totalPayments = await prisma.payment.count({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      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 payments = await prisma.payment.findMany({
 | 
				
			||||||
 | 
					    where: {
 | 
				
			||||||
 | 
					      OR: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          devices: {
 | 
				
			||||||
 | 
					            every: {
 | 
				
			||||||
 | 
					              name: {
 | 
				
			||||||
 | 
					                contains: query || "",
 | 
				
			||||||
 | 
					                mode: "insensitive",
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    include: {
 | 
				
			||||||
 | 
					      devices: true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    skip: offset,
 | 
				
			||||||
 | 
					    take: limit,
 | 
				
			||||||
 | 
					    orderBy: {
 | 
				
			||||||
 | 
					      createdAt: "desc",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      {payments.length === 0 ? (
 | 
				
			||||||
 | 
					        <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
 | 
				
			||||||
 | 
					          <h3>No Payments yet.</h3>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <Table className="overflow-scroll">
 | 
				
			||||||
 | 
					            <TableCaption>Table of all devices.</TableCaption>
 | 
				
			||||||
 | 
					            <TableHeader>
 | 
				
			||||||
 | 
					              <TableRow>
 | 
				
			||||||
 | 
					                <TableHead>Details</TableHead>
 | 
				
			||||||
 | 
					                <TableHead>Duration</TableHead>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <TableHead>Amount</TableHead>
 | 
				
			||||||
 | 
					              </TableRow>
 | 
				
			||||||
 | 
					            </TableHeader>
 | 
				
			||||||
 | 
					            <TableBody className="overflow-scroll">
 | 
				
			||||||
 | 
					              {payments.map((payment) => (
 | 
				
			||||||
 | 
					                <TableRow key={payment.id}>
 | 
				
			||||||
 | 
					                  <TableCell>
 | 
				
			||||||
 | 
					                    <div className="flex flex-col items-start title-bg border rounded p-2">
 | 
				
			||||||
 | 
					                      <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                        <Calendar size={16} opacity={0.5} />
 | 
				
			||||||
 | 
					                        <span className="text-muted-foreground">
 | 
				
			||||||
 | 
					                          {new Date(payment.createdAt).toLocaleDateString("en-US", {
 | 
				
			||||||
 | 
					                            month: "short",
 | 
				
			||||||
 | 
					                            day: "2-digit",
 | 
				
			||||||
 | 
					                            year: "numeric",
 | 
				
			||||||
 | 
					                          })}
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      <div className="flex items-center gap-2 mt-2">
 | 
				
			||||||
 | 
					                        <Link className="font-medium hover:underline" href={`/payments/${payment.id}`}>
 | 
				
			||||||
 | 
					                          <Button size={"sm"} variant="outline">
 | 
				
			||||||
 | 
					                            View Details
 | 
				
			||||||
 | 
					                          </Button>
 | 
				
			||||||
 | 
					                        </Link>
 | 
				
			||||||
 | 
					                        <Badge className="p-2" variant={payment.paid ? "outline" : "secondary"}>
 | 
				
			||||||
 | 
					                          {payment.paid ? "Paid" : "Unpaid"}
 | 
				
			||||||
 | 
					                        </Badge>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div className="bg-white dark:bg-black p-2 rounded mt-2 w-full">
 | 
				
			||||||
 | 
					                        <h3 className="text-sm font-medium">Devices</h3>
 | 
				
			||||||
 | 
					                        <ol className="list-disc list-inside text-sm">
 | 
				
			||||||
 | 
					                          {payment.devices.map((device) => (
 | 
				
			||||||
 | 
					                            <li key={device.id} className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					                              {device.name}
 | 
				
			||||||
 | 
					                            </li>
 | 
				
			||||||
 | 
					                          ))}
 | 
				
			||||||
 | 
					                        </ol>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </TableCell>
 | 
				
			||||||
 | 
					                  <TableCell className="font-medium">
 | 
				
			||||||
 | 
					                    {payment.numberOfMonths} Months
 | 
				
			||||||
 | 
					                  </TableCell>
 | 
				
			||||||
 | 
					                  <TableCell>
 | 
				
			||||||
 | 
					                    <span className="font-semibold pr-2">
 | 
				
			||||||
 | 
					                      {payment.amount.toFixed(2)}
 | 
				
			||||||
 | 
					                    </span>
 | 
				
			||||||
 | 
					                    MVR
 | 
				
			||||||
 | 
					                  </TableCell>
 | 
				
			||||||
 | 
					                </TableRow>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </TableBody>
 | 
				
			||||||
 | 
					            <TableFooter>
 | 
				
			||||||
 | 
					              <TableRow>
 | 
				
			||||||
 | 
					                <TableCell colSpan={2}>
 | 
				
			||||||
 | 
					                  {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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								components/price-calculator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								components/price-calculator.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  discountPercentageAtom,
 | 
				
			||||||
 | 
					  formulaResultAtom,
 | 
				
			||||||
 | 
					  initialPriceAtom,
 | 
				
			||||||
 | 
					  numberOfDaysAtom,
 | 
				
			||||||
 | 
					  numberOfDevicesAtom,
 | 
				
			||||||
 | 
					} from "@/lib/atoms";
 | 
				
			||||||
 | 
					import { useAtom } from "jotai";
 | 
				
			||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					import NumberInput from "./number-input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function PriceCalculator() {
 | 
				
			||||||
 | 
					  const [initialPrice, setInitialPrice] = useAtom(initialPriceAtom);
 | 
				
			||||||
 | 
					  const [discountPercentage, setDiscountPercentage] = useAtom(
 | 
				
			||||||
 | 
					    discountPercentageAtom,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const [numberOfDevices, setNumberOfDevices] = useAtom(numberOfDevicesAtom);
 | 
				
			||||||
 | 
					  const [numberOfDays, setNumberOfDays] = useAtom(numberOfDaysAtom);
 | 
				
			||||||
 | 
					  const [formulaResult, setFormulaResult] = useAtom(formulaResultAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const basePrice = initialPrice + (numberOfDevices - 1) * discountPercentage;
 | 
				
			||||||
 | 
					    setFormulaResult(
 | 
				
			||||||
 | 
					      `Price for ${numberOfDevices} device(s) over ${numberOfDays} day(s): MVR ${basePrice.toFixed(2)}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }, [
 | 
				
			||||||
 | 
					    initialPrice,
 | 
				
			||||||
 | 
					    discountPercentage,
 | 
				
			||||||
 | 
					    numberOfDevices,
 | 
				
			||||||
 | 
					    numberOfDays,
 | 
				
			||||||
 | 
					    setFormulaResult,
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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>
 | 
				
			||||||
 | 
					      <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
 | 
				
			||||||
 | 
					        {/* Initial Price Input */}
 | 
				
			||||||
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          label="Initial Price"
 | 
				
			||||||
 | 
					          value={initialPrice}
 | 
				
			||||||
 | 
					          onChange={(value) => setInitialPrice(value)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        {/* Number of Devices Input */}
 | 
				
			||||||
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          label="Number of Devices"
 | 
				
			||||||
 | 
					          value={numberOfDevices}
 | 
				
			||||||
 | 
					          onChange={(value) => setNumberOfDevices(value)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        {/* Number of Days Input */}
 | 
				
			||||||
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          label="Number of Days"
 | 
				
			||||||
 | 
					          value={numberOfDays}
 | 
				
			||||||
 | 
					          onChange={(value) => setNumberOfDays(value)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/* Discount Percentage Input */}
 | 
				
			||||||
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          label="Discount Percentage"
 | 
				
			||||||
 | 
					          value={discountPercentage}
 | 
				
			||||||
 | 
					          onChange={(value) => setDiscountPercentage(value)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className="mt-4">
 | 
				
			||||||
 | 
					        <div className="title-bg relative rounded-lg border border-input shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 [&:has(input:is(:disabled))_*]:pointer-events-none">
 | 
				
			||||||
 | 
					          <label
 | 
				
			||||||
 | 
					            htmlFor=""
 | 
				
			||||||
 | 
					            className="block px-3 pt-2 text-md font-medium text-foreground"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            Total
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            className="flex font-mono font-semibold h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
 | 
				
			||||||
 | 
					            value={formulaResult}
 | 
				
			||||||
 | 
					            readOnly
 | 
				
			||||||
 | 
					            placeholder={"Result"}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,6 +9,7 @@ export const initialPriceAtom = atom(100);
 | 
				
			|||||||
export const discountPercentageAtom = atom(75);
 | 
					export const discountPercentageAtom = atom(75);
 | 
				
			||||||
export const numberOfDevicesAtom = atom(1);
 | 
					export const numberOfDevicesAtom = atom(1);
 | 
				
			||||||
export const numberOfDaysAtom = atom(30);
 | 
					export const numberOfDaysAtom = atom(30);
 | 
				
			||||||
 | 
					export const numberOfMonths = atom(1);
 | 
				
			||||||
export const formulaResultAtom = atom("");
 | 
					export const formulaResultAtom = atom("");
 | 
				
			||||||
export const deviceCartAtom = atom<Device[]>([]);
 | 
					export const deviceCartAtom = atom<Device[]>([]);
 | 
				
			||||||
export const cartDrawerOpenAtom = atom(false);
 | 
					export const cartDrawerOpenAtom = atom(false);
 | 
				
			||||||
@@ -18,6 +19,7 @@ export const atoms = {
 | 
				
			|||||||
	discountPercentageAtom,
 | 
						discountPercentageAtom,
 | 
				
			||||||
	numberOfDevicesAtom,
 | 
						numberOfDevicesAtom,
 | 
				
			||||||
	numberOfDaysAtom,
 | 
						numberOfDaysAtom,
 | 
				
			||||||
 | 
						numberOfMonths,
 | 
				
			||||||
	formulaResultAtom,
 | 
						formulaResultAtom,
 | 
				
			||||||
	deviceCartAtom,
 | 
						deviceCartAtom,
 | 
				
			||||||
	cartDrawerOpenAtom,
 | 
						cartDrawerOpenAtom,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,3 +12,13 @@ export async function AdminAuthGuard() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return true;
 | 
						return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function hasSession() {
 | 
				
			||||||
 | 
						const session = await auth.api.getSession({
 | 
				
			||||||
 | 
							headers: await headers(),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (!session) {
 | 
				
			||||||
 | 
							return redirect("/login");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { phoneNumber } from "better-auth/plugins";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const prisma = new PrismaClient();
 | 
					const prisma = new PrismaClient();
 | 
				
			||||||
export const auth = betterAuth({
 | 
					export const auth = betterAuth({
 | 
				
			||||||
 | 
						trustedOrigins: ["http://localhost:3000", "http://192.168.18.194:3000"],
 | 
				
			||||||
	user: {
 | 
						user: {
 | 
				
			||||||
		additionalFields: {
 | 
							additionalFields: {
 | 
				
			||||||
			role: {
 | 
								role: {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								lib/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					export type PaymentType = {
 | 
				
			||||||
 | 
						numberOfMonths: number;
 | 
				
			||||||
 | 
						userId: string;
 | 
				
			||||||
 | 
						deviceIds: string[];
 | 
				
			||||||
 | 
						amount: number;
 | 
				
			||||||
 | 
						paid: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										23
									
								
								prisma/migrations/20241207032927_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								prisma/migrations/20241207032927_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					  Warnings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - You are about to drop the column `billId` on the `Device` table. All the data in the column will be lost.
 | 
				
			||||||
 | 
					  - You are about to drop the column `name` on the `Payment` table. All the data in the column will be lost.
 | 
				
			||||||
 | 
					  - Added the required column `numberOfMonths` to the `Payment` table without a default value. This is not possible if the table is not empty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					-- DropForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "Device" DROP CONSTRAINT "Device_billId_fkey";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "Device" DROP COLUMN "billId",
 | 
				
			||||||
 | 
					ADD COLUMN     "expiryDate" TIMESTAMP(3),
 | 
				
			||||||
 | 
					ADD COLUMN     "paymentId" TEXT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "Payment" DROP COLUMN "name",
 | 
				
			||||||
 | 
					ADD COLUMN     "numberOfMonths" INTEGER NOT NULL,
 | 
				
			||||||
 | 
					ALTER COLUMN "amount" SET DATA TYPE DOUBLE PRECISION;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AddForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "Device" ADD CONSTRAINT "Device_paymentId_fkey" FOREIGN KEY ("paymentId") REFERENCES "Payment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
 | 
				
			||||||
							
								
								
									
										2
									
								
								prisma/migrations/20241207051101_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								prisma/migrations/20241207051101_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "Payment" ADD COLUMN     "paidAt" TIMESTAMP(3);
 | 
				
			||||||
							
								
								
									
										2
									
								
								prisma/migrations/20241207051242_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								prisma/migrations/20241207051242_add/migration.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "Payment" ADD COLUMN     "expiresAt" TIMESTAMP(3);
 | 
				
			||||||
@@ -109,29 +109,31 @@ model Island {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Device {
 | 
					model Device {
 | 
				
			||||||
  id        String   @id @default(cuid())
 | 
					  id         String    @id @default(cuid())
 | 
				
			||||||
  name      String
 | 
					  name       String
 | 
				
			||||||
  mac       String
 | 
					  mac        String
 | 
				
			||||||
  isActive  Boolean  @default(false)
 | 
					  isActive   Boolean   @default(false)
 | 
				
			||||||
  createdAt DateTime @default(now())
 | 
					  expiryDate DateTime?
 | 
				
			||||||
  updatedAt DateTime @updatedAt
 | 
					  createdAt  DateTime  @default(now())
 | 
				
			||||||
  User      User?    @relation(fields: [userId], references: [id])
 | 
					  updatedAt  DateTime  @updatedAt
 | 
				
			||||||
  userId    String?
 | 
					  User       User?     @relation(fields: [userId], references: [id])
 | 
				
			||||||
  Bill      Payment? @relation(fields: [billId], references: [id])
 | 
					  userId     String?
 | 
				
			||||||
  billId    String?
 | 
					  payment    Payment?  @relation(fields: [paymentId], references: [id])
 | 
				
			||||||
 | 
					  paymentId  String?
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Payment {
 | 
					model Payment {
 | 
				
			||||||
  id     String  @id @default(cuid())
 | 
					  id             String    @id @default(cuid())
 | 
				
			||||||
  name   String
 | 
					  numberOfMonths Int
 | 
				
			||||||
  amount Int
 | 
					  amount         Float
 | 
				
			||||||
  paid   Boolean @default(false)
 | 
					  paid           Boolean   @default(false)
 | 
				
			||||||
  user   User    @relation(fields: [userId], references: [id])
 | 
					  user           User      @relation(fields: [userId], references: [id])
 | 
				
			||||||
 | 
					  paidAt         DateTime?
 | 
				
			||||||
  createdAt DateTime @default(now())
 | 
					  expiresAt      DateTime?
 | 
				
			||||||
  updatedAt DateTime @updatedAt
 | 
					  createdAt      DateTime  @default(now())
 | 
				
			||||||
  devices   Device[]
 | 
					  updatedAt      DateTime  @updatedAt
 | 
				
			||||||
  userId    String
 | 
					  devices        Device[]
 | 
				
			||||||
 | 
					  userId         String
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model BillFormula {
 | 
					model BillFormula {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user