refactor: add animations
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 10m27s

This commit is contained in:
2025-09-24 19:33:48 +05:00
parent f8774f51e6
commit 9ad1887f88
10 changed files with 994 additions and 993 deletions

View File

@@ -27,11 +27,16 @@ export default async function DeviceDetails({
<div>
<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">
<h3 className="text-2xl text-sarLinkOrange font-bold">
<h3 className="text-2xl text-sarLinkOrange motion-preset-slide-down-md font-bold">
{device?.name}
</h3>
<Badge variant={"secondary"}>{device?.mac}</Badge>
<p className="text-muted-foreground text-sm mt-2">
<Badge
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{" "}
{new Date(device?.expiry_date || "").toLocaleDateString("en-US", {
month: "short",
@@ -40,7 +45,7 @@ export default async function DeviceDetails({
})}
</p>
</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) && (
<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

View File

@@ -9,96 +9,99 @@ import { cn } from "@/lib/utils";
import AddDevicesToCartButton from "./add-devices-to-cart-button";
import BlockDeviceDialog from "./block-device-dialog";
export default function ClickableRow({
device,
parentalControl,
admin = false,
device,
parentalControl,
admin = false,
idx,
}: {
device: Device;
parentalControl?: boolean;
admin?: boolean;
device: Device;
parentalControl?: boolean;
admin?: boolean;
idx?: number;
}) {
const [devices, setDeviceCart] = useAtom(deviceCartAtom);
const [devices, setDeviceCart] = useAtom(deviceCartAtom);
return (
<TableRow
key={device.id}
className={cn(
(parentalControl === false && device.blocked) || device.is_active
? "cursor-not-allowed hover:bg-accent-foreground/10"
: "cursor-pointer hover:bg-muted-foreground/10",
)}
onClick={() => {
if (device.blocked) return;
if (device.is_active === true) return;
if (device.has_a_pending_payment === true) return;
if (parentalControl === true) return;
setDeviceCart((prev) =>
devices.some((d) => d.id === device.id)
? prev.filter((d) => d.id !== device.id)
: [...prev, device],
);
}}
>
<TableCell>
<div className="flex flex-col items-start">
<Link
className={cn(
"hover:underline font-semibold",
device.is_active ? "text-green-600" : "",
)}
href={`/devices/${device.id}`}
onClick={(e) => e.stopPropagation()}
>
{device.name}
</Link>
{device.is_active ? (
<div className="text-muted-foreground">
Active until{" "}
<span className="font-semibold">
{new Date(device.expiry_date || "").toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
},
)}
</span>
</div>
) : (
<p className="text-muted-foreground">Device Inactive</p>
)}
{device.has_a_pending_payment && (
<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-muted-foreground">
Payment Pending{" "}
<HandCoins className="animate-pulse" size={14} />
</span>
</Link>
)}
return (
<TableRow
key={device.id}
className={cn(
(parentalControl === false && device.blocked) || device.is_active
? "cursor-not-allowed hover:bg-accent-foreground/10"
: "cursor-pointer hover:bg-muted-foreground/10",
`motion-preset-fade-md motion-delay-${(idx || 1) * 75}ms`,
)}
onClick={() => {
if (device.blocked) return;
if (device.is_active === true) return;
if (device.has_a_pending_payment === true) return;
if (parentalControl === true) return;
setDeviceCart((prev) =>
devices.some((d) => d.id === device.id)
? prev.filter((d) => d.id !== device.id)
: [...prev, device],
);
}}
>
<TableCell>
<div className="flex flex-col items-start">
<Link
className={cn(
"hover:underline font-semibold",
device.is_active ? "text-green-600" : "",
)}
href={`/devices/${device.id}`}
onClick={(e) => e.stopPropagation()}
>
{device.name}
</Link>
{device.is_active ? (
<div className="text-muted-foreground">
Active until{" "}
<span className="font-semibold">
{new Date(device.expiry_date || "").toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
},
)}
</span>
</div>
) : (
<p className="text-muted-foreground">Device Inactive</p>
)}
{device.has_a_pending_payment && (
<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-muted-foreground">
Payment Pending{" "}
<HandCoins className="animate-pulse" size={14} />
</span>
</Link>
)}
{device.blocked_by === "ADMIN" && device.blocked && (
<div className="p-2 rounded border my-2 bg-white dark:bg-neutral-800 shadow">
<span className="font-semibold">Comment</span>
<p className="text-neutral-400">{device?.reason_for_blocking}</p>
</div>
)}
</div>
</TableCell>
<TableCell className="font-medium">{device.mac}</TableCell>
<TableCell className="font-medium">{device?.vendor}</TableCell>
<TableCell>
{!parentalControl ? (
<AddDevicesToCartButton device={device} />
) : (
<BlockDeviceDialog
admin={admin}
type={device.blocked ? "unblock" : "block"}
device={device}
parentalControl={parentalControl}
/>
)}
</TableCell>
</TableRow>
);
{device.blocked_by === "ADMIN" && device.blocked && (
<div className="p-2 rounded border my-2 bg-white dark:bg-neutral-800 shadow">
<span className="font-semibold">Comment</span>
<p className="text-neutral-400">{device?.reason_for_blocking}</p>
</div>
)}
</div>
</TableCell>
<TableCell className="font-medium">{device.mac}</TableCell>
<TableCell className="font-medium">{device?.vendor}</TableCell>
<TableCell>
{!parentalControl ? (
<AddDevicesToCartButton device={device} />
) : (
<BlockDeviceDialog
admin={admin}
type={device.blocked ? "unblock" : "block"}
device={device}
parentalControl={parentalControl}
/>
)}
</TableCell>
</TableRow>
);
}

