+
{devices.map((device) => (
))}
+
+ setMonths(value)}
+ maxAllowed={12}
+ isDisabled={devices.length === 0}
+ />
+ {message && (
+
+ {message}
+
+ )}
+
-
-
-
+
-
-
+ variant="outline"
+ >
+ Reset
+
- )
+ );
}
-
function DeviceCard({ device }: { device: Device }) {
- const setDeviceCart = useSetAtom(deviceCartAtom)
+ const setDeviceCart = useSetAtom(deviceCartAtom);
return (
-
+
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/components/devices-table.tsx b/components/devices-table.tsx
index 9b8ffb6..6cbdc79 100644
--- a/components/devices-table.tsx
+++ b/components/devices-table.tsx
@@ -9,6 +9,7 @@ import {
TableRow,
} from "@/components/ui/table";
import prisma from "@/lib/db";
+import Link from "next/link";
import AddDevicesToCartButton from "./add-devices-to-cart-button";
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,
@@ -92,7 +103,24 @@ export async function DevicesTable({
{devices.map((device) => (
- {device.name}
+
+
+
+ {device.name}
+
+
+ Active until{" "}
+ {new Date().toLocaleDateString("en-US", {
+ month: "short",
+ day: "2-digit",
+ year: "numeric",
+ })}
+
+
+
{device.mac}
diff --git a/components/devices-to-pay.tsx b/components/devices-to-pay.tsx
index 2f206db..6d1227b 100644
--- a/components/devices-to-pay.tsx
+++ b/components/devices-to-pay.tsx
@@ -1,4 +1,3 @@
-'use client'
import {
Table,
TableBody,
@@ -7,26 +6,33 @@ import {
TableFooter,
TableRow,
} from "@/components/ui/table"
-import { deviceCartAtom } from '@/lib/atoms'
-import type { BillFormula } from "@prisma/client"
-import { useAtomValue } from 'jotai'
+import type { BillFormula, Prisma } from "@prisma/client"
import React from 'react'
-export default function DevicesToPay({ billFormula }: { billFormula?: BillFormula }) {
- const devices = useAtomValue(deviceCartAtom)
- if (devices.length === 0) {
+
+type PaymentWithDevices = Prisma.PaymentGetPayload<{
+ include: {
+ devices: true
+ }
+}>
+
+export default function DevicesToPay({ billFormula, payment }: { billFormula?: BillFormula, payment?: PaymentWithDevices }) {
+ 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) * discountPercentage
+ const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage
return (
-
Devices to pay
+
+ {!payment?.paid ? 'Devices to pay' : 'Devices Paid'}
+
- {devices.map((device) => (
+ {devices?.map((device) => (
{device.name}
@@ -44,7 +50,7 @@ export default function DevicesToPay({ billFormula }: { billFormula?: BillFormul
Total Devices
- {devices.length}
+ {devices?.length}
diff --git a/components/number-input.tsx b/components/number-input.tsx
new file mode 100644
index 0000000..e75099e
--- /dev/null
+++ b/components/number-input.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/payments-table.tsx b/components/payments-table.tsx
new file mode 100644
index 0000000..0c133d8
--- /dev/null
+++ b/components/payments-table.tsx
@@ -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 (
+
+ {payments.length === 0 ? (
+
+
No Payments yet.
+
+ ) : (
+ <>
+
+ Table of all devices.
+
+
+ Details
+ Duration
+
+ Amount
+
+
+
+ {payments.map((payment) => (
+
+
+
+
+
+
+ {new Date(payment.createdAt).toLocaleDateString("en-US", {
+ month: "short",
+ day: "2-digit",
+ year: "numeric",
+ })}
+
+
+
+
+
+
+
+
+ {payment.paid ? "Paid" : "Unpaid"}
+
+
+
+
Devices
+
+ {payment.devices.map((device) => (
+ -
+ {device.name}
+
+ ))}
+
+
+
+
+
+ {payment.numberOfMonths} Months
+
+
+
+ {payment.amount.toFixed(2)}
+
+ MVR
+
+
+ ))}
+
+
+
+
+ {query.length > 0 && (
+
+ Showing {payments.length} locations for "{query}
+ "
+
+ )}
+
+
+ {totalPayments} payments
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/components/price-calculator.tsx b/components/price-calculator.tsx
new file mode 100644
index 0000000..668028f
--- /dev/null
+++ b/components/price-calculator.tsx
@@ -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 (
+
+
+
Price Calculator
+
+
+ {/* Initial Price Input */}
+ setInitialPrice(value)}
+ />
+ {/* Number of Devices Input */}
+ setNumberOfDevices(value)}
+ />
+ {/* Number of Days Input */}
+ setNumberOfDays(value)}
+ />
+
+ {/* Discount Percentage Input */}
+ setDiscountPercentage(value)}
+ />
+
+
+
+
+ );
+}
+
diff --git a/lib/atoms.ts b/lib/atoms.ts
index 19a4b66..620d532 100644
--- a/lib/atoms.ts
+++ b/lib/atoms.ts
@@ -9,6 +9,7 @@ export const initialPriceAtom = atom(100);
export const discountPercentageAtom = atom(75);
export const numberOfDevicesAtom = atom(1);
export const numberOfDaysAtom = atom(30);
+export const numberOfMonths = atom(1);
export const formulaResultAtom = atom("");
export const deviceCartAtom = atom([]);
export const cartDrawerOpenAtom = atom(false);
@@ -18,6 +19,7 @@ export const atoms = {
discountPercentageAtom,
numberOfDevicesAtom,
numberOfDaysAtom,
+ numberOfMonths,
formulaResultAtom,
deviceCartAtom,
cartDrawerOpenAtom,
diff --git a/lib/auth-guard.ts b/lib/auth-guard.ts
index 720e6aa..3dcfbdc 100644
--- a/lib/auth-guard.ts
+++ b/lib/auth-guard.ts
@@ -12,3 +12,13 @@ export async function AdminAuthGuard() {
}
return true;
}
+
+export async function hasSession() {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+ if (!session) {
+ return redirect("/login");
+ }
+ return true;
+}
diff --git a/lib/auth.ts b/lib/auth.ts
index c0a31e6..07c2772 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -6,6 +6,7 @@ import { phoneNumber } from "better-auth/plugins";
const prisma = new PrismaClient();
export const auth = betterAuth({
+ trustedOrigins: ["http://localhost:3000", "http://192.168.18.194:3000"],
user: {
additionalFields: {
role: {
diff --git a/lib/types.ts b/lib/types.ts
new file mode 100644
index 0000000..053fdea
--- /dev/null
+++ b/lib/types.ts
@@ -0,0 +1,7 @@
+export type PaymentType = {
+ numberOfMonths: number;
+ userId: string;
+ deviceIds: string[];
+ amount: number;
+ paid: boolean;
+};
diff --git a/prisma/migrations/20241207032927_add/migration.sql b/prisma/migrations/20241207032927_add/migration.sql
new file mode 100644
index 0000000..4f5ab2a
--- /dev/null
+++ b/prisma/migrations/20241207032927_add/migration.sql
@@ -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;
diff --git a/prisma/migrations/20241207051101_add/migration.sql b/prisma/migrations/20241207051101_add/migration.sql
new file mode 100644
index 0000000..ea7fcd2
--- /dev/null
+++ b/prisma/migrations/20241207051101_add/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Payment" ADD COLUMN "paidAt" TIMESTAMP(3);
diff --git a/prisma/migrations/20241207051242_add/migration.sql b/prisma/migrations/20241207051242_add/migration.sql
new file mode 100644
index 0000000..496f8b4
--- /dev/null
+++ b/prisma/migrations/20241207051242_add/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Payment" ADD COLUMN "expiresAt" TIMESTAMP(3);
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e598a7e..6b731de 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -109,29 +109,31 @@ model Island {
}
model Device {
- id String @id @default(cuid())
- name String
- mac String
- isActive Boolean @default(false)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- User User? @relation(fields: [userId], references: [id])
- userId String?
- Bill Payment? @relation(fields: [billId], references: [id])
- billId String?
+ id String @id @default(cuid())
+ name String
+ mac String
+ isActive Boolean @default(false)
+ expiryDate DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ User User? @relation(fields: [userId], references: [id])
+ userId String?
+ payment Payment? @relation(fields: [paymentId], references: [id])
+ paymentId String?
}
model Payment {
- id String @id @default(cuid())
- name String
- amount Int
- paid Boolean @default(false)
- user User @relation(fields: [userId], references: [id])
-
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- devices Device[]
- userId String
+ id String @id @default(cuid())
+ numberOfMonths Int
+ amount Float
+ paid Boolean @default(false)
+ user User @relation(fields: [userId], references: [id])
+ paidAt DateTime?
+ expiresAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ devices Device[]
+ userId String
}
model BillFormula {