refactor: remove table captions from all tables 🔧

This commit is contained in:
2025-09-20 14:40:10 +05:00
parent 19043aa692
commit 39e84723b1
9 changed files with 1380 additions and 1413 deletions

View File

@@ -1,78 +1,76 @@
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export default function DevicesTableSkeleton() { export default function DevicesTableSkeleton() {
return ( return (
<> <>
<div className="hidden sm:block"> <div className="hidden sm:block">
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Device Name</TableHead>
<TableHead>Device Name</TableHead> <TableHead>MAC Address</TableHead>
<TableHead>MAC Address</TableHead> <TableHead>#</TableHead>
<TableHead>#</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody className="overflow-scroll">
<TableBody className="overflow-scroll"> {Array.from({ length: 10 }).map((_, i) => (
{Array.from({ length: 10 }).map((_, i) => ( <TableRow key={`${i + 1}`}>
<TableRow key={`${i + 1}`}> <TableCell>
<TableCell> <Skeleton className="w-full h-10 rounded" />
<Skeleton className="w-full h-10 rounded" /> </TableCell>
</TableCell> <TableCell>
<TableCell> <Skeleton className="w-full h-10 rounded" />
<Skeleton className="w-full h-10 rounded" /> </TableCell>
</TableCell> <TableCell>
<TableCell> <Skeleton className="w-full h-10 rounded" />
<Skeleton className="w-full h-10 rounded" /> </TableCell>
</TableCell> </TableRow>
</TableRow> ))}
))} </TableBody>
</TableBody> <TableFooter>
<TableFooter> <TableRow>
<TableRow> <TableCell colSpan={2}>
<TableCell colSpan={2}> <Skeleton className="w-full h-4 rounded" />
<Skeleton className="w-full h-4 rounded" /> </TableCell>
</TableCell> <TableCell className="text-muted-foreground">
<TableCell className="text-muted-foreground"> <Skeleton className="w-20 h-4 rounded" />
<Skeleton className="w-20 h-4 rounded" /> </TableCell>
</TableCell> </TableRow>
</TableRow> </TableFooter>
</TableFooter> </Table>
</Table> </div>
</div> <div className="sm:hidden my-4">
<div className="sm:hidden my-4"> {Array.from({ length: 10 }).map((_, i) => (
{Array.from({ length: 10 }).map((_, i) => ( <DeviceCardSkeleton key={`${i + 1}`} />
<DeviceCardSkeleton key={`${i + 1}`} /> ))}
))} </div>
</div> </>
</> );
);
} }
function DeviceCardSkeleton() { function DeviceCardSkeleton() {
return ( return (
<div <div
className={cn( className={cn(
"flex text-sm justify-between items-center my-2 p-4 border rounded-md bg-gray-100", "flex text-sm justify-between items-center my-2 p-4 border rounded-md bg-gray-100",
)} )}
> >
<div className="font-semibold flex w-full flex-col items-start gap-2 mb-2 relative"> <div className="font-semibold flex w-full flex-col items-start gap-2 mb-2 relative">
<Skeleton className="w-32 h-6" /> <Skeleton className="w-32 h-6" />
<Skeleton className="w-36 h-6" /> <Skeleton className="w-36 h-6" />
<Skeleton className="w-32 h-4" /> <Skeleton className="w-32 h-4" />
<Skeleton className="w-40 h-8" /> <Skeleton className="w-40 h-8" />
</div> </div>
</div> </div>
); );
} }

View File