View File

@@ -10,108 +10,108 @@ import BlockDeviceDialog from "./block-device-dialog";
import { Badge } from "./ui/badge";
export default function DeviceCard({
device,
parentalControl,
isAdmin,
device,
parentalControl,
isAdmin,
}: {
device: Device;
parentalControl?: boolean;
isAdmin?: boolean;
device: Device;
parentalControl?: 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 (
// biome-ignore lint/a11y/noStaticElementInteractions: <dw about it>
<div
onKeyUp={() => {}}
onClick={() => {
if (device.blocked) return;
if (device.is_active === true) return;
if (device.has_a_pending_payment === true) return;
if (parentalControl === true) return;
setDeviceCart((prev) =>
devices.some((d) => d.id === device.id)
? prev.filter((d) => d.id !== device.id)
: [...prev, device],
);
}}
className="w-full"
>
<div
className={cn(
"flex text-sm justify-between items-center my-2 p-4 border rounded-md",
isChecked ? "bg-accent" : "",
device.is_active
? "cursor-not-allowed text-green-600 hover:bg-accent-foreground/10"
: "cursor-pointer hover:bg-muted-foreground/10",
)}
>
<div className="">
<div className="font-semibold flex flex-col items-start gap-2 mb-2">
<Link
className={cn(
"font-medium hover:underline ml-0.5",
device.is_active ? "text-green-600" : "",
)}
href={`/devices/${device.id}`}
>
{device.name}
</Link>
<Badge variant={"outline"}>
<span className="font-medium">{device.mac}</span>
</Badge>
<Badge variant={"outline"}>
<span className="font-medium">{device.vendor}</span>
</Badge>
</div>
return (
// biome-ignore lint/a11y/noStaticElementInteractions: <dw about it>
<div
onKeyUp={() => {}}
onClick={() => {
if (device.blocked) return;
if (device.is_active === true) return;
if (device.has_a_pending_payment === true) return;
if (parentalControl === true) return;
setDeviceCart((prev) =>
devices.some((d) => d.id === device.id)
? prev.filter((d) => d.id !== device.id)
: [...prev, device],
);
}}
className="w-full"
>
<div
className={cn(
"flex text-sm justify-between items-center my-2 p-4 border rounded-md motion-preset-fade-md",
isChecked ? "bg-accent" : "",
device.is_active
? "cursor-not-allowed text-green-600 hover:bg-accent-foreground/10"
: "cursor-pointer hover:bg-muted-foreground/10",
)}
>
<div className="">
<div className="font-semibold flex flex-col items-start gap-2 mb-2">
<Link
className={cn(
"font-medium hover:underline ml-0.5",
device.is_active ? "text-green-600" : "",
)}
href={`/devices/${device.id}`}
>
{device.name}
</Link>
<Badge variant={"outline"}>
<span className="font-medium">{device.mac}</span>
</Badge>
<Badge variant={"outline"}>
<span className="font-medium">{device.vendor}</span>
</Badge>
</div>
{device.is_active ? (
<div className="text-muted-foreground ml-0.5">
Active until{" "}
<span className="font-semibold">
{new Date(device.expiry_date || "").toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
},
)}
</span>
</div>
) : (
<p className="text-muted-foreground ml-0.5">Device Inactive</p>
)}
{device.has_a_pending_payment && (
<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">
Payment Pending{" "}
<HandCoins className="animate-pulse" size={14} />
</span>
</Link>
)}
{device.is_active ? (
<div className="text-muted-foreground ml-0.5">
Active until{" "}
<span className="font-semibold">
{new Date(device.expiry_date || "").toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
},
)}
</span>
</div>
) : (
<p className="text-muted-foreground ml-0.5">Device Inactive</p>
)}
{device.has_a_pending_payment && (
<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">
Payment Pending{" "}
<HandCoins className="animate-pulse" size={14} />
</span>
</Link>
)}
{device.blocked && device.blocked_by === "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">{device?.reason_for_blocking}</p>
</div>
)}
</div>
<div>
{!parentalControl ? (
<AddDevicesToCartButton device={device} />
) : (
<BlockDeviceDialog
admin={isAdmin}
type={device.blocked ? "unblock" : "block"}
device={device}
/>
)}
</div>
</div>
</div>
);
{device.blocked && device.blocked_by === "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">{device?.reason_for_blocking}</p>
</div>
)}
</div>
<div>
{!parentalControl ? (
<AddDevicesToCartButton device={device} />
) : (
<BlockDeviceDialog
admin={isAdmin}
type={device.blocked ? "unblock" : "block"}
device={device}
/>
)}
</div>
</div>
</div>
);
}

