diff --git a/actions/payment.ts b/actions/payment.ts index 4c140aa..c1af340 100644 --- a/actions/payment.ts +++ b/actions/payment.ts @@ -24,7 +24,6 @@ export async function createPayment(data: NewPayment) { body: JSON.stringify(data), }, ); - if (!response.ok) { const errorData = await response.json(); // Throw an error with the message from the API @@ -32,7 +31,7 @@ export async function createPayment(data: NewPayment) { } const payment = (await response.json()) as Payment; revalidatePath("/devices"); - redirect(`/payments/${payment.id}`); + return payment; } export async function getPayment({ id }: { id: string }) { @@ -48,8 +47,13 @@ export async function getPayment({ id }: { id: string }) { }, ); + if (response.status === 404) { + throw new Error("Payment not found"); + } + if (!response.ok) { const errorData = await response.json(); + console.log(errorData); // Throw an error with the message from the API throw new Error(errorData.message || "Something went wrong."); } @@ -59,7 +63,7 @@ export async function getPayment({ id }: { id: string }) { export async function getPayments() { const session = await getServerSession(authOptions); - const respose = await fetch( + const response = await fetch( `${process.env.SARLINK_API_BASE_URL}/api/billing/payment/`, { method: "GET", @@ -69,10 +73,35 @@ export async function getPayments() { }, }, ); - const data = (await respose.json()) as ApiResponse; + console.log("response statys", response.status); + if (response.status === 401) { + // Redirect to the signin page if the user is unauthorized + throw new Error("Unauthorized; redirect to /auth/signin"); + } + const data = (await response.json()) as ApiResponse; return data; } +export async function cancelPayment({ id }: { id: string }) { + const session = await getServerSession(authOptions); + const response = await fetch( + `${process.env.SARLINK_API_BASE_URL}/api/billing/payment/${id}/delete/`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Token ${session?.apiToken}`, + }, + }, + ); + if (response.status === 401) { + // Redirect to the signin page if the user is unauthorized + throw new Error("Unauthorized; redirect to /auth/signin"); + } + // Since the response is 204 No Content, there's no JSON to parse + return { message: "Payment successfully canceled." }; +} + type UpdatePayment = Pick< Payment, "id" | "paid" | "paid_at" | "method" | "number_of_months" diff --git a/app/(dashboard)/payments/[paymentId]/page.tsx b/app/(dashboard)/payments/[paymentId]/page.tsx index b77fc34..a624d42 100644 --- a/app/(dashboard)/payments/[paymentId]/page.tsx +++ b/app/(dashboard)/payments/[paymentId]/page.tsx @@ -1,11 +1,12 @@ import { getPayment } from "@/actions/payment"; import { authOptions } from "@/app/auth"; +import CancelPaymentButton from "@/components/billing/cancel-payment-button"; import DevicesToPay from "@/components/devices-to-pay"; +import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { tryCatch } from "@/utils/tryCatch"; +import { Trash2 } from "lucide-react"; import { getServerSession } from "next-auth"; -import { headers } from "next/headers"; -import React from "react"; export default async function PaymentPage({ params, }: { @@ -16,22 +17,25 @@ export default async function PaymentPage({ const paymentId = (await params).paymentId; const [error, payment] = await tryCatch(getPayment({ id: paymentId })); if (error) { - return
Error getting payment: {error.message}
; + return Error getting payment: {error.message}; } return (
-
+

Payment

- - {payment?.paid ? "Paid" : "Pending"} - +
+ + {payment?.paid ? "Paid" : "Pending"} + + +
; + searchParams: Promise<{ + query: string; + page: number; + sortBy: string; + status: string; + }>; }) { - const query = (await searchParams)?.query || ""; - return ( -
-
-

- My Payments -

-
+ const query = (await searchParams)?.query || ""; + return ( +
+
+

My Payments

+
-
- -
- - - -
- ); +
+ +
+ + + +
+ ); } diff --git a/components/add-devices-to-cart-button.tsx b/components/add-devices-to-cart-button.tsx index 782acdd..38e367f 100644 --- a/components/add-devices-to-cart-button.tsx +++ b/components/add-devices-to-cart-button.tsx @@ -10,6 +10,9 @@ export default function AddDevicesToCartButton({ device }: { device: Device }) { const isChecked = devices.some((d) => d.id === device.id); + if (device.has_a_pending_payment || device.is_active) { + return null; + } return ( { + setLoading(true); + const [error, _] = await tryCatch(cancelPayment({ id: paymentId })); + if (error) { + toast.error(error.message); + setLoading(false); + } else { + router.replace("/devices"); + } + }} + disabled={loading} + variant={"destructive"} + > + Cancel Payment + {loading ? : } + + ); +} diff --git a/components/clickable-row.tsx b/components/clickable-row.tsx index ab40a77..9c732b2 100644 --- a/components/clickable-row.tsx +++ b/components/clickable-row.tsx @@ -5,6 +5,7 @@ import type { Device } from "@/lib/backend-types"; import { cn } from "@/lib/utils"; import { pl } from "date-fns/locale"; import { useAtom } from "jotai"; +import { Hourglass } from "lucide-react"; import Link from "next/link"; import AddDevicesToCartButton from "./add-devices-to-cart-button"; import BlockDeviceDialog from "./block-device-dialog"; @@ -20,12 +21,13 @@ export default function ClickableRow({ key={device.id} className={cn( (parentalControl === false && device.blocked) || device.is_active - ? "cursor-not-allowed title-bg" + ? "cursor-not-allowed bg-accent-foreground/10 hover:bg-accent-foreground/10" : "cursor-pointer hover:bg-muted", )} onClick={() => { if (device.blocked) return; if (device.is_active === true) return; + if (device.has_a_pending_payment === true) return; if (parentalControl === true) return; setDeviceCart((prev) => devices.some((d) => d.id === device.id) @@ -47,22 +49,34 @@ export default function ClickableRow({ {device.name} {device.is_active ? ( - +
Active until{" "} - {new Date(device.expiry_date || "").toLocaleDateString("en-US", { - month: "short", - day: "2-digit", - year: "numeric", - })} - + + {new Date(device.expiry_date || "").toLocaleDateString( + "en-US", + { + month: "short", + day: "2-digit", + year: "numeric", + }, + )} + +
) : (

Device Inactive

)} + {device.has_a_pending_payment && ( + + + Payment Pending + + + )} {device.blocked_by === "ADMIN" && device.blocked && ( -
- Comment: -

{device?.reason_for_blocking}

+
+ Comment +

{device?.reason_for_blocking}

)}
diff --git a/components/device-card.tsx b/components/device-card.tsx index 385c656..f28b0d7 100644 --- a/components/device-card.tsx +++ b/components/device-card.tsx @@ -1,76 +1,104 @@ -'use client' -import { deviceCartAtom } from '@/lib/atoms' -import { cn } from '@/lib/utils' -import type { Device } from '@prisma/client' -import { useAtom } from 'jotai' -import Link from 'next/link' -import AddDevicesToCartButton from './add-devices-to-cart-button' -import BlockDeviceDialog from './block-device-dialog' -import { Badge } from './ui/badge' +"use client"; +import { deviceCartAtom } from "@/lib/atoms"; +import type { Device } from "@/lib/backend-types"; +import { cn } from "@/lib/utils"; +import { useAtom } from "jotai"; +import { Hourglass } from "lucide-react"; +import Link from "next/link"; +import AddDevicesToCartButton from "./add-devices-to-cart-button"; +import BlockDeviceDialog from "./block-device-dialog"; +import { Badge } from "./ui/badge"; -export default function DeviceCard({ device, parentalControl }: { device: Device, parentalControl?: boolean }) { - const [devices, setDeviceCart] = useAtom(deviceCartAtom) +export default function DeviceCard({ + device, + parentalControl, +}: { device: Device; parentalControl?: boolean }) { + const [devices, setDeviceCart] = useAtom(deviceCartAtom); - const isChecked = devices.some((d) => d.id === device.id); + const isChecked = devices.some((d) => d.id === device.id); - return ( -
{ }} - onClick={() => { - if (parentalControl === true) return - setDeviceCart((prev) => - devices.some((d) => d.id === device.id) - ? prev.filter((d) => d.id !== device.id) - : [...prev, device] - ) - } - } - className="w-full"> -
-
-
- - {device.name} - - - - {device.mac} - - -
+ return ( +
{}} + onClick={() => { + if (device.blocked) return; + if (device.is_active === true) return; + if (device.has_a_pending_payment === true) return; + if (parentalControl === true) return; + setDeviceCart((prev) => + devices.some((d) => d.id === device.id) + ? prev.filter((d) => d.id !== device.id) + : [...prev, device], + ); + }} + className="w-full" + > +
+
+
+ + {device.name} + + + {device.mac} + +
- {device.isActive && ( - - Active until{" "} - {new Date().toLocaleDateString("en-US", { - month: "short", - day: "2-digit", - year: "numeric", - })} - - )} + {device.is_active ? ( +
+ Active until{" "} + + {new Date(device.expiry_date || "").toLocaleDateString( + "en-US", + { + month: "short", + day: "2-digit", + year: "numeric", + }, + )} + +
+ ) : ( +

Device Inactive

+ )} + {device.has_a_pending_payment && ( + + + Payment Pending + + + )} - {(device.blocked && device.blockedBy === "ADMIN") && ( -
- Blocked by admin -

- {device?.reasonForBlocking} -

-
- )} - -
-
- {!parentalControl ? ( - - ) : ( - - )} -
-
-
- ) + {device.blocked && device.blocked_by === "ADMIN" && ( +
+ Blocked by admin +

{device?.reason_for_blocking}

+
+ )} +
+
+ {!parentalControl ? ( + + ) : ( + + )} +
+
+
+ ); } diff --git a/components/devices-for-payment.tsx b/components/devices-for-payment.tsx index 36d9890..65ad832 100644 --- a/components/devices-for-payment.tsx +++ b/components/devices-for-payment.tsx @@ -10,7 +10,7 @@ import { tryCatch } from "@/utils/tryCatch"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { CircleDollarSign, Loader2 } from "lucide-react"; import { useSession } from "next-auth/react"; -import { usePathname } from "next/navigation"; +import { redirect, usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; export default function DevicesForPayment() { @@ -35,7 +35,7 @@ export default function DevicesForPayment() { setTotal(baseAmount + (devices.length + 1 - 1) * discountPercentage); }, [months, devices.length]); - if (pathname === "/payment") { + if (pathname === "/payments") { return null; } @@ -45,6 +45,9 @@ export default function DevicesForPayment() { device_ids: devices.map((device) => device.id), }; + if (disabled) { + return "Please wait..."; + } return (
@@ -69,15 +72,15 @@ export default function DevicesForPayment() {