mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-10-05 09:55:25 +00:00
style: add skeletons to paymentId and deviceId pages ♻️
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 10m58s
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 10m58s
This commit is contained in:
33
app/(dashboard)/devices/[deviceId]/loading.tsx
Normal file
33
app/(dashboard)/devices/[deviceId]/loading.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
export default function DeviceLoading() {
|
||||||
|
return (
|
||||||
|
<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 space-y-2 justify-between items-start">
|
||||||
|
<Skeleton className="h-8 w-44" />
|
||||||
|
<Skeleton className="h-4 w-40" />
|
||||||
|
<Skeleton className="h-4 w-48" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-col">
|
||||||
|
<Skeleton className="h-10 w-32" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="user-filters"
|
||||||
|
className=" py-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||||
|
>
|
||||||
|
<Skeleton className="h-10 sm:w-64" />
|
||||||
|
{/* <Filter
|
||||||
|
options={sortfilterOptions}
|
||||||
|
defaultOption="asc"
|
||||||
|
queryParamKey="sortBy"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
{/* <Suspense key={query} fallback={"loading...."}>
|
||||||
|
<DevicesTable searchParams={searchParams} />
|
||||||
|
</Suspense> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,68 +1,68 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
import ClientErrorMessage from "@/components/client-error-message";
|
import ClientErrorMessage from "@/components/client-error-message";
|
||||||
import Search from "@/components/search";
|
import Search from "@/components/search";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { getDevice } from "@/queries/devices";
|
import { getDevice } from "@/queries/devices";
|
||||||
import { tryCatch } from "@/utils/tryCatch";
|
import { tryCatch } from "@/utils/tryCatch";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
export default async function DeviceDetails({
|
export default async function DeviceDetails({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ deviceId: string }>;
|
params: Promise<{ deviceId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const deviceId = (await params)?.deviceId;
|
const deviceId = (await params)?.deviceId;
|
||||||
const [error, device] = await tryCatch(getDevice({ deviceId: deviceId }));
|
const [error, device] = await tryCatch(getDevice({ deviceId: deviceId }));
|
||||||
if (error) {
|
if (error) {
|
||||||
// Handle specific actions for certain errors, but reuse the error message
|
// Handle specific actions for certain errors, but reuse the error message
|
||||||
if (error.message === "UNAUTHORIZED") {
|
if (error.message === "UNAUTHORIZED") {
|
||||||
redirect("/auth/signin");
|
redirect("/auth/signin");
|
||||||
} else {
|
} else {
|
||||||
// For all other errors, display the error message directly
|
// For all other errors, display the error message directly
|
||||||
return <ClientErrorMessage message={error.message} />;
|
return <ClientErrorMessage message={error.message} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!device) return null;
|
if (!device) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between title-bg title-bg ring-2 ring-sarLinkOrange/50 rounded-lg p-2">
|
<div className="flex items-center justify-between title-bg title-bg ring-2 ring-sarLinkOrange/50 rounded-lg p-4">
|
||||||
<div className="flex flex-col justify-between items-start">
|
<div className="flex flex-col justify-between items-start">
|
||||||
<h3 className="text-2xl text-sarLinkOrange font-bold">
|
<h3 className="text-2xl text-sarLinkOrange font-bold">
|
||||||
{device?.name}
|
{device?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant={"secondary"}>{device?.mac}</Badge>
|
<Badge variant={"secondary"}>{device?.mac}</Badge>
|
||||||
<p className="text-muted-foreground text-sm mt-2">
|
<p className="text-muted-foreground text-sm mt-2">
|
||||||
Device active until{" "}
|
Device active until{" "}
|
||||||
{new Date(device?.expiry_date || "").toLocaleDateString("en-US", {
|
{new Date(device?.expiry_date || "").toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-col">
|
<div className="flex items-center gap-2 flex-col">
|
||||||
{device?.expiry_date && new Date() < new Date(device.expiry_date) && (
|
{device?.expiry_date && new Date() < new Date(device.expiry_date) && (
|
||||||
<p className="text-base font-semibold font-mono w-full text-center px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400">
|
<p className="text-base font-semibold font-mono w-full text-center px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400">
|
||||||
ACTIVE
|
ACTIVE
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="user-filters"
|
id="user-filters"
|
||||||
className=" py-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
className=" py-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||||
>
|
>
|
||||||
<Search />
|
<Search />
|
||||||
{/* <Filter
|
{/* <Filter
|
||||||
options={sortfilterOptions}
|
options={sortfilterOptions}
|
||||||
defaultOption="asc"
|
defaultOption="asc"
|
||||||
queryParamKey="sortBy"
|
queryParamKey="sortBy"
|
||||||
/> */}
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
{/* <Suspense key={query} fallback={"loading...."}>
|
{/* <Suspense key={query} fallback={"loading...."}>
|
||||||
<DevicesTable searchParams={searchParams} />
|
<DevicesTable searchParams={searchParams} />
|
||||||
</Suspense> */}
|
</Suspense> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -2,21 +2,24 @@ import DevicesTableSkeleton from "@/components/device-table-skeleton";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export default function LoadingComponent() {
|
export default function LoadingComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
|
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
|
||||||
<Skeleton className="w-48 h-8" />
|
<Skeleton className="w-48 h-8" />
|
||||||
<Skeleton className="w-20 h-8" />
|
<Skeleton className="w-36 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div>
|
||||||
id="user-filters"
|
<Skeleton className="w-full rounded-md mt-5 mb-6 sm:w-48 h-9" />
|
||||||
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
|
</div>
|
||||||
>
|
<div
|
||||||
<DevicesTableSkeleton
|
id="user-filters"
|
||||||
headers={["Device Name", "Mac Address", "Vendor", "#"]}
|
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
|
||||||
length={10}
|
>
|
||||||
/>
|
<DevicesTableSkeleton
|
||||||
</div>
|
headers={["Device Name", "Mac Address", "Vendor", "#"]}
|
||||||
</div>
|
length={10}
|
||||||
);
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
63
app/(dashboard)/payments/[paymentId]/loading.tsx
Normal file
63
app/(dashboard)/payments/[paymentId]/loading.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
export default function PaymentLoading() {
|
||||||
|
return (
|
||||||
|
<div className="mx-2">
|
||||||
|
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4">
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 mt-7 w-full border rounded-md border-dashed title-bg py-3 px-2 mb-3">
|
||||||
|
<Skeleton className="h-5 w-36" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
{Array.from({ length: 1 }).map((_, i) => (
|
||||||
|
<Skeleton
|
||||||
|
key={`${i + 1}`}
|
||||||
|
className="w-full border border-gray-300 h-13 rounded-sm"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="pb-4 w-full gap-4 flex sm:flex-row flex-col items-start justify-start">
|
||||||
|
<div className="my-1 w-full flex items-center justify-between p-2 text-sm text-foreground border rounded">
|
||||||
|
<Table>
|
||||||
|
<TableBody className="">
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Payment created</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Skeleton className="h-5 inline-block w-24" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Total Devices</TableCell>
|
||||||
|
<TableCell className="text-right text-xl">
|
||||||
|
<Skeleton className="h-5 w-24 inline-block" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Duration</TableCell>
|
||||||
|
<TableCell className="text-right text-xl">
|
||||||
|
<Skeleton className="h-5 w-24 inline-block" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow className="">
|
||||||
|
<TableCell colSpan={1}>Total Due</TableCell>
|
||||||
|
<TableCell className="text-right text-3xl font-bold">
|
||||||
|
<Skeleton className="h-5 w-24 inline-block" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -10,77 +10,77 @@ import { TextShimmer } from "@/components/ui/text-shimmer";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { tryCatch } from "@/utils/tryCatch";
|
import { tryCatch } from "@/utils/tryCatch";
|
||||||
export default async function PaymentPage({
|
export default async function PaymentPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ paymentId: string }>;
|
params: Promise<{ paymentId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const paymentId = (await params).paymentId;
|
const paymentId = (await params).paymentId;
|
||||||
const [error, payment] = await tryCatch(getPayment({ id: paymentId }));
|
const [error, payment] = await tryCatch(getPayment({ id: paymentId }));
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error.message === "Invalid token.") redirect("/auth/signin");
|
if (error.message === "Invalid token.") redirect("/auth/signin");
|
||||||
return <ClientErrorMessage message={error.message} />;
|
return <ClientErrorMessage message={error.message} />;
|
||||||
}
|
}
|
||||||
const [userError, userProfile] = await tryCatch(getProfile());
|
const [userError, userProfile] = await tryCatch(getProfile());
|
||||||
if (userError) {
|
if (userError) {
|
||||||
if (userError.message === "Invalid token.") redirect("/auth/signin");
|
if (userError.message === "Invalid token.") redirect("/auth/signin");
|
||||||
return <ClientErrorMessage message={userError.message} />;
|
return <ClientErrorMessage message={userError.message} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2">
|
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2">
|
||||||
<h3 className="text-sarLinkOrange text-2xl">Payment</h3>
|
<h3 className="text-sarLinkOrange text-2xl">Payment</h3>
|
||||||
<div className="flex flex-col gap-4 items-end w-full">
|
<div className="flex flex-col gap-4 items-end w-full">
|
||||||
{!payment.is_expired &&
|
{!payment.is_expired &&
|
||||||
payment.paid &&
|
payment.paid &&
|
||||||
payment.status !== "PENDING" && (
|
payment.status !== "PENDING" && (
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md opacity-100! uppercase font-semibold",
|
"rounded-md opacity-100! uppercase font-semibold",
|
||||||
payment?.paid
|
payment?.paid
|
||||||
? "text-green-900 bg-green-500/20"
|
? "text-green-900 bg-green-500/20"
|
||||||
: "text-inherit bg-yellow-400",
|
: "text-inherit bg-yellow-400",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{payment.status}
|
{payment.status}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{payment.status === "PENDING" && !payment.is_expired && (
|
{payment.status === "PENDING" && !payment.is_expired && (
|
||||||
<Button>
|
<Button>
|
||||||
<TextShimmer>Payment Pending</TextShimmer>{" "}
|
<TextShimmer>Payment Pending</TextShimmer>{" "}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!payment.paid &&
|
{!payment.paid &&
|
||||||
(payment.is_expired ? (
|
(payment.is_expired ? (
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
|
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
|
||||||
>
|
>
|
||||||
Payment Expired
|
Payment Expired
|
||||||
</Button>
|
</Button>
|
||||||
) : payment.status === "PENDING" ? (
|
) : payment.status === "PENDING" ? (
|
||||||
<CancelPaymentButton paymentId={paymentId} />
|
<CancelPaymentButton paymentId={paymentId} />
|
||||||
) : payment.status === "CANCELLED" ? (
|
) : payment.status === "CANCELLED" ? (
|
||||||
<Button disabled>Payment Cancelled</Button>
|
<Button disabled>Payment Cancelled</Button>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!payment.paid && (
|
{!payment.paid && (
|
||||||
<ExpiryCountDown expiryLabel="Payment" expiresAt={payment.expires_at} />
|
<ExpiryCountDown expiryLabel="Payment" expiresAt={payment.expires_at} />
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
id="user-device-payments"
|
id="user-device-payments"
|
||||||
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||||
>
|
>
|
||||||
<DevicesToPay
|
<DevicesToPay
|
||||||
disabled={payment.paid || payment.is_expired}
|
disabled={payment.paid || payment.is_expired}
|
||||||
user={userProfile || undefined}
|
user={userProfile || undefined}
|
||||||
payment={payment || undefined}
|
payment={payment || undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -2,21 +2,24 @@ import DevicesTableSkeleton from "@/components/device-table-skeleton";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export default function LoadingComponent() {
|
export default function LoadingComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
|
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
|
||||||
<Skeleton className="w-48 h-8" />
|
<Skeleton className="w-48 h-8" />
|
||||||
<Skeleton className="w-20 h-8" />
|
<Skeleton className="w-20 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div>
|
||||||
id="user-filters"
|
<Skeleton className="w-full rounded-md mt-5 mb-6 sm:w-48 h-9" />
|
||||||
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
|
</div>
|
||||||
>
|
<div
|
||||||
<DevicesTableSkeleton
|
id="user-filters"
|
||||||
headers={["Details", "Duration", "Status", "Amount"]}
|
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
|
||||||
length={10}
|
>
|
||||||
/>
|
<DevicesTableSkeleton
|
||||||
</div>
|
headers={["Details", "Duration", "Status", "Amount"]}
|
||||||
</div>
|
length={10}
|
||||||
);
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -78,11 +78,13 @@ export default function DevicesToPay({
|
|||||||
{devices?.map((device) => (
|
{devices?.map((device) => (
|
||||||
<div
|
<div
|
||||||
key={device.id}
|
key={device.id}
|
||||||
className="bg-muted border rounded p-2 flex gap-2 items-center"
|
className="bg-muted border rounded p-2 flex gap-2 items-center motion-preset-fade"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-sm font-medium">{device.name}</div>
|
<div className="text-sm font-medium motion-preset-slide-up">
|
||||||
<div className="text-xs text-muted-foreground">
|
{device.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground motion-preset-slide-up motion-delay-100">
|
||||||
{device.mac}
|
{device.mac}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,8 +162,10 @@ export default function DevicesToPay({
|
|||||||
</TableCaption>
|
</TableCaption>
|
||||||
<TableBody className="">
|
<TableBody className="">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Payment created</TableCell>
|
<TableCell className="motion-preset-slide-left-sm">
|
||||||
<TableCell className="text-right">
|
Payment created
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right motion-preset-slide-right-sm">
|
||||||
{new Date(payment?.created_at ?? "").toLocaleDateString(
|
{new Date(payment?.created_at ?? "").toLocaleDateString(
|
||||||
"en-US",
|
"en-US",
|
||||||
{
|
{
|
||||||
@@ -176,22 +180,31 @@ export default function DevicesToPay({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Total Devices</TableCell>
|
<TableCell className="motion-preset-slide-left-sm motion-delay-75">
|
||||||
<TableCell className="text-right text-xl">
|
Total Devices
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-75">
|
||||||
{devices?.length}
|
{devices?.length}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Duration</TableCell>
|
<TableCell className="motion-preset-slide-left-sm motion-delay-100">
|
||||||
<TableCell className="text-right text-xl">
|
Duration
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-100">
|
||||||
{payment?.number_of_months} Months
|
{payment?.number_of_months} Months
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter>
|
||||||
<TableRow className="">
|
<TableRow className="">
|
||||||
<TableCell colSpan={1}>Total Due</TableCell>
|
<TableCell
|
||||||
<TableCell className="text-right text-3xl font-bold">
|
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">
|
||||||
{payment?.amount?.toFixed(2)}
|
{payment?.amount?.toFixed(2)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@@ -2,13 +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,
|
||||||
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";
|
||||||
@@ -19,223 +19,227 @@ 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-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">Total Debit</h5>
|
<h5 className="text-lg font-semibold uppercase font-barlow">
|
||||||
<p>{totalDebit.toFixed(2)} MVR</p>
|
Total Debit
|
||||||
</div>
|
</h5>
|
||||||
<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">
|
<p>{totalDebit.toFixed(2)} MVR</p>
|
||||||
<h5 className="text-lg font-semibold">Total Credit</h5>
|
</div>
|
||||||
<p>{totalCredit.toFixed(2)} MVR</p>
|
<div className="bg-green-300 ring-4 ring-green-500/20 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>
|
<h5 className="text-lg font-semibold uppercase font-barlow">
|
||||||
</div>
|
Total Credit
|
||||||
<div className="hidden sm:block">
|
</h5>
|
||||||
<Table className="overflow-scroll">
|
<p>{totalCredit.toFixed(2)} MVR</p>
|
||||||
<TableHeader>
|
</div>
|
||||||
<TableRow>
|
</div>
|
||||||
<TableHead>Description</TableHead>
|
<div className="hidden sm:block">
|
||||||
<TableHead>Amount</TableHead>
|
<Table className="overflow-scroll">
|
||||||
<TableHead>Transaction Type</TableHead>
|
<TableHeader>
|
||||||
<TableHead>View Details</TableHead>
|
<TableRow>
|
||||||
<TableHead>Created at</TableHead>
|
<TableHead>Description</TableHead>
|
||||||
</TableRow>
|
<TableHead>Amount</TableHead>
|
||||||
</TableHeader>
|
<TableHead>Transaction Type</TableHead>
|
||||||
<TableBody className="overflow-scroll">
|
<TableHead>View Details</TableHead>
|
||||||
{transactions?.data?.map((trx) => (
|
<TableHead>Created at</TableHead>
|
||||||
<TableRow
|
</TableRow>
|
||||||
className={cn(
|
</TableHeader>
|
||||||
"items-start border rounded p-2",
|
<TableBody className="overflow-scroll">
|
||||||
trx?.transaction_type === "TOPUP"
|
{transactions?.data?.map((trx) => (
|
||||||
? "credit-bg"
|
<TableRow
|
||||||
: "debit-bg",
|
className={cn(
|
||||||
)}
|
"items-start border rounded p-2",
|
||||||
key={trx.id}
|
trx?.transaction_type === "TOPUP"
|
||||||
>
|
? "credit-bg"
|
||||||
<TableCell>
|
: "debit-bg",
|
||||||
<span className="text-muted-foreground">
|
)}
|
||||||
{trx.description}
|
key={trx.id}
|
||||||
</span>
|
>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>{trx.amount.toFixed(2)} MVR</TableCell>
|
<span className="text-muted-foreground">
|
||||||
|
{trx.description}
|
||||||
|
</span>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user