View File

@@ -2,13 +2,13 @@ import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getDevices } from "@/queries/devices";
import { tryCatch } from "@/utils/tryCatch";
@@ -18,107 +18,108 @@ import DeviceCard from "./device-card";
import Pagination from "./pagination";
export async function DevicesTable({
searchParams,
parentalControl,
additionalFilters = {},
searchParams,
parentalControl,
additionalFilters = {},
}: {
searchParams: Promise<{
[key: string]: unknown;
}>;
parentalControl?: boolean;
additionalFilters?: Record<string, string | number | boolean>;
searchParams: Promise<{
[key: string]: unknown;
}>;
parentalControl?: boolean;
additionalFilters?: Record<string, string | number | boolean>;
}) {
const resolvedParams = await searchParams;
const session = await getServerSession(authOptions);
const isAdmin = session?.user?.is_admin;
const resolvedParams = await searchParams;
const session = await getServerSession(authOptions);
const isAdmin = session?.user?.is_admin;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
// Build params object for getDevices
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
// Build params object for getDevices
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
for (const [key, value] of Object.entries(additionalFilters)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, devices] = await tryCatch(getDevices(apiParams));
if (error) {
if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
return <ClientErrorMessage message={error.message} />;
}
}
const { meta, data } = devices;
return (
<div>
{data?.length === 0 ? (
<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>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Device Name</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>Vendor</TableHead>
<TableHead>#</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{data?.map((device) => (
<ClickableRow
admin={isAdmin}
key={device.id}
device={device}
parentalControl={parentalControl}
/>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} device.</p>
) : (
<p className="text-center">
Total {meta?.total} devices.
</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<div className="sm:hidden my-4">
{data?.map((device) => (
<DeviceCard
parentalControl={parentalControl}
key={device.id}
device={device}
isAdmin={isAdmin}
/>
))}
</div>
<Pagination
totalPages={meta?.last_page}
currentPage={meta?.current_page}
/>
</>
)}
</div>
);
for (const [key, value] of Object.entries(additionalFilters)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, devices] = await tryCatch(getDevices(apiParams));
if (error) {
if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
return <ClientErrorMessage message={error.message} />;
}
}
const { meta, data } = devices;
return (
<div>
{data?.length === 0 ? (
<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>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Device Name</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>Vendor</TableHead>
<TableHead>#</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{data?.map((device, idx) => (
<ClickableRow
admin={isAdmin}
key={device.id}
device={device}
parentalControl={parentalControl}
idx={idx + 1}
/>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} device.</p>
) : (
<p className="text-center">
Total {meta?.total} devices.
</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<div className="sm:hidden my-4">
{data?.map((device) => (
<DeviceCard
parentalControl={parentalControl}
key={device.id}
device={device}
isAdmin={isAdmin}
/>
))}
</div>
<Pagination
totalPages={meta?.last_page}
currentPage={meta?.current_page}
/>
</>
)}
</div>
);
}

View File

@@ -126,6 +126,7 @@ export default function DevicesToPay({
type="submit"
variant={"secondary"}
size={"lg"}
className="w-full"
>
{isPending
? "Processing payment..."
@@ -145,7 +146,7 @@ export default function DevicesToPay({
disabled={isPending || disabled}
type="submit"
size={"lg"}
className="mb-4"
className="mb-4 w-full"
>
{isPending ? "Processing payment..." : "I have paid"}
{isPending ? (
@@ -162,10 +163,8 @@ export default function DevicesToPay({
</TableCaption>
<TableBody className="">
<TableRow>
<TableCell className="motion-preset-slide-left-sm">
Payment created
</TableCell>
<TableCell className="text-right motion-preset-slide-right-sm">
<TableCell>Payment created</TableCell>
<TableCell className="text-right motion-preset-slide-up-sm">
{new Date(payment?.created_at ?? "").toLocaleDateString(
"en-US",
{
@@ -180,31 +179,22 @@ export default function DevicesToPay({
</TableCell>
</TableRow>
<TableRow>
<TableCell className="motion-preset-slide-left-sm motion-delay-75">
Total Devices
</TableCell>
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-75">
<TableCell>Total Devices</TableCell>
<TableCell className="text-right text-xl motion-preset-slide-up-sm motion-delay-75">
{devices?.length}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="motion-preset-slide-left-sm motion-delay-100">
Duration
</TableCell>
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-100">
<TableCell>Duration</TableCell>
<TableCell className="text-right text-xl motion-preset-slide-up-sm motion-delay-100">
{payment?.number_of_months} Months
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow className="">
<TableCell
className="motion-preset-slide-left-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">
<TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold motion-preset-slide-up-sm motion-delay-150">
{payment?.amount?.toFixed(2)}
</TableCell>
</TableRow>

View File

@@ -3,13 +3,13 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import { getPayments } from "@/actions/payment";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { Payment } from "@/lib/backend-types";
import { cn } from "@/lib/utils";
@@ -20,265 +20,265 @@ import { Button } from "./ui/button";
import { Separator } from "./ui/separator";
export async function PaymentsTable({
searchParams,
searchParams,
}: {
searchParams: Promise<{
[key: string]: unknown;
}>;
searchParams: Promise<{
[key: string]: unknown;
}>;
}) {
const resolvedParams = await searchParams;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, payments] = await tryCatch(getPayments(apiParams));
const resolvedParams = await searchParams;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, payments] = await tryCatch(getPayments(apiParams));
if (error) {
if (error.message.includes("Unauthorized")) {
redirect("/auth/signin");
} else {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
}
const { data, meta } = payments;
return (
<div>
{data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
<h3>No Payments.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Duration</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{payments?.data?.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"
: payment?.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">
{new Date(payment.created_at).toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
timeZone: "Indian/Maldives", // Force consistent timezone
},
)}
</span>
</div>
if (error) {
if (error.message.includes("Unauthorized")) {
redirect("/auth/signin");
} else {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
}
const { data, meta } = payments;
return (
<div>
{data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] text-muted-foreground flex flex-col items-center justify-center my-4">
<h3>No Payments.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Duration</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{payments?.data?.map((payment) => (
<TableRow className="motion-preset-fade-md" 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"
: payment?.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">
{new Date(payment.created_at).toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
timeZone: "Indian/Maldives", // Force consistent timezone
},
)}
</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>
</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>
</TableCell>
<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>
</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>
</TableCell>
<TableCell className="font-medium">
{payment.number_of_months} Months
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.paid ? (
<Badge
className={cn(
payment.status === "PENDING"
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
: "bg-green-100 dark:bg-green-700",
)}
variant="outline"
>
{payment.status}
</Badge>
) : payment.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="outline">{payment.status}</Badge>
)}
</span>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">
Total {meta?.total} payment.
</p>
) : (
<p className="text-center">
Total {meta?.total} payments.
</p>
)}{" "}
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination
totalPages={meta.last_page}
currentPage={meta.current_page}
/>
</div>
<div className="sm:hidden block">
{data.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} />
))}
</div>
</>
)}
</div>
);
<TableCell className="font-medium">
{payment.number_of_months} Months
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.paid ? (
<Badge
className={cn(
payment.status === "PENDING"
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
: "bg-green-100 dark:bg-green-700",
)}
variant="outline"
>
{payment.status}
</Badge>
) : payment.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="outline">{payment.status}</Badge>
)}
</span>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">
Total {meta?.total} payment.
</p>
) : (
<p className="text-center">
Total {meta?.total} payments.
</p>
)}{" "}
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination
totalPages={meta.last_page}
currentPage={meta.current_page}
/>
</div>
<div className="sm:hidden block">
{data.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} />
))}
</div>
</>
)}
</div>
);
}
export function MobilePaymentDetails({
payment,
isAdmin = false,
payment,
isAdmin = false,
}: {
payment: Payment;
isAdmin?: boolean;
payment: Payment;
isAdmin?: boolean;
}) {
return (
<div
className={cn(
"flex flex-col items-start border rounded p-2 my-2",
payment?.paid
? "bg-green-500/10 border-dashed border-green-500"
: payment?.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">
{new Date(payment.created_at).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
timeZone: "Indian/Maldives", // Force consistent timezone
})}
</span>
</div>
return (
<div
className={cn(
"flex flex-col items-start border rounded p-2 my-2 motion-preset-fade-md",
payment?.paid
? "bg-green-500/10 border-dashed border-green-500"
: payment?.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">
{new Date(payment.created_at).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
timeZone: "Indian/Maldives", // Force consistent timezone
})}
</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>
</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.number_of_months} Months
</span>
<Separator className="my-2" />
<h3 className="text-sm font-medium">Amount</h3>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{payment.amount.toFixed(2)} MVR
</span>
<span className="font-semibold pr-2">
{payment.paid ? (
<Badge
className={cn(
payment.status === "PENDING"
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
: "bg-green-100 dark:bg-green-700",
)}
variant="outline"
>
{payment.status}
</Badge>
) : payment.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="secondary">{payment.status}</Badge>
)}
</span>
{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">
{payment?.user?.name}
<span className="text-muted-foreground">
{payment?.user?.id_card}
</span>
</div>
)}
</div>
</div>
</div>
</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>
</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.number_of_months} Months
</span>
<Separator className="my-2" />
<h3 className="text-sm font-medium">Amount</h3>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{payment.amount.toFixed(2)} MVR
</span>
<span className="font-semibold pr-2">
{payment.paid ? (
<Badge
className={cn(
payment.status === "PENDING"
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100"
: "bg-green-100 dark:bg-green-700",
)}
variant="outline"
>
{payment.status}
</Badge>
) : payment.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="secondary">{payment.status}</Badge>
)}
</span>
{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">
{payment?.user?.name}
<span className="text-muted-foreground">
{payment?.user?.id_card}
</span>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -3,13 +3,13 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import { getTopups } from "@/actions/payment";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { Topup } from "@/lib/backend-types";
import { cn } from "@/lib/utils";
@@ -19,199 +19,201 @@ import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
export async function TopupsTable({
searchParams,
searchParams,
}: {
searchParams: Promise<{
[key: string]: unknown;
}>;
searchParams: Promise<{
[key: string]: unknown;
}>;
}) {
const resolvedParams = await searchParams;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
// Build params object
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, topups] = await tryCatch(getTopups(apiParams));
const resolvedParams = await searchParams;
const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10;
const offset = (page - 1) * limit;
// Build params object
const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value);
}
}
apiParams.limit = limit;
apiParams.offset = offset;
const [error, topups] = await tryCatch(getTopups(apiParams));
if (error) {
if (error.message.includes("Unauthorized")) {
redirect("/auth/signin");
} else {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
}
const { data, meta } = topups;
return (
<div>
{data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex text-muted-foreground flex-col items-center justify-center my-4">
<h3>No topups.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{topups?.data?.map((topup) => (
<TableRow key={topup.id}>
<TableCell>
<div
className={cn(
"flex flex-col items-start border rounded p-2",
topup?.paid
? "bg-green-500/10 border-dashed border-green-500"
: 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">
{new Date(topup.created_at).toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
},
)}
</span>
</div>
if (error) {
if (error.message.includes("Unauthorized")) {
redirect("/auth/signin");
} else {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
}
const { data, meta } = topups;
return (
<div>
{data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex text-muted-foreground flex-col items-center justify-center my-4">
<h3>No topups.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{topups?.data?.map((topup) => (
<TableRow key={topup.id}>
<TableCell>
<div
className={cn(
"flex flex-col items-start border rounded p-2 motion-preset-fade-md",
topup?.paid
? "bg-green-500/10 border-dashed border-green-500"
: 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">
{new Date(topup.created_at).toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
minute: "2-digit",
hour: "2-digit",
},
)}
</span>
</div>
<div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/top-ups/${topup.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
</div>
</div>
</TableCell>
<div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/top-ups/${topup.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
</div>
</div>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{topup.paid ? (
<Badge
className="bg-green-100 dark:bg-green-700"
variant="outline"
>
{topup.status}
</Badge>
) : topup.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="outline">{topup.status}</Badge>
)}
</span>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{topup.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} topup.</p>
) : (
<p className="text-center">Total {meta?.total} topups.</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<div className="sm:hidden block">
{data.map((topup) => (
<MobileTopupDetails key={topup.id} topup={topup} />
))}
</div>
<Pagination
totalPages={meta?.last_page}
currentPage={meta?.current_page}
/>
</>
)}
</div>
);
<TableCell>
<span className="font-semibold pr-2">
{topup.paid ? (
<Badge
className="bg-green-100 dark:bg-green-700"
variant="outline"
>
{topup.status}
</Badge>
) : topup.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="outline">{topup.status}</Badge>
)}
</span>
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{topup.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-muted-foreground">
{meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} topup.</p>
) : (
<p className="text-center">Total {meta?.total} topups.</p>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<div className="sm:hidden block">
{data.map((topup) => (
<MobileTopupDetails key={topup.id} topup={topup} />
))}
</div>
<Pagination
totalPages={meta?.last_page}
currentPage={meta?.current_page}
/>
</>
)}
</div>
);
}
function MobileTopupDetails({ topup }: { topup: Topup }) {
return (
<div
className={cn(
"flex flex-col items-start border rounded p-2 my-2",
topup?.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(topup.created_at).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
</div>
return (
<div
className={cn(
"flex flex-col items-start border rounded p-2 my-2 motion-preset-fade-md",
topup?.paid
? "bg-green-500/10 border-dashed border-green-500"
: 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">
{new Date(topup.created_at).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={`/top-ups/${topup.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
</div>
<div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/top-ups/${topup.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
</div>
<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">
<h3 className="text-sm font-medium">Amount</h3>
<span className="text-sm text-muted-foreground">
{topup.amount.toFixed(2)} MVR
</span>
</div>
<span className="font-semibold pr-2">
{topup.paid ? (
<Badge className="bg-green-100 dark:bg-green-700" variant="outline">
{topup.status}
</Badge>
) : topup.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="secondary">{topup.status}</Badge>
)}
</span>
</div>
</div>
);
<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">
<h3 className="text-sm font-medium">Amount</h3>
<span className="text-sm text-muted-foreground">
{topup.amount.toFixed(2)} MVR
</span>
</div>
<span className="font-semibold pr-2">
{topup.paid ? (
<Badge className="bg-green-100 dark:bg-green-700" variant="outline">
{topup.status}
</Badge>
) : topup.is_expired ? (
<Badge>Expired</Badge>
) : (
<Badge variant="secondary">{topup.status}</Badge>
)}
</span>
</div>
</div>
);
}

View File

@@ -1,226 +1,226 @@
import {
BadgePlus,
Calculator,
ChevronRight,
Coins,
CreditCard,
Handshake,
MonitorSpeaker,
Smartphone,
UsersRound,
Wallet2Icon,
BadgePlus,
Calculator,
ChevronRight,
Coins,
CreditCard,
Handshake,
MonitorSpeaker,
Smartphone,
UsersRound,
Wallet2Icon,
} from "lucide-react";
import Link from "next/link";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/auth";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from "@/components/ui/sidebar";
type Permission = {
id: number;
name: string;
id: number;
name: string;
};
type Categories = {
id: string;
children: (
| {
title: string;
link: string;
perm_identifier: string;
icon: React.JSX.Element;
}
| {
title: string;
link: string;
icon: React.JSX.Element;
perm_identifier?: undefined;
}
)[];
id: string;
children: (
| {
title: string;
link: string;
perm_identifier: string;
icon: React.JSX.Element;
}
| {
title: string;
link: string;
icon: React.JSX.Element;
perm_identifier?: undefined;
}
)[];
}[];
export async function AppSidebar({
...props
...props
}: React.ComponentProps<typeof Sidebar>) {
const categories = [
{
id: "MENU",
url: "#",
children: [
{
title: "Devices",
link: "/devices?page=1",
perm_identifier: "device",
icon: <Smartphone size={16} />,
},
{
title: "Parental Control",
link: "/parental-control?page=1",
icon: <CreditCard size={16} />,
perm_identifier: "device",
},
{
title: "Subscriptions",
link: "/payments?page=1",
icon: <CreditCard size={16} />,
perm_identifier: "payment",
},
{
title: "Top Ups",
link: "/top-ups?page=1",
icon: <BadgePlus size={16} />,
perm_identifier: "topup",
},
{
title: "Transaction History",
link: "/wallet",
icon: <Wallet2Icon size={16} />,
perm_identifier: "wallet transaction",
},
{
title: "Agreements",
link: "/agreements",
icon: <Handshake size={16} />,
perm_identifier: "device",
},
],
},
{
id: "ADMIN CONTROL",
url: "#",
children: [
{
title: "Users",
link: "/users",
icon: <UsersRound size={16} />,
perm_identifier: "device",
},
{
title: "User Devices",
link: "/user-devices",
icon: <MonitorSpeaker size={16} />,
perm_identifier: "device",
},
{
title: "User Payments",
link: "/user-payments",
icon: <Coins size={16} />,
perm_identifier: "payment",
},
{
title: "User Topups",
link: "/user-topups",
icon: <Coins size={16} />,
perm_identifier: "topup",
},
{
title: "Price Calculator",
link: "/price-calculator",
icon: <Calculator size={16} />,
perm_identifier: "device",
},
],
},
];
const categories = [
{
id: "MENU",
url: "#",
children: [
{
title: "Devices",
link: "/devices?page=1",
perm_identifier: "device",
icon: <Smartphone size={16} />,
},
{
title: "Parental Control",
link: "/parental-control?page=1",
icon: <CreditCard size={16} />,
perm_identifier: "device",
},
{
title: "Subscriptions",
link: "/payments?page=1",
icon: <CreditCard size={16} />,
perm_identifier: "payment",
},
{
title: "Top Ups",
link: "/top-ups?page=1",
icon: <BadgePlus size={16} />,
perm_identifier: "topup",
},
{
title: "Transaction History",
link: "/wallet",
icon: <Wallet2Icon size={16} />,
perm_identifier: "wallet transaction",
},
{
title: "Agreements",
link: "/agreements",
icon: <Handshake size={16} />,
perm_identifier: "device",
},
],
},
{
id: "ADMIN CONTROL",
url: "#",
children: [
{
title: "Users",
link: "/users",
icon: <UsersRound size={16} />,
perm_identifier: "device",
},
{
title: "User Devices",
link: "/user-devices",
icon: <MonitorSpeaker size={16} />,
perm_identifier: "device",
},
{
title: "User Payments",
link: "/user-payments",
icon: <Coins size={16} />,
perm_identifier: "payment",
},
{
title: "User Topups",
link: "/user-topups",
icon: <Coins size={16} />,
perm_identifier: "topup",
},
{
title: "Price Calculator",
link: "/price-calculator",
icon: <Calculator size={16} />,
perm_identifier: "device",
},
],
},
];
const session = await getServerSession(authOptions);
const session = await getServerSession(authOptions);
let CATEGORIES: Categories;
if (session?.user?.is_admin) {
CATEGORIES = categories;
} else {
// Filter out ADMIN CONTROL category for non-admin users
const nonAdminCategories = categories.filter(
(category) => category.id !== "ADMIN CONTROL",
);
let CATEGORIES: Categories;
if (session?.user?.is_admin) {
CATEGORIES = categories;
} else {
// Filter out ADMIN CONTROL category for non-admin users
const nonAdminCategories = categories.filter(
(category) => category.id !== "ADMIN CONTROL",
);
const filteredCategories = nonAdminCategories.map((category) => {
const filteredChildren = category.children.filter((child) => {
const permIdentifier = child.perm_identifier;
return session?.user?.user_permissions?.some(
(permission: Permission) => {
const permissionParts = permission.name.split(" ");
const modelNameFromPermission = permissionParts.slice(2).join(" ");
return modelNameFromPermission === permIdentifier;
},
);
});
const filteredCategories = nonAdminCategories.map((category) => {
const filteredChildren = category.children.filter((child) => {
const permIdentifier = child.perm_identifier;
return session?.user?.user_permissions?.some(
(permission: Permission) => {
const permissionParts = permission.name.split(" ");
const modelNameFromPermission = permissionParts.slice(2).join(" ");
return modelNameFromPermission === permIdentifier;
},
);
});
return { ...category, children: filteredChildren };
});
return { ...category, children: filteredChildren };
});
CATEGORIES = filteredCategories.filter(
(category) => category.children.length > 0,
);
}
CATEGORIES = filteredCategories.filter(
(category) => category.children.length > 0,
);
}
return (
<Sidebar {...props} className="z-50">
<SidebarHeader>
<h4 className="p-2 rounded title-bg border text-center uppercase ">
Sar Link Portal
</h4>
</SidebarHeader>
<SidebarContent className="gap-0">
{CATEGORIES.map((item) => {
return (
<Collapsible
key={item.id}
title={item.id}
defaultOpen
className="group/collapsible"
>
<SidebarGroup>
<SidebarGroupLabel
asChild
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<CollapsibleTrigger>
{item.id}{" "}
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{item.children.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton className="py-6" asChild>
<Link className="text-md" href={item.link}>
{item.icon}
<span
className={`opacity-70 motion-preset-slide-left-md ml-2`}
>
{item.title}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
);
})}
</SidebarContent>
<SidebarRail />
</Sidebar>
);
return (
<Sidebar {...props} className="z-50">
<SidebarHeader>
<h4 className="p-2 rounded title-bg border text-center uppercase ">
Sar Link Portal
</h4>
</SidebarHeader>
<SidebarContent className="gap-0">
{CATEGORIES.map((item) => {
return (
<Collapsible
key={item.id}
title={item.id}
defaultOpen
className="group/collapsible"
>
<SidebarGroup>
<SidebarGroupLabel
asChild
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<CollapsibleTrigger>
{item.id}{" "}
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{item.children.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton className="py-6" asChild>
<Link className="text-md" href={item.link}>
{item.icon}
<span
className={`opacity-70 motion-preset-fade motion-duration-300 ml-2`}
>
{item.title}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
);
})}
</SidebarContent>
<SidebarRail />
</Sidebar>
);
}

View File

@@ -66,7 +66,7 @@ export async function WalletTransactionsTable({
</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">
<h5 className="text-lg font-semibold uppercase font-barlow">
Total Debit
@@ -95,7 +95,7 @@ export async function WalletTransactionsTable({
{transactions?.data?.map((trx) => (
<TableRow
className={cn(
"items-start border rounded p-2",
"items-start border rounded p-2 motion-preset-slide-down-sm",
trx?.transaction_type === "TOPUP"
? "credit-bg"
: "debit-bg",

View File

@@ -4,36 +4,36 @@ import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState } from "react";
interface WelcomeBannerProps {
firstName?: string | null;
lastName?: string | null;
firstName?: string | null;
lastName?: string | null;
}
export function WelcomeBanner({ firstName, lastName }: WelcomeBannerProps) {
const [isVisible, setIsVisible] = useState(true);
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(false);
}, 4000);
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(false);
}, 4000);
return () => clearTimeout(timer);
}, []);
return () => clearTimeout(timer);
}, []);
return (
<AnimatePresence>
{isVisible && (
<motion.div
className="text-sm font-mono px-2 p-1 bg-green-500/10 text-green-900 dark:text-green-400"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Welcome,{" "}
<p className="font-semibold motion-preset-slide-down inline-block motion-delay-200">
{firstName} {lastName}
</p>
</motion.div>
)}
</AnimatePresence>
);
return (
<AnimatePresence>
{isVisible && (
<motion.div
className="text-sm font-mono px-2 p-1 bg-green-500/10 text-green-900 dark:text-green-400"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Welcome,{" "}
<p className="font-semibold motion-preset-fade inline-block motion-delay-200">
{firstName} {lastName}
</p>
</motion.div>
)}
</AnimatePresence>
);
}