mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-10-05 09:55:25 +00:00
Enhance device management and user experience features
- Updated `package.json` to include the latest version of `@radix-ui/react-separator` and added `moment` for date handling. - Modified `blockDevice` function in `omada-actions.ts` to include a `blockedBy` parameter, allowing differentiation between admin and parent actions. - Refactored `payment.ts` to include expiry date handling for devices during payment processing. - Improved `DevicesTable` and `ClickableRow` components to support admin functionalities and enhance device interaction. - Updated `BlockDeviceDialog` to accept an `admin` prop, allowing for tailored blocking actions based on user role. - Enhanced UI components for better consistency and responsiveness across the dashboard. These changes improve the overall functionality and maintainability of the application, providing a better user experience in device management.
This commit is contained in:
@@ -36,6 +36,11 @@ export async function ApplicationLayout({
|
||||
<div className="flex items-center gap-2 ">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{session?.user.role === "ADMIN" && (
|
||||
<span className="text-sm font-mono px-2 p-1 rounded-md bg-green-500/10 text-green-900">
|
||||
Welcome back {session?.user.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { blockDevice } from "@/actions/omada-actions"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { blockDevice } from "@/actions/omada-actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,127 +9,174 @@ import {
|
||||
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"
|
||||
|
||||
|
||||
} 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 { TextShimmer } from "./ui/text-shimmer";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
|
||||
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)
|
||||
export default function BlockDeviceDialog({
|
||||
device,
|
||||
type,
|
||||
admin,
|
||||
}: { device: Device; type: "block" | "unblock"; admin?: boolean }) {
|
||||
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!"
|
||||
setDisabled(true);
|
||||
console.log(data);
|
||||
toast.promise(
|
||||
blockDevice({
|
||||
macAddress: device.mac,
|
||||
type: type,
|
||||
reason: data.reasonForBlocking,
|
||||
blockedBy: "ADMIN",
|
||||
// reason: data.reasonForBlocking,
|
||||
}),
|
||||
{
|
||||
loading: "Blocking...",
|
||||
success: () => {
|
||||
setDisabled(false);
|
||||
setOpen((prev) => !prev);
|
||||
return "Blocked!";
|
||||
},
|
||||
error: (error) => {
|
||||
setDisabled(false);
|
||||
return error || "Something went wrong";
|
||||
},
|
||||
},
|
||||
error: (error) => {
|
||||
setDisabled(false)
|
||||
return error || "Something went wrong"
|
||||
},
|
||||
})
|
||||
setDisabled(false)
|
||||
|
||||
}
|
||||
);
|
||||
setDisabled(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{device.blocked ? (
|
||||
<Button onClick={
|
||||
() => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
toast.promise(blockDevice({
|
||||
macAddress: device.mac,
|
||||
type: "unblock",
|
||||
reason: '',
|
||||
}), {
|
||||
loading: "unblockinig...",
|
||||
success: () => {
|
||||
setDisabled(false);
|
||||
return "Unblocked!";
|
||||
toast.promise(
|
||||
blockDevice({
|
||||
macAddress: device.mac,
|
||||
type: "unblock",
|
||||
reason: "",
|
||||
}),
|
||||
{
|
||||
loading: "unblockinig...",
|
||||
success: () => {
|
||||
setDisabled(false);
|
||||
return "Unblocked!";
|
||||
},
|
||||
error: () => {
|
||||
setDisabled(false);
|
||||
return "Something went wrong";
|
||||
},
|
||||
},
|
||||
error: () => {
|
||||
setDisabled(false);
|
||||
return "Something went wrong";
|
||||
},
|
||||
})
|
||||
}
|
||||
}>
|
||||
{disabled ? (
|
||||
<TextShimmer>
|
||||
Unblocking
|
||||
</TextShimmer>
|
||||
) : "Unblock"}
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
{disabled ? <TextShimmer>Unblocking</TextShimmer> : "Unblock"}
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={disabled} variant="destructive">
|
||||
<>
|
||||
{!admin ? (
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
toast.promise(
|
||||
blockDevice({
|
||||
macAddress: device.mac,
|
||||
type: "block",
|
||||
reason: "",
|
||||
blockedBy: "PARENT",
|
||||
}),
|
||||
{
|
||||
loading: "blocking...",
|
||||
success: () => {
|
||||
setDisabled(false);
|
||||
return "blocked!";
|
||||
},
|
||||
error: () => {
|
||||
setDisabled(false);
|
||||
return "Something went wrong";
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<OctagonX />
|
||||
Block
|
||||
{disabled ? <TextShimmer>Blocking</TextShimmer> : "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">
|
||||
) : (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={disabled} variant="destructive">
|
||||
<OctagonX />
|
||||
Block
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</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>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ import { useAtom } from "jotai";
|
||||
import Link from 'next/link';
|
||||
import AddDevicesToCartButton from "./add-devices-to-cart-button";
|
||||
import BlockDeviceDialog from "./block-device-dialog";
|
||||
export default function ClickableRow({ device, parentalControl }: { device: Device, parentalControl?: boolean }) {
|
||||
export default function ClickableRow({ device, parentalControl, admin = false }: { device: Device, parentalControl?: boolean, admin?: boolean }) {
|
||||
const [devices, setDeviceCart] = useAtom(deviceCartAtom)
|
||||
|
||||
|
||||
@@ -39,13 +39,13 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
|
||||
</Link>
|
||||
<span className="text-muted-foreground">
|
||||
Active until{" "}
|
||||
{new Date().toLocaleDateString("en-US", {
|
||||
{new Date(device.expiryDate || "").toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
{(parentalControl && device.blocked) && (
|
||||
{(device.blockedBy === "ADMIN" && device.blocked) && (
|
||||
<div className="p-2 rounded border my-2">
|
||||
<span>Comment: </span>
|
||||
<p className="text-neutral-500">
|
||||
@@ -60,7 +60,7 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
|
||||
{!parentalControl ? (
|
||||
<AddDevicesToCartButton device={device} />
|
||||
) : (
|
||||
<BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} />
|
||||
<BlockDeviceDialog admin={admin} type={device.blocked ? "unblock" : "block"} device={device} />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow >
|
||||
|
@@ -53,11 +53,11 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
|
||||
</span>
|
||||
)}
|
||||
|
||||
{device.blocked && (
|
||||
{(device.blocked && device.blockedBy === "ADMIN") && (
|
||||
<div className="p-2 rounded border my-2 w-full">
|
||||
<span className='uppercase text-red-500'>Blocked by admin </span>
|
||||
<p className="text-neutral-500">
|
||||
blocked because he was watching youtube
|
||||
{device?.reasonForBlocking}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -67,7 +67,7 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
|
||||
{!parentalControl ? (
|
||||
<AddDevicesToCartButton device={device} />
|
||||
) : (
|
||||
<BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} />
|
||||
<BlockDeviceDialog admin={false} type={device.blocked ? "unblock" : "block"} device={device} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -17,7 +17,7 @@ import Pagination from "./pagination";
|
||||
|
||||
export async function DevicesTable({
|
||||
searchParams,
|
||||
parentalControl
|
||||
parentalControl,
|
||||
}: {
|
||||
searchParams: Promise<{
|
||||
query: string;
|
||||
@@ -29,12 +29,13 @@ export async function DevicesTable({
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers()
|
||||
})
|
||||
const isAdmin = session?.user.role === "ADMIN"
|
||||
const query = (await searchParams)?.query || "";
|
||||
const page = (await searchParams)?.page;
|
||||
const sortBy = (await searchParams)?.sortBy || "asc";
|
||||
const totalDevices = await prisma.device.count({
|
||||
where: {
|
||||
userId: session?.session.userId,
|
||||
userId: isAdmin ? undefined : session?.session.userId,
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
@@ -54,8 +55,8 @@ export async function DevicesTable({
|
||||
paid: false
|
||||
}
|
||||
},
|
||||
isActive: parentalControl,
|
||||
blocked: parentalControl !== undefined ? undefined : false,
|
||||
isActive: isAdmin ? undefined : parentalControl,
|
||||
blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -65,7 +66,7 @@ export async function DevicesTable({
|
||||
|
||||
const devices = await prisma.device.findMany({
|
||||
where: {
|
||||
userId: session?.session.userId,
|
||||
userId: isAdmin ? undefined : session?.session.userId,
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
@@ -85,8 +86,8 @@ export async function DevicesTable({
|
||||
paid: false
|
||||
}
|
||||
},
|
||||
isActive: parentalControl,
|
||||
blocked: parentalControl !== undefined ? undefined : false,
|
||||
isActive: isAdmin ? undefined : parentalControl,
|
||||
blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false,
|
||||
},
|
||||
|
||||
skip: offset,
|
||||
@@ -153,7 +154,7 @@ export async function DevicesTable({
|
||||
// )}
|
||||
// </TableCell>
|
||||
// </TableRow>
|
||||
<ClickableRow key={device.id} device={device} parentalControl={parentalControl} />
|
||||
<ClickableRow admin={isAdmin} key={device.id} device={device} parentalControl={parentalControl} />
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
@@ -180,7 +181,6 @@ export async function DevicesTable({
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@@ -13,11 +13,19 @@ import Link from "next/link";
|
||||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Payment, Prisma } from "@prisma/client";
|
||||
import { Calendar } from "lucide-react";
|
||||
import { headers } from "next/headers";
|
||||
import Pagination from "./pagination";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { Separator } from "./ui/separator"
|
||||
|
||||
type PaymentWithDevices = Prisma.PaymentGetPayload<{
|
||||
include: {
|
||||
devices: true;
|
||||
};
|
||||
}>
|
||||
|
||||
export async function PaymentsTable({
|
||||
searchParams,
|
||||
@@ -90,85 +98,143 @@ export async function PaymentsTable({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table className="overflow-scroll">
|
||||
<TableCaption>Table of all devices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Details</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<div className="hidden sm:block">
|
||||
<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={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={16} opacity={0.5} />
|
||||
<span className="text-muted-foreground">
|
||||
{new Date(payment.createdAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<TableHead>Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="overflow-scroll">
|
||||
{payments.map((payment) => (
|
||||
<TableRow key={payment.id}>
|
||||
<TableCell>
|
||||
<div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={16} opacity={0.5} />
|
||||
<span className="text-muted-foreground">
|
||||
{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={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}>
|
||||
{payment.paid ? "Paid" : "Unpaid"}
|
||||
</Badge>
|
||||
<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={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} 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 border">
|
||||
<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>
|
||||
<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="font-medium">
|
||||
{payment.numberOfMonths} Months
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-semibold pr-2">
|
||||
{payment.amount.toFixed(2)}
|
||||
</span>
|
||||
MVR
|
||||
<TableCell className="text-muted-foreground">
|
||||
{totalPayments} payments
|
||||
</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} />
|
||||
</TableFooter>
|
||||
</Table>
|
||||
<Pagination totalPages={totalPages} currentPage={page} />
|
||||
</div>
|
||||
<div className="sm:hidden block">
|
||||
{payments.map((payment) => (
|
||||
<MobilePaymentDetails key={payment.id} payment={payment} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) {
|
||||
return (
|
||||
<div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={16} opacity={0.5} />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{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={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} 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 border">
|
||||
<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 className="block sm:hidden">
|
||||
<Separator className="my-2" />
|
||||
<h3 className="text-sm font-medium">Duration</h3>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{payment.numberOfMonths} Months
|
||||
</span>
|
||||
<Separator className="my-2" />
|
||||
<h3 className="text-sm font-medium">Amount</h3>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{payment.amount.toFixed(2)} MVR
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -55,7 +55,7 @@ export function Wallet({
|
||||
<Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button onClick={() => setIsOpen(!isOpen)} variant="outline">
|
||||
{walletBalance} MVR
|
||||
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)} MVR
|
||||
<Wallet2 />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
@@ -67,7 +67,7 @@ export function Wallet({
|
||||
<div>
|
||||
Your wallet balance is{" "}
|
||||
<span className="font-semibold">
|
||||
{walletBalance.toFixed(2)}
|
||||
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)}
|
||||
</span>{" "}
|
||||
</div>
|
||||
</DrawerDescription>
|
||||
|
Reference in New Issue
Block a user