@@ -4,14 +4,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,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { getDevices } from "@/queries/devices"; import { getDevices } from "@/queries/devices";
@@ -21,156 +20,155 @@ import ClientErrorMessage from "../client-error-message";
import Pagination from "../pagination"; import Pagination from "../pagination";
export async function AdminDevicesTable({ export async function AdminDevicesTable({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
[key: string]: unknown; [key: string]: unknown;
}>; }>;
}) { }) {
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);
} }
} }
apiParams.limit = limit; apiParams.limit = limit;
apiParams.offset = offset; apiParams.offset = offset;
const [error, devices] = await tryCatch(getDevices(apiParams, true)); const [error, devices] = await tryCatch(getDevices(apiParams, true));
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>No devices.</h3> <h3>No devices.</h3>
</div> </div>
) : ( ) : (
<> <>
<div> <div>
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Device Name</TableHead>
<TableHead>Device Name</TableHead> <TableHead>User</TableHead>
<TableHead>User</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) => ( <TableRow key={device.id}>
<TableRow key={device.id}> <TableCell>
<TableCell> <div className="flex flex-col items-start">
<div className="flex flex-col items-start"> <Link
<Link className={cn(
className={cn( "hover:underline font-semibold",
"hover:underline font-semibold", 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> {device.is_active ? (
{device.is_active ? ( <div className="text-muted-foreground">
<div className="text-muted-foreground"> Active until{" "}
Active until{" "} <span className="font-semibold">
<span className="font-semibold"> {new Date(
{new Date( device.expiry_date || "",
device.expiry_date || "", ).toLocaleDateString("en-US", {
).toLocaleDateString("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">
<p className="text-muted-foreground"> Device Inactive
Device Inactive </p>
</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-muted-foreground">
<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{" "}
Payment Pending{" "} <HandCoins className="animate-pulse" size={14} />
<HandCoins className="animate-pulse" size={14} /> </span>
</span> </Link>
</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"> <p className="text-neutral-400">
{device?.reason_for_blocking} {device?.reason_for_blocking}
</p> </p>
</div> </div>
)} )}
</div> </div>
</TableCell> </TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
{device?.user?.name} {device?.user?.name}
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{device?.user?.id_card} {device?.user?.id_card}
</span> </span>
</div> </div>
</TableCell> </TableCell>
<TableCell className="font-medium">{device.mac}</TableCell> <TableCell className="font-medium">{device.mac}</TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium">
{device?.vendor} {device?.vendor}
</TableCell> </TableCell>
<TableCell> <TableCell>
{!device.has_a_pending_payment && ( {!device.has_a_pending_payment && (
<BlockDeviceDialog <BlockDeviceDialog
admin={isAdmin} admin={isAdmin}
type={device.blocked ? "unblock" : "block"} type={device.blocked ? "unblock" : "block"}
device={device} device={device}
/> />
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-muted-foreground"> <TableCell colSpan={5} className="text-muted-foreground">
{meta?.total === 1 ? ( {meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} device.</p> <p className="text-center">Total {meta?.total} device.</p>
) : ( ) : (
<p className="text-center"> <p className="text-center">
Total {meta?.total} devices. Total {meta?.total} devices.
</p> </p>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
</div> </div>
<Pagination <Pagination
totalPages={meta?.last_page} totalPages={meta?.last_page}
currentPage={meta?.current_page} currentPage={meta?.current_page}
/> />
</> </>
)} )}
</div> </div>
); );
} }

View File

@@ -2,14 +2,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,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
import Pagination from "../pagination"; import Pagination from "../pagination";
@@ -17,123 +16,122 @@ import { Badge } from "../ui/badge";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
export async function AdminTopupsTable({ export async function AdminTopupsTable({
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, true)); const [error, topups] = await tryCatch(getTopups(apiParams, true));
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 flex-col items-center justify-center my-4"> <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No topups yet.</h3> <h3>No topups yet.</h3>
</div> </div>
) : ( ) : (
<> <>
<div> <div>
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all topups.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>User</TableHead>
<TableHead>User</TableHead> <TableHead>Status</TableHead>
<TableHead>Status</TableHead> <TableHead>Amount</TableHead>
<TableHead>Amount</TableHead> <TableHead>Action</TableHead>
<TableHead>Action</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 className="flex flex-col items-start">
<div className="flex flex-col items-start"> {topup?.user?.name}
{topup?.user?.name} <span className="text-muted-foreground">
<span className="text-muted-foreground"> {topup?.user?.id_card}
{topup?.user?.id_card} </span>
</span> </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> <TableCell>
<TableCell> <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> </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> <Pagination
<Pagination totalPages={meta?.last_page}
totalPages={meta?.last_page} currentPage={meta?.current_page}
currentPage={meta?.current_page} />
/> </>
</> )}
)} </div>
</div> );
);
} }

View File

@@ -5,170 +5,168 @@ import Pagination from "@/components/pagination";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
import ClientErrorMessage from "../client-error-message"; import ClientErrorMessage from "../client-error-message";
export async function UsersPaymentsTable({ export async function UsersPaymentsTable({
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 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);
} }
} }
apiParams.limit = limit; apiParams.limit = limit;
apiParams.offset = offset; apiParams.offset = offset;
const [error, payments] = await tryCatch(getPayments(apiParams, true)); const [error, payments] = await tryCatch(getPayments(apiParams, true));
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 } = payments; const { meta, data } = payments;
// return <pre>{JSON.stringify(payments, null, 2)}</pre>; // return <pre>{JSON.stringify(payments, null, 2)}</pre>;
return ( return (
<div> <div>
{data.length === 0 ? ( {data.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No user payments yet.</h3> <h3>No user payments yet.</h3>
</div> </div>
) : ( ) : (
<> <>
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all users.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Devices paid</TableHead>
<TableHead>Devices paid</TableHead> <TableHead>User</TableHead>
<TableHead>User</TableHead> <TableHead>Amount</TableHead>
<TableHead>Amount</TableHead> <TableHead>Duration</TableHead>
<TableHead>Duration</TableHead> <TableHead>Payment Status</TableHead>
<TableHead>Payment Status</TableHead> <TableHead>Payment Method</TableHead>
<TableHead>Payment Method</TableHead> <TableHead>MIB Reference</TableHead>
<TableHead>MIB Reference</TableHead> <TableHead>Paid At</TableHead>
<TableHead>Paid At</TableHead> <TableHead>Action</TableHead>
<TableHead>Action</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody className="overflow-scroll">
<TableBody className="overflow-scroll"> {data.map((payment) => (
{data.map((payment) => ( <TableRow
<TableRow className={`${payment.paid && "title-bg dark:bg-black"}`}
className={`${payment.paid && "title-bg dark:bg-black"}`} key={payment.id}
key={payment.id} >
> <TableCell className="font-medium">
<TableCell className="font-medium"> <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> </TableCell>
</TableCell> <TableCell className="font-medium">
<TableCell className="font-medium"> {/* {payment.user.id_card} */}
{/* {payment.user.id_card} */} <div className="flex flex-col items-start">
<div className="flex flex-col items-start"> {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>{" "} </TableCell>
</TableCell> <TableCell>{payment.amount} MVR</TableCell>
<TableCell>{payment.amount} MVR</TableCell> <TableCell>{payment.number_of_months} Months</TableCell>
<TableCell>{payment.number_of_months} Months</TableCell>
<TableCell> <TableCell>
{payment.status === "PENDING" ? ( {payment.status === "PENDING" ? (
<Badge <Badge
variant="outline" variant="outline"
className="bg-yellow-100 text-black" className="bg-yellow-100 text-black"
> >
{payment.status} {payment.status}
</Badge> </Badge>
) : payment.status === "PAID" ? ( ) : payment.status === "PAID" ? (
<Badge <Badge
variant="outline" variant="outline"
className="bg-lime-100 text-black" className="bg-lime-100 text-black"
> >
{payment.status} {payment.status}
</Badge> </Badge>
) : ( ) : (
<Badge <Badge
variant="outline" variant="outline"
className="bg-red-100 text-black" className="bg-red-100 text-black"
> >
{payment.status} {payment.status}
</Badge> </Badge>
)} )}
</TableCell> </TableCell>
<TableCell>{payment.method}</TableCell> <TableCell>{payment.method}</TableCell>
<TableCell>{payment.mib_reference}</TableCell> <TableCell>{payment.mib_reference}</TableCell>
<TableCell> <TableCell>
{new Date(payment.paid_at ?? "").toLocaleDateString( {new Date(payment.paid_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",
}, },
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Link href={`/payments/${payment.id}`}> <Link href={`/payments/${payment.id}`}>
<Button>Details</Button> <Button>Details</Button>
</Link> </Link>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell colSpan={10} className="text-muted-foreground"> <TableCell colSpan={10} className="text-muted-foreground">
{meta?.total === 1 ? ( {meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} payment.</p> <p className="text-center">Total {meta?.total} payment.</p>
) : ( ) : (
<p className="text-center">Total {meta?.total} payments.</p> <p className="text-center">Total {meta?.total} payments.</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>
); );
} }

View File

@@ -2,14 +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,
TableCaption, 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";
@@ -19,107 +18,106 @@ 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">
<TableCaption>Table of all devices.</TableCaption> <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) => ( <ClickableRow
<ClickableRow admin={isAdmin}
admin={isAdmin} key={device.id}
key={device.id} device={device}
device={device} parentalControl={parentalControl}
parentalControl={parentalControl} />
/> ))}
))} </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} device.</p>
<p className="text-center">Total {meta?.total} device.</p> ) : (
) : ( <p className="text-center">
<p className="text-center"> Total {meta?.total} devices.
Total {meta?.total} devices. </p>
</p> )}
)} </TableCell>
</TableCell> </TableRow>
</TableRow> </TableFooter>
</TableFooter> </Table>
</Table> </div>
</div> <div className="sm:hidden my-4">
<div className="sm:hidden my-4"> {data?.map((device) => (
{data?.map((device) => ( <DeviceCard
<DeviceCard parentalControl={parentalControl}
parentalControl={parentalControl} key={device.id}
key={device.id} device={device}
device={device} />
/> ))}
))} </div>
</div> <Pagination
<Pagination totalPages={meta?.last_page}
totalPages={meta?.last_page} currentPage={meta?.current_page}
currentPage={meta?.current_page} />
/> </>
</> )}
)} </div>
</div> );
);
} }

