mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-10-05 09:55:25 +00:00
refactor: add animations ✨
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 10m27s
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 10m27s
This commit is contained in:
@@ -27,11 +27,16 @@ export default async function DeviceDetails({
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between title-bg title-bg ring-2 ring-sarLinkOrange/50 rounded-lg p-4">
|
<div className="flex items-center justify-between title-bg title-bg ring-2 ring-sarLinkOrange/50 rounded-lg p-4">
|
||||||
<div className="flex flex-col justify-between items-start">
|
<div className="flex flex-col justify-between items-start">
|
||||||
<h3 className="text-2xl text-sarLinkOrange font-bold">
|
<h3 className="text-2xl text-sarLinkOrange motion-preset-slide-down-md font-bold">
|
||||||
{device?.name}
|
{device?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant={"secondary"}>{device?.mac}</Badge>
|
<Badge
|
||||||
<p className="text-muted-foreground text-sm mt-2">
|
className="motion-preset-slide-down-md motion-delay-75"
|
||||||
|
variant={"secondary"}
|
||||||
|
>
|
||||||
|
{device?.mac}
|
||||||
|
</Badge>
|
||||||
|
<p className="text-muted-foreground text-sm mt-2 motion-preset-slide-down-md motion-delay-100">
|
||||||
Device active until{" "}
|
Device active until{" "}
|
||||||
{new Date(device?.expiry_date || "").toLocaleDateString("en-US", {
|
{new Date(device?.expiry_date || "").toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
@@ -40,7 +45,7 @@ export default async function DeviceDetails({
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-col">
|
<div className="flex items-center gap-2 flex-col motion-preset-fade">
|
||||||
{device?.expiry_date && new Date() < new Date(device.expiry_date) && (
|
{device?.expiry_date && new Date() < new Date(device.expiry_date) && (
|
||||||
<p className="text-base font-semibold font-mono w-full text-center px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400">
|
<p className="text-base font-semibold font-mono w-full text-center px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400">
|
||||||
ACTIVE
|
ACTIVE
|
||||||
|
@@ -9,96 +9,99 @@ import { cn } from "@/lib/utils";
|
|||||||
import AddDevicesToCartButton from "./add-devices-to-cart-button";
|
import AddDevicesToCartButton from "./add-devices-to-cart-button";
|
||||||
import BlockDeviceDialog from "./block-device-dialog";
|
import BlockDeviceDialog from "./block-device-dialog";
|
||||||
export default function ClickableRow({
|
export default function ClickableRow({
|
||||||
device,
|
device,
|
||||||
parentalControl,
|
parentalControl,
|
||||||
admin = false,
|
admin = false,
|
||||||
|
idx,
|
||||||
}: {
|
}: {
|
||||||
device: Device;
|
device: Device;
|
||||||
parentalControl?: boolean;
|
parentalControl?: boolean;
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
|
idx?: number;
|
||||||
}) {
|
}) {
|
||||||
const [devices, setDeviceCart] = useAtom(deviceCartAtom);
|
const [devices, setDeviceCart] = useAtom(deviceCartAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={device.id}
|
key={device.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
(parentalControl === false && device.blocked) || device.is_active
|
(parentalControl === false && device.blocked) || device.is_active
|
||||||
? "cursor-not-allowed hover:bg-accent-foreground/10"
|
? "cursor-not-allowed hover:bg-accent-foreground/10"
|
||||||
: "cursor-pointer hover:bg-muted-foreground/10",
|
: "cursor-pointer hover:bg-muted-foreground/10",
|
||||||
)}
|
`motion-preset-fade-md motion-delay-${(idx || 1) * 75}ms`,
|
||||||
onClick={() => {
|
)}
|
||||||
if (device.blocked) return;
|
onClick={() => {
|
||||||
if (device.is_active === true) return;
|
if (device.blocked) return;
|
||||||
if (device.has_a_pending_payment === true) return;
|
if (device.is_active === true) return;
|
||||||
if (parentalControl === true) return;
|
if (device.has_a_pending_payment === true) return;
|
||||||
setDeviceCart((prev) =>
|
if (parentalControl === true) return;
|
||||||
devices.some((d) => d.id === device.id)
|
setDeviceCart((prev) =>
|
||||||
? prev.filter((d) => d.id !== device.id)
|
devices.some((d) => d.id === device.id)
|
||||||
: [...prev, device],
|
? prev.filter((d) => d.id !== device.id)
|
||||||
);
|
: [...prev, device],
|
||||||
}}
|
);
|
||||||
>
|
}}
|
||||||
<TableCell>
|
>
|
||||||
<div className="flex flex-col items-start">
|
<TableCell>
|
||||||
<Link
|
<div className="flex flex-col items-start">
|
||||||
className={cn(
|
<Link
|
||||||
"hover:underline font-semibold",
|
className={cn(
|
||||||
device.is_active ? "text-green-600" : "",
|
"hover:underline font-semibold",
|
||||||
)}
|
device.is_active ? "text-green-600" : "",
|
||||||
href={`/devices/${device.id}`}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
href={`/devices/${device.id}`}
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{device.name}
|
>
|
||||||
</Link>
|
{device.name}
|
||||||
{device.is_active ? (
|
</Link>
|
||||||
<div className="text-muted-foreground">
|
{device.is_active ? (
|
||||||
Active until{" "}
|
<div className="text-muted-foreground">
|
||||||
<span className="font-semibold">
|
Active until{" "}
|
||||||
{new Date(device.expiry_date || "").toLocaleDateString(
|
<span className="font-semibold">
|
||||||
"en-US",
|
{new Date(device.expiry_date || "").toLocaleDateString(
|
||||||
{
|
"en-US",
|
||||||
month: "short",
|
{
|
||||||
day: "2-digit",
|
month: "short",
|
||||||
year: "numeric",
|
day: "2-digit",
|
||||||
},
|
year: "numeric",
|
||||||
)}
|
},
|
||||||
</span>
|
)}
|
||||||
</div>
|
</span>
|
||||||
) : (
|
</div>
|
||||||
<p className="text-muted-foreground">Device Inactive</p>
|
) : (
|
||||||
)}
|
<p className="text-muted-foreground">Device Inactive</p>
|
||||||
{device.has_a_pending_payment && (
|
)}
|
||||||
<Link href={`/payments/${device.pending_payment_id}`}>
|
{device.has_a_pending_payment && (
|
||||||
<span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-muted-foreground">
|
<Link href={`/payments/${device.pending_payment_id}`}>
|
||||||
Payment Pending{" "}
|
<span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-muted-foreground">
|
||||||
<HandCoins className="animate-pulse" size={14} />
|
Payment Pending{" "}
|
||||||
</span>
|
<HandCoins className="animate-pulse" size={14} />
|
||||||
</Link>
|
</span>
|
||||||
)}
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
{device.blocked_by === "ADMIN" && device.blocked && (
|
{device.blocked_by === "ADMIN" && device.blocked && (
|
||||||
<div className="p-2 rounded border my-2 bg-white dark:bg-neutral-800 shadow">
|
<div className="p-2 rounded border my-2 bg-white dark:bg-neutral-800 shadow">
|
||||||
<span className="font-semibold">Comment</span>
|
<span className="font-semibold">Comment</span>
|
||||||
<p className="text-neutral-400">{device?.reason_for_blocking}</p>
|
<p className="text-neutral-400">{device?.reason_for_blocking}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-medium">{device.mac}</TableCell>
|
<TableCell className="font-medium">{device.mac}</TableCell>
|
||||||
<TableCell className="font-medium">{device?.vendor}</TableCell>
|
<TableCell className="font-medium">{device?.vendor}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{!parentalControl ? (
|
{!parentalControl ? (
|
||||||
<AddDevicesToCartButton device={device} />
|
<AddDevicesToCartButton device={device} />
|
||||||
) : (
|
) : (
|
||||||
<BlockDeviceDialog
|
<BlockDeviceDialog
|
||||||
admin={admin}
|
admin={admin}
|
||||||
type={device.blocked ? "unblock" : "block"}
|
type={device.blocked ? "unblock" : "block"}
|
||||||
device={device}
|
device={device}
|
||||||
parentalControl={parentalControl}
|
parentalControl={parentalControl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -10,108 +10,108 @@ import BlockDeviceDialog from "./block-device-dialog";
|
|||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
|
|
||||||
export default function DeviceCard({
|
export default function DeviceCard({
|
||||||
device,
|
device,
|
||||||
parentalControl,
|
parentalControl,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
}: {
|
}: {
|
||||||
device: Device;
|
device: Device;
|
||||||
parentalControl?: boolean;
|
parentalControl?: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [devices, setDeviceCart] = useAtom(deviceCartAtom);
|
const [devices, setDeviceCart] = useAtom(deviceCartAtom);
|
||||||
|
|
||||||
const isChecked = devices.some((d) => d.id === device.id);
|
const isChecked = devices.some((d) => d.id === device.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/a11y/noStaticElementInteractions: <dw about it>
|
// biome-ignore lint/a11y/noStaticElementInteractions: <dw about it>
|
||||||
<div
|
<div
|
||||||
onKeyUp={() => {}}
|
onKeyUp={() => {}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (device.blocked) return;
|
if (device.blocked) return;
|
||||||
if (device.is_active === true) return;
|
if (device.is_active === true) return;
|
||||||
if (device.has_a_pending_payment === true) return;
|
if (device.has_a_pending_payment === true) return;
|
||||||
if (parentalControl === true) return;
|
if (parentalControl === true) return;
|
||||||
setDeviceCart((prev) =>
|
setDeviceCart((prev) =>
|
||||||
devices.some((d) => d.id === device.id)
|
devices.some((d) => d.id === device.id)
|
||||||
? prev.filter((d) => d.id !== device.id)
|
? prev.filter((d) => d.id !== device.id)
|
||||||
: [...prev, device],
|
: [...prev, device],
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex text-sm justify-between items-center my-2 p-4 border rounded-md",
|
"flex text-sm justify-between items-center my-2 p-4 border rounded-md motion-preset-fade-md",
|
||||||
isChecked ? "bg-accent" : "",
|
isChecked ? "bg-accent" : "",
|
||||||
device.is_active
|
device.is_active
|
||||||
? "cursor-not-allowed text-green-600 hover:bg-accent-foreground/10"
|
? "cursor-not-allowed text-green-600 hover:bg-accent-foreground/10"
|
||||||
: "cursor-pointer hover:bg-muted-foreground/10",
|
: "cursor-pointer hover:bg-muted-foreground/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="font-semibold flex flex-col items-start gap-2 mb-2">
|
<div className="font-semibold flex flex-col items-start gap-2 mb-2">
|
||||||
<Link
|
<Link
|
||||||
className={cn(
|
className={cn(
|
||||||
"font-medium hover:underline ml-0.5",
|
"font-medium hover:underline ml-0.5",
|
||||||
device.is_active ? "text-green-600" : "",
|
device.is_active ? "text-green-600" : "",
|
||||||
)}
|
)}
|
||||||
href={`/devices/${device.id}`}
|
href={`/devices/${device.id}`}
|
||||||
>
|
>
|
||||||
{device.name}
|
{device.name}
|
||||||
</Link>
|
</Link>
|
||||||
<Badge variant={"outline"}>
|
<Badge variant={"outline"}>
|
||||||
<span className="font-medium">{device.mac}</span>
|
<span className="font-medium">{device.mac}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant={"outline"}>
|
<Badge variant={"outline"}>
|
||||||
<span className="font-medium">{device.vendor}</span>
|
<span className="font-medium">{device.vendor}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{device.is_active ? (
|
{device.is_active ? (
|
||||||
<div className="text-muted-foreground ml-0.5">
|
<div className="text-muted-foreground ml-0.5">
|
||||||
Active until{" "}
|
Active until{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{new Date(device.expiry_date || "").toLocaleDateString(
|
{new Date(device.expiry_date || "").toLocaleDateString(
|
||||||
"en-US",
|
"en-US",
|
||||||
{
|
{
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground ml-0.5">Device Inactive</p>
|
<p className="text-muted-foreground ml-0.5">Device Inactive</p>
|
||||||
)}
|
)}
|
||||||
{device.has_a_pending_payment && (
|
{device.has_a_pending_payment && (
|
||||||
<Link href={`/payments/${device.pending_payment_id}`}>
|
<Link href={`/payments/${device.pending_payment_id}`}>
|
||||||
<span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-yellow-600">
|
<span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-yellow-600">
|
||||||
Payment Pending{" "}
|
Payment Pending{" "}
|
||||||
<HandCoins className="animate-pulse" size={14} />
|
<HandCoins className="animate-pulse" size={14} />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{device.blocked && device.blocked_by === "ADMIN" && (
|
{device.blocked && device.blocked_by === "ADMIN" && (
|
||||||
<div className="p-2 rounded border my-2 w-full">
|
<div className="p-2 rounded border my-2 w-full">
|
||||||
<span className="uppercase text-red-500">Blocked by admin </span>
|
<span className="uppercase text-red-500">Blocked by admin </span>
|
||||||
<p className="text-neutral-500">{device?.reason_for_blocking}</p>
|
<p className="text-neutral-500">{device?.reason_for_blocking}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{!parentalControl ? (
|
{!parentalControl ? (
|
||||||
<AddDevicesToCartButton device={device} />
|
<AddDevicesToCartButton device={device} />
|
||||||
) : (
|
) : (
|
||||||
<BlockDeviceDialog
|
<BlockDeviceDialog
|
||||||
admin={isAdmin}
|
admin={isAdmin}
|
||||||
type={device.blocked ? "unblock" : "block"}
|
type={device.blocked ? "unblock" : "block"}
|
||||||
device={device}
|
device={device}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -2,13 +2,13 @@ import { redirect } from "next/navigation";
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "@/app/auth";
|
import { authOptions } from "@/app/auth";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { getDevices } from "@/queries/devices";
|
import { getDevices } from "@/queries/devices";
|
||||||
import { tryCatch } from "@/utils/tryCatch";
|
import { tryCatch } from "@/utils/tryCatch";
|
||||||
@@ -18,107 +18,108 @@ import DeviceCard from "./device-card";
|
|||||||
import Pagination from "./pagination";
|
import Pagination from "./pagination";
|
||||||
|
|
||||||
export async function DevicesTable({
|
export async function DevicesTable({
|
||||||
searchParams,
|
searchParams,
|
||||||
parentalControl,
|
parentalControl,
|
||||||
additionalFilters = {},
|
additionalFilters = {},
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}>;
|
}>;
|
||||||
parentalControl?: boolean;
|
parentalControl?: boolean;
|
||||||
additionalFilters?: Record<string, string | number | boolean>;
|
additionalFilters?: Record<string, string | number | boolean>;
|
||||||
}) {
|
}) {
|
||||||
const resolvedParams = await searchParams;
|
const resolvedParams = await searchParams;
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
const isAdmin = session?.user?.is_admin;
|
const isAdmin = session?.user?.is_admin;
|
||||||
|
|
||||||
const page = Number.parseInt(resolvedParams.page as string) || 1;
|
const page = Number.parseInt(resolvedParams.page as string) || 1;
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// Build params object for getDevices
|
// Build params object for getDevices
|
||||||
const apiParams: Record<string, string | number | undefined> = {};
|
const apiParams: Record<string, string | number | undefined> = {};
|
||||||
for (const [key, value] of Object.entries(resolvedParams)) {
|
for (const [key, value] of Object.entries(resolvedParams)) {
|
||||||
if (value !== undefined && value !== "") {
|
if (value !== undefined && value !== "") {
|
||||||
apiParams[key] = typeof value === "number" ? value : String(value);
|
apiParams[key] = typeof value === "number" ? value : String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(additionalFilters)) {
|
for (const [key, value] of Object.entries(additionalFilters)) {
|
||||||
if (value !== undefined && value !== "") {
|
if (value !== undefined && value !== "") {
|
||||||
apiParams[key] = typeof value === "number" ? value : String(value);
|
apiParams[key] = typeof value === "number" ? value : String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apiParams.limit = limit;
|
apiParams.limit = limit;
|
||||||
apiParams.offset = offset;
|
apiParams.offset = offset;
|
||||||
const [error, devices] = await tryCatch(getDevices(apiParams));
|
const [error, devices] = await tryCatch(getDevices(apiParams));
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error.message === "UNAUTHORIZED") {
|
if (error.message === "UNAUTHORIZED") {
|
||||||
redirect("/auth/signin");
|
redirect("/auth/signin");
|
||||||
} else {
|
} else {
|
||||||
return <ClientErrorMessage message={error.message} />;
|
return <ClientErrorMessage message={error.message} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { meta, data } = devices;
|
const { meta, data } = devices;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
|
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
|
||||||
<h3>{parentalControl ? "No active devices" : "No devices."}</h3>
|
<h3>{parentalControl ? "No active devices" : "No devices."}</h3>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<Table className="overflow-scroll">
|
<Table className="overflow-scroll">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Device Name</TableHead>
|
<TableHead>Device Name</TableHead>
|
||||||
<TableHead>MAC Address</TableHead>
|
<TableHead>MAC Address</TableHead>
|
||||||
<TableHead>Vendor</TableHead>
|
<TableHead>Vendor</TableHead>
|
||||||
<TableHead>#</TableHead>
|
<TableHead>#</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="overflow-scroll">
|
<TableBody className="overflow-scroll">
|
||||||
{data?.map((device) => (
|
{data?.map((device, idx) => (
|
||||||
<ClickableRow
|
<ClickableRow
|
||||||
admin={isAdmin}
|
admin={isAdmin}
|
||||||
key={device.id}
|
key={device.id}
|
||||||
device={device}
|
device={device}
|
||||||
parentalControl={parentalControl}
|
parentalControl={parentalControl}
|
||||||
/>
|
idx={idx + 1}
|
||||||
))}
|
/>
|
||||||
</TableBody>
|
))}
|
||||||
<TableFooter>
|
</TableBody>
|
||||||
<TableRow>
|
<TableFooter>
|
||||||
<TableCell colSpan={4} className="text-muted-foreground">
|
<TableRow>
|
||||||
{meta?.total === 1 ? (
|
<TableCell colSpan={4} className="text-muted-foreground">
|
||||||
<p className="text-center">Total {meta?.total} device.</p>
|
{meta?.total === 1 ? (
|
||||||
) : (
|
<p className="text-center">Total {meta?.total} device.</p>
|
||||||
<p className="text-center">
|
) : (
|
||||||
Total {meta?.total} devices.
|
<p className="text-center">
|
||||||
</p>
|
Total {meta?.total} devices.
|
||||||
)}
|
</p>
|
||||||
</TableCell>
|
)}
|
||||||
</TableRow>
|
</TableCell>
|
||||||
</TableFooter>
|
</TableRow>
|
||||||
</Table>
|
</TableFooter>
|
||||||
</div>
|
</Table>
|
||||||
<div className="sm:hidden my-4">
|
</div>
|
||||||
{data?.map((device) => (
|
<div className="sm:hidden my-4">
|
||||||
<DeviceCard
|
{data?.map((device) => (
|
||||||
parentalControl={parentalControl}
|
<DeviceCard
|
||||||
key={device.id}
|
parentalControl={parentalControl}
|
||||||
device={device}
|
key={device.id}
|
||||||
isAdmin={isAdmin}
|
device={device}
|
||||||
/>
|
isAdmin={isAdmin}
|
||||||
))}
|
/>
|
||||||
</div>
|
))}
|
||||||
<Pagination
|
</div>
|
||||||
totalPages={meta?.last_page}
|
<Pagination
|
||||||
currentPage={meta?.current_page}
|
totalPages={meta?.last_page}
|
||||||
/>
|
currentPage={meta?.current_page}
|
||||||
</>
|
/>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -126,6 +126,7 @@ export default function DevicesToPay({
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
{isPending
|
{isPending
|
||||||
? "Processing payment..."
|
? "Processing payment..."
|
||||||
@@ -145,7 +146,7 @@ export default function DevicesToPay({
|
|||||||
disabled={isPending || disabled}
|
disabled={isPending || disabled}
|
||||||
type="submit"
|
type="submit"
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
className="mb-4"
|
className="mb-4 w-full"
|
||||||
>
|
>
|
||||||
{isPending ? "Processing payment..." : "I have paid"}
|
{isPending ? "Processing payment..." : "I have paid"}
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
@@ -162,10 +163,8 @@ export default function DevicesToPay({
|
|||||||
</TableCaption>
|
</TableCaption>
|
||||||
<TableBody className="">
|
<TableBody className="">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="motion-preset-slide-left-sm">
|
<TableCell>Payment created</TableCell>
|
||||||
Payment created
|
<TableCell className="text-right motion-preset-slide-up-sm">
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right motion-preset-slide-right-sm">
|
|
||||||
{new Date(payment?.created_at ?? "").toLocaleDateString(
|
{new Date(payment?.created_at ?? "").toLocaleDateString(
|
||||||
"en-US",
|
"en-US",
|
||||||
{
|
{
|
||||||
@@ -180,31 +179,22 @@ export default function DevicesToPay({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="motion-preset-slide-left-sm motion-delay-75">
|
<TableCell>Total Devices</TableCell>
|
||||||
Total Devices
|
<TableCell className="text-right text-xl motion-preset-slide-up-sm motion-delay-75">
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-75">
|
|
||||||
{devices?.length}
|
{devices?.length}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="motion-preset-slide-left-sm motion-delay-100">
|
<TableCell>Duration</TableCell>
|
||||||
Duration
|
<TableCell className="text-right text-xl motion-preset-slide-up-sm motion-delay-100">
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-100">
|
|
||||||
{payment?.number_of_months} Months
|
{payment?.number_of_months} Months
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
<TableRow className="">
|
<TableRow className="">
|
||||||
<TableCell
|
<TableCell colSpan={1}>Total Due</TableCell>
|
||||||
className="motion-preset-slide-left-sm motion-delay-150"
|
<TableCell className="text-right text-3xl font-bold motion-preset-slide-up-sm motion-delay-150">
|
||||||
colSpan={1}
|
|
||||||
>
|
|
||||||
Total Due
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-3xl font-bold motion-preset-slide-right-sm motion-delay-150">
|
|
||||||
{payment?.amount?.toFixed(2)}
|
{payment?.amount?.toFixed(2)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@@ -3,13 +3,13 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getPayments } from "@/actions/payment";
|
import { getPayments } from "@/actions/payment";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import type { Payment } from "@/lib/backend-types";
|
import type { Payment } from "@/lib/backend-types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -20,265 +20,265 @@ import { Button } from "./ui/button";
|
|||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
|
|
||||||
export async function PaymentsTable({
|
export async function PaymentsTable({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const resolvedParams = await searchParams;
|
const resolvedParams = await searchParams;
|
||||||
const page = Number.parseInt(resolvedParams.page as string) || 1;
|
const page = Number.parseInt(resolvedParams.page as string) || 1;
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
const apiParams: Record<string, string | number | undefined> = {};
|
const apiParams: Record<string, string | number | undefined> = {};
|
||||||
for (const [key, value] of Object.entries(resolvedParams)) {
|
for (const [key, value] of Object.entries(resolvedParams)) {
|
||||||
if (value !== undefined && value !== "") {
|
if (value !== undefined && value !== "") {
|
||||||
apiParams[key] = typeof value === "number" ? value : String(value);
|
apiParams[key] = typeof value === "number" ? value : String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apiParams.limit = limit;
|
apiParams.limit = limit;
|
||||||
apiParams.offset = offset;
|
apiParams.offset = offset;
|
||||||
const [error, payments] = await tryCatch(getPayments(apiParams));
|
const [error, payments] = await tryCatch(getPayments(apiParams));
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error.message.includes("Unauthorized")) {
|
if (error.message.includes("Unauthorized")) {
|
||||||
redirect("/auth/signin");
|
redirect("/auth/signin");
|
||||||
} else {
|
} else {
|
||||||
return <pre>{JSON.stringify(error, null, 2)}</pre>;
|
return <pre>{JSON.stringify(error, null, 2)}</pre>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { data, meta } = payments;
|
const { data, meta } = payments;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
|
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
|
||||||
<h3>No Payments.</h3>
|
<h3>No Payments.</h3>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<Table className="overflow-scroll">
|
<Table className="overflow-scroll">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Details</TableHead>
|
<TableHead>Details</TableHead>
|
||||||
<TableHead>Duration</TableHead>
|
<TableHead>Duration</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Amount</TableHead>
|
<TableHead>Amount</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="overflow-scroll">
|
<TableBody className="overflow-scroll">
|
||||||
{payments?.data?.map((payment) => (
|
{payments?.data?.map((payment) => (
|
||||||
<TableRow key={payment.id}>
|
<TableRow className="motion-preset-fade-md" key={payment.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start border rounded p-2",
|
"flex flex-col items-start border rounded p-2",
|
||||||
payment?.paid
|
payment?.paid
|
||||||
? "bg-green-500/10 border-dashed border-green-500"
|
? "bg-green-500/10 border-dashed border-green-500"
|
||||||
: payment?.is_expired
|
: payment?.is_expired
|
||||||
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
|
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
|
||||||
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
|
: "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">
|
||||||
{new Date(payment.created_at).toLocaleDateString(
|
{new Date(payment.created_at).toLocaleDateString(
|
||||||
"en-US",
|
"en-US",
|
||||||
{
|
{
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
timeZone: "Indian/Maldives", // Force consistent timezone
|
timeZone: "Indian/Maldives", // Force consistent timezone
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Link
|
<Link
|
||||||
className="font-medium hover:underline"
|
className="font-medium hover:underline"
|
||||||
href={`/payments/${payment.id}`}
|
href={`/payments/${payment.id}`}
|
||||||
>
|
>
|
||||||
<Button size={"sm"} variant="outline">
|
<Button size={"sm"} variant="outline">
|
||||||
View Details
|
View Details
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
||||||
<h3 className="text-sm font-medium">Devices</h3>
|
<h3 className="text-sm font-medium">Devices</h3>
|
||||||
<ol className="list-disc list-inside text-sm">
|
<ol className="list-disc list-inside text-sm">
|
||||||
{payment.devices.map((device) => (
|
{payment.devices.map((device) => (
|
||||||
<li
|
<li
|
||||||
key={device.id}
|
key={device.id}
|
||||||
className="text-sm text-muted-foreground"
|
className="text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
{device.name}
|
{device.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
{payment.number_of_months} Months
|
{payment.number_of_months} Months
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-semibold pr-2">
|
<span className="font-semibold pr-2">
|
||||||
{payment.paid ? (
|
{payment.paid ? (
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
payment.status === "PENDING"
|
payment.status === "PENDING"
|
||||||
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
|
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
|
||||||
: "bg-green-100 dark:bg-green-700",
|
: "bg-green-100 dark:bg-green-700",
|
||||||
)}
|
)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{payment.status}
|
{payment.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : payment.is_expired ? (
|
) : payment.is_expired ? (
|
||||||
<Badge>Expired</Badge>
|
<Badge>Expired</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline">{payment.status}</Badge>
|
<Badge variant="outline">{payment.status}</Badge>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-semibold pr-2">
|
<span className="font-semibold pr-2">
|
||||||
{payment.amount.toFixed(2)}
|
{payment.amount.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
MVR
|
MVR
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className="text-muted-foreground">
|
<TableCell colSpan={4} className="text-muted-foreground">
|
||||||
{meta?.total === 1 ? (
|
{meta?.total === 1 ? (
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
Total {meta?.total} payment.
|
Total {meta?.total} payment.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
Total {meta?.total} payments.
|
Total {meta?.total} payments.
|
||||||
</p>
|
</p>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableFooter>
|
</TableFooter>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination
|
<Pagination
|
||||||
totalPages={meta.last_page}
|
totalPages={meta.last_page}
|
||||||
currentPage={meta.current_page}
|
currentPage={meta.current_page}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden block">
|
<div className="sm:hidden block">
|
||||||
{data.map((payment) => (
|
{data.map((payment) => (
|
||||||
<MobilePaymentDetails key={payment.id} payment={payment} />
|
<MobilePaymentDetails key={payment.id} payment={payment} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MobilePaymentDetails({
|
export function MobilePaymentDetails({
|
||||||
payment,
|
payment,
|
||||||
isAdmin = false,
|
isAdmin = false,
|
||||||
}: {
|
}: {
|
||||||
payment: Payment;
|
payment: Payment;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start border rounded p-2 my-2",
|
"flex flex-col items-start border rounded p-2 my-2 motion-preset-fade-md",
|
||||||
payment?.paid
|
payment?.paid
|
||||||
? "bg-green-500/10 border-dashed border-green-500"
|
? "bg-green-500/10 border-dashed border-green-500"
|
||||||
: payment?.is_expired
|
: payment?.is_expired
|
||||||
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
|
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
|
||||||
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
|
: "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 text-sm">
|
<span className="text-muted-foreground text-sm">
|
||||||
{new Date(payment.created_at).toLocaleDateString("en-US", {
|
{new Date(payment.created_at).toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
timeZone: "Indian/Maldives", // Force consistent timezone
|
timeZone: "Indian/Maldives", // Force consistent timezone
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Link
|
<Link
|
||||||
className="font-medium hover:underline"
|
className="font-medium hover:underline"
|
||||||
href={`/payments/${payment.id}`}
|
href={`/payments/${payment.id}`}
|
||||||
>
|
>
|
||||||
<Button size={"sm"} variant="outline">
|
<Button size={"sm"} variant="outline">
|
||||||
View Details
|
View Details
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
|
||||||
<h3 className="text-sm font-medium">Devices</h3>
|
<h3 className="text-sm font-medium">Devices</h3>
|
||||||
<ol className="list-disc list-inside text-sm">
|
<ol className="list-disc list-inside text-sm">
|
||||||
{payment.devices.map((device) => (
|
{payment.devices.map((device) => (
|
||||||
<li key={device.id} className="text-sm text-muted-foreground">
|
<li key={device.id} className="text-sm text-muted-foreground">
|
||||||
{device.name}
|
{device.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
<h3 className="text-sm font-medium">Duration</h3>
|
<h3 className="text-sm font-medium">Duration</h3>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{payment.number_of_months} Months
|
{payment.number_of_months} Months
|
||||||
</span>
|
</span>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
<h3 className="text-sm font-medium">Amount</h3>
|
<h3 className="text-sm font-medium">Amount</h3>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{payment.amount.toFixed(2)} MVR
|
{payment.amount.toFixed(2)} MVR
|
||||||
</span>
|
</span>
|
||||||
<span className="font-semibold pr-2">
|
<span className="font-semibold pr-2">
|
||||||
{payment.paid ? (
|
{payment.paid ? (
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
payment.status === "PENDING"
|
payment.status === "PENDING"
|
||||||
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
|
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
|
||||||
: "bg-green-100 dark:bg-green-700",
|
: "bg-green-100 dark:bg-green-700",
|
||||||
)}
|
)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{payment.status}
|
{payment.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : payment.is_expired ? (
|
) : payment.is_expired ? (
|
||||||
<Badge>Expired</Badge>
|
<Badge>Expired</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="secondary">{payment.status}</Badge>
|
<Badge variant="secondary">{payment.status}</Badge>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="my-2 text-primary flex flex-col items-start text-sm border rounded p-2 mt-2 w-full bg-white dark:bg-black">
|
<div className="my-2 text-primary flex flex-col items-start text-sm border rounded p-2 mt-2 w-full bg-white dark:bg-black">
|
||||||
{payment?.user?.name}
|
{payment?.user?.name}
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{payment?.user?.id_card}
|
{payment?.user?.id_card}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -3,13 +3,13 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTopups } from "@/actions/payment";
|
import { getTopups } from "@/actions/payment";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import type { Topup } from "@/lib/backend-types";
|
import type { Topup } from "@/lib/backend-types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -19,199 +19,201 @@ import { Badge } from "./ui/badge";
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export async function TopupsTable({
|
export async function TopupsTable({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const resolvedParams = await searchParams;
|
const resolvedParams = await searchParams;
|
||||||
const page = Number.parseInt(resolvedParams.page as string) || 1;
|
const page = Number.parseInt(resolvedParams.page as string) || 1;
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
// Build params object
|
// Build params object
|
||||||
const apiParams: Record<string, string | number | undefined> = {};
|
const apiParams: Record<string, string | number | undefined> = {};
|
||||||
for (const [key, value] of Object.entries(resolvedParams)) {
|
for (const [key, value] of Object.entries(resolvedParams)) {
|
||||||
if (value !== undefined && value !== "") {
|
if (value !== undefined && value !== "") {
|
||||||
apiParams[key] = typeof value === "number" ? value : String(value);
|
apiParams[key] = typeof value === "number" ? value : String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apiParams.limit = limit;
|
apiParams.limit = limit;
|
||||||
apiParams.offset = offset;
|
apiParams.offset = offset;
|
||||||
const [error, topups] = await tryCatch(getTopups(apiParams));
|
const [error, topups] = await tryCatch(getTopups(apiParams));
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error.message.includes("Unauthorized")) {
|
if (error.message.includes("Unauthorized")) {
|
||||||
redirect("/auth/signin");
|
redirect("/auth/signin");
|
||||||
} else {
|
} else {
|
||||||
return <pre>{JSON.stringify(error, null, 2)}</pre>;
|
return <pre>{JSON.stringify(error, null, 2)}</pre>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { data, meta } = topups;
|
const { data, meta } = topups;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="h-[calc(100svh-400px)] flex text-muted-foreground flex-col items-center justify-center my-4">
|
<div className="h-[calc(100svh-400px)] flex text-muted-foreground flex-col items-center justify-center my-4">
|
||||||
<h3>No topups.</h3>
|
<h3>No topups.</h3>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<Table className="overflow-scroll">
|
<Table className="overflow-scroll">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Details</TableHead>
|
<TableHead>Details</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Amount</TableHead>
|
<TableHead>Amount</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="overflow-scroll">
|
<TableBody className="overflow-scroll">
|
||||||
{topups?.data?.map((topup) => (
|
{topups?.data?.map((topup) => (
|
||||||
<TableRow key={topup.id}>
|
<TableRow key={topup.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start border rounded p-2",
|
"flex flex-col items-start border rounded p-2 motion-preset-fade-md",
|
||||||
topup?.paid
|
topup?.paid
|
||||||
? "bg-green-500/10 border-dashed border-green-500"
|
? "bg-green-500/10 border-dashed border-green-500"
|
||||||
: topup?.is_expired
|
: topup?.is_expired
|
||||||
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
|
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
|
||||||
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
|
: "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">
|
||||||
{new Date(topup.created_at).toLocaleDateString(
|
{new Date(topup.created_at).toLocaleDateString(
|
||||||
"en-US",
|
"en-US",
|
||||||
{
|
{
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Link
|
<Link
|
||||||
className="font-medium hover:underline"
|
className="font-medium hover:underline"
|
||||||
href={`/top-ups/${topup.id}`}
|
href={`/top-ups/${topup.id}`}
|
||||||
>
|
>
|
||||||
<Button size={"sm"} variant="outline">
|
<Button size={"sm"} variant="outline">
|
||||||
View Details
|
View Details
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-semibold pr-2">
|
<span className="font-semibold pr-2">
|
||||||
{topup.paid ? (
|
{topup.paid ? (
|
||||||
<Badge
|
<Badge
|
||||||
className="bg-green-100 dark:bg-green-700"
|
className="bg-green-100 dark:bg-green-700"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{topup.status}
|
{topup.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : topup.is_expired ? (
|
) : topup.is_expired ? (
|
||||||
<Badge>Expired</Badge>
|
<Badge>Expired</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline">{topup.status}</Badge>
|
<Badge variant="outline">{topup.status}</Badge>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-semibold pr-2">
|
<span className="font-semibold pr-2">
|
||||||
{topup.amount.toFixed(2)}
|
{topup.amount.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
MVR
|
MVR
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className="text-muted-foreground">
|
<TableCell colSpan={4} className="text-muted-foreground">
|
||||||
{meta?.total === 1 ? (
|
{meta?.total === 1 ? (
|
||||||
<p className="text-center">Total {meta?.total} topup.</p>
|
<p className="text-center">Total {meta?.total} topup.</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center">Total {meta?.total} topups.</p>
|
<p className="text-center">Total {meta?.total} topups.</p>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableFooter>
|
</TableFooter>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden block">
|
<div className="sm:hidden block">
|
||||||
{data.map((topup) => (
|
{data.map((topup) => (
|
||||||
<MobileTopupDetails key={topup.id} topup={topup} />
|
<MobileTopupDetails key={topup.id} topup={topup} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
<Pagination
|
||||||
totalPages={meta?.last_page}
|
totalPages={meta?.last_page}
|
||||||
currentPage={meta?.current_page}
|
currentPage={meta?.current_page}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileTopupDetails({ topup }: { topup: Topup }) {
|
function MobileTopupDetails({ topup }: { topup: Topup }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start border rounded p-2 my-2",
|
"flex flex-col items-start border rounded p-2 my-2 motion-preset-fade-md",
|
||||||
topup?.paid
|
topup?.paid
|
||||||
? "bg-green-500/10 border-dashed border-green=500"
|
? "bg-green-500/10 border-dashed border-green-500"
|
||||||
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
|
: topup?.is_expired
|
||||||
)}
|
? "bg-gray-500/10 border-dashed border-gray-500 dark:border-gray-500/50"
|
||||||
>
|
: "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">
|
<div className="flex items-center gap-2">
|
||||||
{new Date(topup.created_at).toLocaleDateString("en-US", {
|
<Calendar size={16} opacity={0.5} />
|
||||||
month: "short",
|
<span className="text-muted-foreground text-sm">
|
||||||
day: "2-digit",
|
{new Date(topup.created_at).toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
month: "short",
|
||||||
})}
|
day: "2-digit",
|
||||||
</span>
|
year: "numeric",
|
||||||
</div>
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Link
|
<Link
|
||||||
className="font-medium hover:underline"
|
className="font-medium hover:underline"
|
||||||
href={`/top-ups/${topup.id}`}
|
href={`/top-ups/${topup.id}`}
|
||||||
>
|
>
|
||||||
<Button size={"sm"} variant="outline">
|
<Button size={"sm"} variant="outline">
|
||||||
View Details
|
View Details
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border flex justify-between items-center">
|
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border flex justify-between items-center">
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
<h3 className="text-sm font-medium">Amount</h3>
|
<h3 className="text-sm font-medium">Amount</h3>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{topup.amount.toFixed(2)} MVR
|
{topup.amount.toFixed(2)} MVR
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold pr-2">
|
<span className="font-semibold pr-2">
|
||||||
{topup.paid ? (
|
{topup.paid ? (
|
||||||
<Badge className="bg-green-100 dark:bg-green-700" variant="outline">
|
<Badge className="bg-green-100 dark:bg-green-700" variant="outline">
|
||||||
{topup.status}
|
{topup.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : topup.is_expired ? (
|
) : topup.is_expired ? (
|
||||||
<Badge>Expired</Badge>
|
<Badge>Expired</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="secondary">{topup.status}</Badge>
|
<Badge variant="secondary">{topup.status}</Badge>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,226 +1,226 @@
|
|||||||
import {
|
import {
|
||||||
BadgePlus,
|
BadgePlus,
|
||||||
Calculator,
|
Calculator,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Coins,
|
Coins,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Handshake,
|
Handshake,
|
||||||
MonitorSpeaker,
|
MonitorSpeaker,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
UsersRound,
|
UsersRound,
|
||||||
Wallet2Icon,
|
Wallet2Icon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "@/app/auth";
|
import { authOptions } from "@/app/auth";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
type Permission = {
|
type Permission = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Categories = {
|
type Categories = {
|
||||||
id: string;
|
id: string;
|
||||||
children: (
|
children: (
|
||||||
| {
|
| {
|
||||||
title: string;
|
title: string;
|
||||||
link: string;
|
link: string;
|
||||||
perm_identifier: string;
|
perm_identifier: string;
|
||||||
icon: React.JSX.Element;
|
icon: React.JSX.Element;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
title: string;
|
title: string;
|
||||||
link: string;
|
link: string;
|
||||||
icon: React.JSX.Element;
|
icon: React.JSX.Element;
|
||||||
perm_identifier?: undefined;
|
perm_identifier?: undefined;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export async function AppSidebar({
|
export async function AppSidebar({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Sidebar>) {
|
}: React.ComponentProps<typeof Sidebar>) {
|
||||||
const categories = [
|
const categories = [
|
||||||
{
|
{
|
||||||
id: "MENU",
|
id: "MENU",
|
||||||
url: "#",
|
url: "#",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "Devices",
|
title: "Devices",
|
||||||
link: "/devices?page=1",
|
link: "/devices?page=1",
|
||||||
perm_identifier: "device",
|
perm_identifier: "device",
|
||||||
icon: <Smartphone size={16} />,
|
icon: <Smartphone size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Parental Control",
|
title: "Parental Control",
|
||||||
link: "/parental-control?page=1",
|
link: "/parental-control?page=1",
|
||||||
icon: <CreditCard size={16} />,
|
icon: <CreditCard size={16} />,
|
||||||
perm_identifier: "device",
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Subscriptions",
|
title: "Subscriptions",
|
||||||
link: "/payments?page=1",
|
link: "/payments?page=1",
|
||||||
icon: <CreditCard size={16} />,
|
icon: <CreditCard size={16} />,
|
||||||
perm_identifier: "payment",
|
perm_identifier: "payment",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Top Ups",
|
title: "Top Ups",
|
||||||
link: "/top-ups?page=1",
|
link: "/top-ups?page=1",
|
||||||
icon: <BadgePlus size={16} />,
|
icon: <BadgePlus size={16} />,
|
||||||
perm_identifier: "topup",
|
perm_identifier: "topup",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Transaction History",
|
title: "Transaction History",
|
||||||
link: "/wallet",
|
link: "/wallet",
|
||||||
icon: <Wallet2Icon size={16} />,
|
icon: <Wallet2Icon size={16} />,
|
||||||
perm_identifier: "wallet transaction",
|
perm_identifier: "wallet transaction",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Agreements",
|
title: "Agreements",
|
||||||
link: "/agreements",
|
link: "/agreements",
|
||||||
icon: <Handshake size={16} />,
|
icon: <Handshake size={16} />,
|
||||||
perm_identifier: "device",
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ADMIN CONTROL",
|
id: "ADMIN CONTROL",
|
||||||
url: "#",
|
url: "#",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "Users",
|
title: "Users",
|
||||||
link: "/users",
|
link: "/users",
|
||||||
icon: <UsersRound size={16} />,
|
icon: <UsersRound size={16} />,
|
||||||
perm_identifier: "device",
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "User Devices",
|
title: "User Devices",
|
||||||
link: "/user-devices",
|
link: "/user-devices",
|
||||||
icon: <MonitorSpeaker size={16} />,
|
icon: <MonitorSpeaker size={16} />,
|
||||||
perm_identifier: "device",
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "User Payments",
|
title: "User Payments",
|
||||||
link: "/user-payments",
|
link: "/user-payments",
|
||||||
icon: <Coins size={16} />,
|
icon: <Coins size={16} />,
|
||||||
perm_identifier: "payment",
|
perm_identifier: "payment",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "User Topups",
|
title: "User Topups",
|
||||||
link: "/user-topups",
|
link: "/user-topups",
|
||||||
icon: <Coins size={16} />,
|
icon: <Coins size={16} />,
|
||||||
perm_identifier: "topup",
|
perm_identifier: "topup",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Price Calculator",
|
title: "Price Calculator",
|
||||||
link: "/price-calculator",
|
link: "/price-calculator",
|
||||||
icon: <Calculator size={16} />,
|
icon: <Calculator size={16} />,
|
||||||
perm_identifier: "device",
|
perm_identifier: "device",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
let CATEGORIES: Categories;
|
let CATEGORIES: Categories;
|
||||||
if (session?.user?.is_admin) {
|
if (session?.user?.is_admin) {
|
||||||
CATEGORIES = categories;
|
CATEGORIES = categories;
|
||||||
} else {
|
} else {
|
||||||
// Filter out ADMIN CONTROL category for non-admin users
|
// Filter out ADMIN CONTROL category for non-admin users
|
||||||
const nonAdminCategories = categories.filter(
|
const nonAdminCategories = categories.filter(
|
||||||
(category) => category.id !== "ADMIN CONTROL",
|
(category) => category.id !== "ADMIN CONTROL",
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredCategories = nonAdminCategories.map((category) => {
|
const filteredCategories = nonAdminCategories.map((category) => {
|
||||||
const filteredChildren = category.children.filter((child) => {
|
const filteredChildren = category.children.filter((child) => {
|
||||||
const permIdentifier = child.perm_identifier;
|
const permIdentifier = child.perm_identifier;
|
||||||
return session?.user?.user_permissions?.some(
|
return session?.user?.user_permissions?.some(
|
||||||
(permission: Permission) => {
|
(permission: Permission) => {
|
||||||
const permissionParts = permission.name.split(" ");
|
const permissionParts = permission.name.split(" ");
|
||||||
const modelNameFromPermission = permissionParts.slice(2).join(" ");
|
const modelNameFromPermission = permissionParts.slice(2).join(" ");
|
||||||
return modelNameFromPermission === permIdentifier;
|
return modelNameFromPermission === permIdentifier;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...category, children: filteredChildren };
|
return { ...category, children: filteredChildren };
|
||||||
});
|
});
|
||||||
|
|
||||||
CATEGORIES = filteredCategories.filter(
|
CATEGORIES = filteredCategories.filter(
|
||||||
(category) => category.children.length > 0,
|
(category) => category.children.length > 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar {...props} className="z-50">
|
<Sidebar {...props} className="z-50">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<h4 className="p-2 rounded title-bg border text-center uppercase ">
|
<h4 className="p-2 rounded title-bg border text-center uppercase ">
|
||||||
Sar Link Portal
|
Sar Link Portal
|
||||||
</h4>
|
</h4>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent className="gap-0">
|
<SidebarContent className="gap-0">
|
||||||
{CATEGORIES.map((item) => {
|
{CATEGORIES.map((item) => {
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
key={item.id}
|
key={item.id}
|
||||||
title={item.id}
|
title={item.id}
|
||||||
defaultOpen
|
defaultOpen
|
||||||
className="group/collapsible"
|
className="group/collapsible"
|
||||||
>
|
>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel
|
<SidebarGroupLabel
|
||||||
asChild
|
asChild
|
||||||
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger>
|
<CollapsibleTrigger>
|
||||||
{item.id}{" "}
|
{item.id}{" "}
|
||||||
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{item.children.map((item) => (
|
{item.children.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarMenuButton className="py-6" asChild>
|
<SidebarMenuButton className="py-6" asChild>
|
||||||
<Link className="text-md" href={item.link}>
|
<Link className="text-md" href={item.link}>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span
|
<span
|
||||||
className={`opacity-70 motion-preset-slide-left-md ml-2`}
|
className={`opacity-70 motion-preset-fade motion-duration-300 ml-2`}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -66,7 +66,7 @@ export async function WalletTransactionsTable({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex gap-4 mb-4 w-full">
|
<div className="flex gap-4 mb-4 w-full motion-preset-fade-md">
|
||||||
<div className="bg-red-300 ring-4 ring-red-500/20 w-full sm:w-fit dark:bg-red-950 dark:text-red-400 text-red-900 p-2 px-4 rounded-md mb-2">
|
<div className="bg-red-300 ring-4 ring-red-500/20 w-full sm:w-fit dark:bg-red-950 dark:text-red-400 text-red-900 p-2 px-4 rounded-md mb-2">
|
||||||
<h5 className="text-lg font-semibold uppercase font-barlow">
|
<h5 className="text-lg font-semibold uppercase font-barlow">
|
||||||
Total Debit
|
Total Debit
|
||||||
@@ -95,7 +95,7 @@ export async function WalletTransactionsTable({
|
|||||||
{transactions?.data?.map((trx) => (
|
{transactions?.data?.map((trx) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
className={cn(
|
className={cn(
|
||||||
"items-start border rounded p-2",
|
"items-start border rounded p-2 motion-preset-slide-down-sm",
|
||||||
trx?.transaction_type === "TOPUP"
|
trx?.transaction_type === "TOPUP"
|
||||||
? "credit-bg"
|
? "credit-bg"
|
||||||
: "debit-bg",
|
: "debit-bg",
|
||||||
|
@@ -4,36 +4,36 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface WelcomeBannerProps {
|
interface WelcomeBannerProps {
|
||||||
firstName?: string | null;
|
firstName?: string | null;
|
||||||
lastName?: string | null;
|
lastName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WelcomeBanner({ firstName, lastName }: WelcomeBannerProps) {
|
export function WelcomeBanner({ firstName, lastName }: WelcomeBannerProps) {
|
||||||
const [isVisible, setIsVisible] = useState(true);
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
}, 4000);
|
}, 4000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="text-sm font-mono px-2 p-1 bg-green-500/10 text-green-900 dark:text-green-400"
|
className="text-sm font-mono px-2 p-1 bg-green-500/10 text-green-900 dark:text-green-400"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
Welcome,{" "}
|
Welcome,{" "}
|
||||||
<p className="font-semibold motion-preset-slide-down inline-block motion-delay-200">
|
<p className="font-semibold motion-preset-fade inline-block motion-delay-200">
|
||||||
{firstName} {lastName}
|
{firstName} {lastName}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user