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