View File

@@ -3,14 +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,
TableCaption, 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";
@@ -21,266 +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">
<TableCaption>Table of all devices.</TableCaption> <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 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",
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>
); );
} }

View File

@@ -3,14 +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,
TableCaption, 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";
@@ -20,200 +19,199 @@ 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">
<TableCaption>Table of all topups.</TableCaption> <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", 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",
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", : "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(topup.created_at).toLocaleDateString("en-US", { {new Date(topup.created_at).toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
})} })}
</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 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>
); );
} }

View File

@@ -1,18 +1,3 @@
// import {
// Table,
// TableBody,
// TableCaption,
// TableCell,
// TableFooter,
// TableHead,
// TableHeader,
// TableRow,
// } from "@/components/ui/table";
// import Link from "next/link";
// import Pagination from "./pagination";
// import { Badge } from "./ui/badge";
// import { Button } from "./ui/button";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getUsers } from "@/queries/users"; import { getUsers } from "@/queries/users";
@@ -22,136 +7,134 @@ import Pagination from "./pagination";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "./ui/table"; } from "./ui/table";
export async function UsersTable({ export async function UsersTable({
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, users] = await tryCatch(getUsers(apiParams)); const [error, users] = await tryCatch(getUsers(apiParams));
if (error) { if (error) {
if (error.message === "UNAUTHORIZED") { if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin"); redirect("/auth/signin");
} }
return <ClientErrorMessage message={error.message} />; return <ClientErrorMessage message={error.message} />;
} }
const { meta, data } = users; const { meta, data } = users;
// return null; // return null;
return ( return (
<div> <div>
{users?.data.length === 0 ? ( {users?.data.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No Users yet.</h3> <h3>No Users yet.</h3>
</div> </div>
) : ( ) : (
<> <>
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all users.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Name</TableHead>
<TableHead>Name</TableHead> <TableHead>ID Card</TableHead>
<TableHead>ID Card</TableHead> <TableHead>Atoll</TableHead>
<TableHead>Atoll</TableHead> <TableHead>Island</TableHead>
<TableHead>Island</TableHead> <TableHead>House Name</TableHead>
<TableHead>House Name</TableHead> <TableHead>Status</TableHead>
<TableHead>Status</TableHead> <TableHead>Dob</TableHead>
<TableHead>Dob</TableHead> <TableHead>Phone Number</TableHead>
<TableHead>Phone Number</TableHead> <TableHead>Action</TableHead>
<TableHead>Action</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody className="overflow-scroll">
<TableBody className="overflow-scroll"> {data.map((user) => (
{data.map((user) => ( <TableRow
<TableRow className={`${user.verified && "title-bg dark:bg-black"}`}
className={`${user.verified && "title-bg dark:bg-black"}`} key={user.id}
key={user.id} >
> <TableCell className="font-medium">
<TableCell className="font-medium"> {user.first_name} {user.last_name}
{user.first_name} {user.last_name} </TableCell>
</TableCell> <TableCell className="font-medium">{user.id_card}</TableCell>
<TableCell className="font-medium">{user.id_card}</TableCell> <TableCell>{user.atoll?.name}</TableCell>
<TableCell>{user.atoll?.name}</TableCell> <TableCell>{user.island?.name}</TableCell>
<TableCell>{user.island?.name}</TableCell> <TableCell>{user.address}</TableCell>
<TableCell>{user.address}</TableCell>
<TableCell> <TableCell>
{user.verified ? ( {user.verified ? (
<Badge <Badge
variant="outline" variant="outline"
className="bg-lime-100 text-black" className="bg-lime-100 text-black"
> >
Verified Verified
</Badge> </Badge>
) : ( ) : (
<Badge <Badge
variant="outline" variant="outline"
className="bg-yellow-100 text-black" className="bg-yellow-100 text-black"
> >
Unverified Unverified
</Badge> </Badge>
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
{new Date(user.dob ?? "").toLocaleDateString("en-US", { {new Date(user.dob ?? "").toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
})} })}
</TableCell> </TableCell>
<TableCell>{user.mobile}</TableCell> <TableCell>{user.mobile}</TableCell>
<TableCell> <TableCell>
<Link href={`/users/${user.id}/details`}> <Link href={`/users/${user.id}/details`}>
<Button>Details</Button> <Button>Details</Button>
</Link> </Link>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell colSpan={9}> <TableCell colSpan={9}>
{meta?.total === 1 ? ( {meta?.total === 1 ? (
<p className="text-center">Total {meta?.total} user.</p> <p className="text-center">Total {meta?.total} user.</p>
) : ( ) : (
<p className="text-center">Total {meta?.total} users.</p> <p className="text-center">Total {meta?.total} users.</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>
); );
} }

View File

@@ -2,14 +2,13 @@ import { Calendar } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCell,
TableCell, TableFooter,
TableFooter, TableHead,
TableHead, TableHeader,
TableHeader, TableRow,
TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { WalletTransaction } from "@/lib/backend-types"; import type { WalletTransaction } from "@/lib/backend-types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -20,224 +19,223 @@ import { Badge } from "./ui/badge";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
export async function WalletTransactionsTable({ export async function WalletTransactionsTable({
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, transactions] = await tryCatch( const [error, transactions] = await tryCatch(
getWaleltTransactions(apiParams), getWaleltTransactions(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 } = transactions; const { data, meta } = transactions;
const totalDebit = data.reduce( const totalDebit = data.reduce(
(acc, trx) => acc + (trx.transaction_type === "DEBIT" ? trx.amount : 0), (acc, trx) => acc + (trx.transaction_type === "DEBIT" ? trx.amount : 0),
0, 0,
); );
const totalCredit = data.reduce( const totalCredit = data.reduce(
(acc, trx) => acc + (trx.transaction_type === "TOPUP" ? trx.amount : 0), (acc, trx) => acc + (trx.transaction_type === "TOPUP" ? trx.amount : 0),
0, 0,
); );
return ( return (
<div> <div>
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No transactions yet.</h3> <h3>No transactions yet.</h3>
</div> </div>
) : ( ) : (
<div> <div>
<div className="flex gap-4 mb-4 w-full"> <div className="flex gap-4 mb-4 w-full">
<div className="bg-red-400 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-400 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">Total Debit</h5> <h5 className="text-lg font-semibold">Total Debit</h5>
<p>{totalDebit.toFixed(2)} MVR</p> <p>{totalDebit.toFixed(2)} MVR</p>
</div> </div>
<div className="bg-green-400 w-full sm:w-fit dark:bg-green-950 dark:text-green-400 text-green-900 p-2 px-4 rounded-md mb-2"> <div className="bg-green-400 w-full sm:w-fit dark:bg-green-950 dark:text-green-400 text-green-900 p-2 px-4 rounded-md mb-2">
<h5 className="text-lg font-semibold">Total Credit</h5> <h5 className="text-lg font-semibold">Total Credit</h5>
<p>{totalCredit.toFixed(2)} MVR</p> <p>{totalCredit.toFixed(2)} MVR</p>
</div> </div>
</div> </div>
<div className="hidden sm:block"> <div className="hidden sm:block">
<Table className="overflow-scroll"> <Table className="overflow-scroll">
<TableCaption>Table of all transactions.</TableCaption> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Description</TableHead>
<TableHead>Description</TableHead> <TableHead>Amount</TableHead>
<TableHead>Amount</TableHead> <TableHead>Transaction Type</TableHead>
<TableHead>Transaction Type</TableHead> <TableHead>View Details</TableHead>
<TableHead>View Details</TableHead> <TableHead>Created at</TableHead>
<TableHead>Created at</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody className="overflow-scroll">
<TableBody className="overflow-scroll"> {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", trx?.transaction_type === "TOPUP"
trx?.transaction_type === "TOPUP" ? "credit-bg"
? "credit-bg" : "debit-bg",
: "debit-bg", )}
)} key={trx.id}
key={trx.id} >
> <TableCell>
<TableCell> <span className="text-muted-foreground">
<span className="text-muted-foreground"> {trx.description}
{trx.description} </span>
</span> </TableCell>
</TableCell> <TableCell>{trx.amount.toFixed(2)} MVR</TableCell>
<TableCell>{trx.amount.toFixed(2)} MVR</TableCell>
<TableCell> <TableCell>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{trx.transaction_type === "TOPUP" ? ( {trx.transaction_type === "TOPUP" ? (
<Badge className="bg-green-100 text-green-950 dark:bg-green-700"> <Badge className="bg-green-100 text-green-950 dark:bg-green-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
) : ( ) : (
<Badge className="bg-red-500 text-red-950 dark:bg-red-700"> <Badge className="bg-red-500 text-red-950 dark:bg-red-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
)} )}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className=""> <span className="">
{new Date(trx.created_at).toLocaleDateString("en-US", { {new Date(trx.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",
})} })}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button> <Button>
<Link <Link
className="font-medium " className="font-medium "
href={ href={
trx.transaction_type === "TOPUP" trx.transaction_type === "TOPUP"
? `/top-ups/${trx.reference_id}` ? `/top-ups/${trx.reference_id}`
: `/payments/${trx.reference_id}` : `/payments/${trx.reference_id}`
} }
> >
View Details View Details
</Link> </Link>
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-muted-foreground"> <TableCell colSpan={5} className="text-muted-foreground">
{meta?.total === 1 ? ( {meta?.total === 1 ? (
<p className="text-center"> <p className="text-center">
Total {meta?.total} transaction. Total {meta?.total} transaction.
</p> </p>
) : ( ) : (
<p className="text-center"> <p className="text-center">
Total {meta?.total} transactions. Total {meta?.total} transactions.
</p> </p>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
</div> </div>
<div className="sm:hidden block"> <div className="sm:hidden block">
{data.map((trx) => ( {data.map((trx) => (
<MobileTransactionDetails key={trx.id} trx={trx} /> <MobileTransactionDetails key={trx.id} trx={trx} />
))} ))}
</div> </div>
<Pagination <Pagination
totalPages={meta?.last_page} totalPages={meta?.last_page}
currentPage={meta?.current_page} currentPage={meta?.current_page}
/> />
</div> </div>
)} )}
</div> </div>
); );
} }
function MobileTransactionDetails({ trx }: { trx: WalletTransaction }) { function MobileTransactionDetails({ trx }: { trx: WalletTransaction }) {
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",
trx?.transaction_type === "TOPUP" ? "credit-bg" : "debit-bg", trx?.transaction_type === "TOPUP" ? "credit-bg" : "debit-bg",
)} )}
> >
<div className="bg-white shadow dark:bg-black p-2 rounded w-full"> <div className="bg-white shadow dark:bg-black p-2 rounded w-full">
<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(trx.created_at).toLocaleDateString("en-US", { {new Date(trx.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",
})} })}
</span> </span>
</div> </div>
<p className="text-sm text-muted-foreground py-4">{trx.description}</p> <p className="text-sm text-muted-foreground py-4">{trx.description}</p>
</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">
{trx.amount.toFixed(2)} MVR {trx.amount.toFixed(2)} MVR
</span> </span>
</div> </div>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{trx.transaction_type === "TOPUP" ? ( {trx.transaction_type === "TOPUP" ? (
<Badge className="bg-green-100 text-green-950 dark:bg-green-700"> <Badge className="bg-green-100 text-green-950 dark:bg-green-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
) : ( ) : (
<Badge className="bg-red-500 text-red-950 dark:bg-red-700"> <Badge className="bg-red-500 text-red-950 dark:bg-red-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
)} )}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 mt-2 w-full"> <div className="flex items-center gap-2 mt-2 w-full">
<Link <Link
className="font-medium hover:underline" className="font-medium hover:underline"
href={ href={
trx.transaction_type === "TOPUP" trx.transaction_type === "TOPUP"
? `/top-ups/${trx.reference_id}` ? `/top-ups/${trx.reference_id}`
: `/payments/${trx.reference_id}` : `/payments/${trx.reference_id}`
} }
> >
<Button size={"sm"} className="w-full"> <Button size={"sm"} className="w-full">
View Details View Details
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }