From e815da495a7fcbdc5ff4cddb7150e16f242aefce Mon Sep 17 00:00:00 2001 From: i701 Date: Sat, 7 Dec 2024 14:09:53 +0500 Subject: [PATCH] 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. --- actions/payment.ts | 26 +++ app/(dashboard)/payment/page.tsx | 24 --- app/(dashboard)/payments/[paymentId]/page.tsx | 37 ++++ app/(dashboard)/payments/page.tsx | 41 ++-- app/(dashboard)/price-calculator/page.tsx | 127 +------------ app/favicon.ico | Bin 15406 -> 15406 bytes components/add-devices-to-cart-button.tsx | 4 +- components/auth/application-layout.tsx | 4 +- components/device-cart.tsx | 179 +++++++++++++----- components/devices-table.tsx | 30 ++- components/devices-to-pay.tsx | 28 +-- components/number-input.tsx | 50 +++++ components/payments-table.tsx | 166 ++++++++++++++++ components/price-calculator.tsx | 88 +++++++++ lib/atoms.ts | 2 + lib/auth-guard.ts | 10 + lib/auth.ts | 1 + lib/types.ts | 7 + .../20241207032927_add/migration.sql | 23 +++ .../20241207051101_add/migration.sql | 2 + .../20241207051242_add/migration.sql | 2 + prisma/schema.prisma | 42 ++-- 22 files changed, 651 insertions(+), 242 deletions(-) create mode 100644 actions/payment.ts delete mode 100644 app/(dashboard)/payment/page.tsx create mode 100644 app/(dashboard)/payments/[paymentId]/page.tsx create mode 100644 components/number-input.tsx create mode 100644 components/payments-table.tsx create mode 100644 components/price-calculator.tsx create mode 100644 lib/types.ts create mode 100644 prisma/migrations/20241207032927_add/migration.sql create mode 100644 prisma/migrations/20241207051101_add/migration.sql create mode 100644 prisma/migrations/20241207051242_add/migration.sql diff --git a/actions/payment.ts b/actions/payment.ts new file mode 100644 index 0000000..e1f75f9 --- /dev/null +++ b/actions/payment.ts @@ -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; +} diff --git a/app/(dashboard)/payment/page.tsx b/app/(dashboard)/payment/page.tsx deleted file mode 100644 index 9792ebe..0000000 --- a/app/(dashboard)/payment/page.tsx +++ /dev/null @@ -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 ( -
-
-

- Payment -

-
- -
- - -
-
- ) -} diff --git a/app/(dashboard)/payments/[paymentId]/page.tsx b/app/(dashboard)/payments/[paymentId]/page.tsx new file mode 100644 index 0000000..40ce820 --- /dev/null +++ b/app/(dashboard)/payments/[paymentId]/page.tsx @@ -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 ( +
+
+

Payment

+
+ +
+ +
+
+ ); +} diff --git a/app/(dashboard)/payments/page.tsx b/app/(dashboard)/payments/page.tsx index 0d3e249..ec73203 100644 --- a/app/(dashboard)/payments/page.tsx +++ b/app/(dashboard)/payments/page.tsx @@ -1,14 +1,33 @@ -"use client"; -import { authClient } from "@/lib/auth-client"; -import React from "react"; +import { PaymentsTable } from "@/components/payments-table"; +import Search from "@/components/search"; +import { Suspense } from "react"; -export default function MyPayments() { - const session = authClient.useSession(); +export default async function Devices({ + searchParams, +}: { + searchParams: Promise<{ + query: string; + page: number; + sortBy: string; + status: string; + }>; +}) { + const query = (await searchParams)?.query || ""; + return ( +
+
+

My Payments

+
- return ( -
-

Client session

-
{JSON.stringify(session.data, null, 2)}
-
- ); +
+ +
+ + + +
+ ); } diff --git a/app/(dashboard)/price-calculator/page.tsx b/app/(dashboard)/price-calculator/page.tsx index 3218101..8a4d414 100644 --- a/app/(dashboard)/price-calculator/page.tsx +++ b/app/(dashboard)/price-calculator/page.tsx @@ -1,125 +1,8 @@ -"use client"; -import { - 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, - ]); +import PriceCalculator from '@/components/price-calculator' +import React from 'react' +export default function Pricing() { 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)} - /> -
- -
-
- - -
-
-
- ); -} - -// Dependencies: pnpm install lucide-react react-aria-components - -function NumberInput({ - label, - value, - onChange, -}: { label: string; value: number; onChange: (value: number) => void }) { - return ( - -
- - - - - - -
-
- ); + + ) } diff --git a/app/favicon.ico b/app/favicon.ico index 45767d427cc943d6bb17386f1793e8da214f1735..86f721702aa8741da5932c8d00f4db7b3b326dbb 100644 GIT binary patch literal 15406 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 Zw42>xE+Te*SNN1Dan=0)$p5=7@Sig#DSZF{ literal 15406 zcmeHN=aXec6@Qlg0s6rz{k9(rSXzoz=1_tO5XGF(6-)?%h*?xrFbj)f1{FjURDvji zt~nrVoVc^QlXqrz!kgKdoq6HD`@((4-|zJ8d;5m>=53%XtL)u3xBKKyr@K#gq42Q6 z!wZjmWC7nTg$o~1C_JlBC~Vnc%BMW4P`DAWQ%?=?Pbd@~cyys~3TVIr4g<$0H%VJG z=_mdp7Vcn?4T%X1Y1_!?KbCX(EsvoiD^otXr6r#KOp0?U6(E?deL+_0cgc~)@50}` zvf8wHe(#l8ZyUb9m(|97vQWQC+Oc3+@P8!bR-aWADqt}r!siI&*+e}CW$;HJbEST} z$_%6?^`I!FU>8Ul`de@Q3$R6%M?FjRU&_(uUyTgPhJE$mu*}x3mAQ@YOR;{dR9ho5 zQ{E;srERiS|Ff$0S`)I?{D(B!M`fn=6{)qRRpxTzZv7Tl?v+Dx*UO>OHIQ+eOfPMd z$(e1^Xs$#4lD19I_EFy-EBzwBXozO}2ylN;*m`LQu%_DP*O$bv7bTD? zVOurk(t4B8t-qR)6d%bpxOgy9RF+>AaKIXr7R{efkn;(%px+y_Ydxks%0DTV31-W^ZZUOe2RM~SW!5)vT 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 {devices.some((d) => d.id === device.id) ? ( <> - Added + Selected ) : ( <> - Add to cart + Select device diff --git a/components/auth/application-layout.tsx b/components/auth/application-layout.tsx index ff648f2..147a255 100644 --- a/components/auth/application-layout.tsx +++ b/components/auth/application-layout.tsx @@ -9,6 +9,7 @@ import { SidebarTrigger, } from "@/components/ui/sidebar"; import { auth } from "@/lib/auth"; +import prisma from "@/lib/db"; import { headers } from "next/headers"; import { AccountPopover } from "./account-popver"; @@ -18,6 +19,7 @@ export async function ApplicationLayout({ const session = await auth.api.getSession({ headers: await headers() }); + const billFormula = await prisma.billFormula.findFirst(); return ( @@ -30,7 +32,7 @@ export async function ApplicationLayout({
- +
diff --git a/components/device-cart.tsx b/components/device-cart.tsx index 4d05475..5249bea 100644 --- a/components/device-cart.tsx +++ b/components/device-cart.tsx @@ -1,8 +1,7 @@ -"use client" +"use client"; -import * as React from "react" - -import { Button } from "@/components/ui/button" +import { createPayment } from "@/actions/payment"; +import { Button } from "@/components/ui/button"; import { Drawer, DrawerClose, @@ -12,98 +11,178 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger, -} from "@/components/ui/drawer" -import { cartDrawerOpenAtom, deviceCartAtom } from "@/lib/atoms" -import type { Device } from "@prisma/client" -import { useAtom, useAtomValue, useSetAtom } from "jotai" -import { CircleDollarSign, ShoppingCart, Trash2 } from "lucide-react" -import Link from "next/link" -import { usePathname } from "next/navigation" +} from "@/components/ui/drawer"; +import { + cartDrawerOpenAtom, + deviceCartAtom, + numberOfMonths, +} from "@/lib/atoms"; +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() - const devices = useAtomValue(deviceCartAtom) - const setDeviceCart = useSetAtom(deviceCartAtom) - const [isOpen, setIsOpen] = useAtom(cartDrawerOpenAtom) + +export function DeviceCartDrawer({ + billFormula, +}: { + 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") { - 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 ( -
- Cart Devices - Devices in your cart to pay. + Selected Devices + Selected devices pay. -
+
{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) => ( +
  1. + {device.name} +
  2. + ))} +
+
+
+
+ + {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 {