202 lines
7.1 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
import { ErrorState } from "@/components/atoms/error-state";
import { CheckCircleIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
import { PageLayout } from "@/components/templates/PageLayout";
import { logger } from "@customer-portal/logging";
import { apiClient, getDataOrThrow } from "@/lib/api";
import { openSsoLink } from "@/features/billing/utils/sso";
import { useInvoice, useCreateInvoiceSsoLink } from "@/features/billing/hooks";
import type { InvoiceSsoLink } from "@customer-portal/domain/billing";
import {
InvoiceItems,
InvoiceTotals,
InvoiceSummaryBar,
} from "@/features/billing/components/InvoiceDetail";
export function InvoiceDetailContainer() {
const params = useParams();
const [loadingDownload, setLoadingDownload] = useState(false);
const [loadingPayment, setLoadingPayment] = useState(false);
const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false);
const rawInvoiceParam = params.id;
const invoiceIdParam = Array.isArray(rawInvoiceParam) ? rawInvoiceParam[0] : rawInvoiceParam;
const invoiceId = Number.parseInt(invoiceIdParam ?? "", 10);
const createSsoLinkMutation = useCreateInvoiceSsoLink();
const { data: invoice, isLoading, error } = useInvoice(invoiceIdParam ?? "");
const handleCreateSsoLink = (target: "view" | "download" | "pay" = "view") => {
void (async () => {
if (!invoice) return;
if (target === "download") setLoadingDownload(true);
else setLoadingPayment(true);
try {
const ssoLink = await createSsoLinkMutation.mutateAsync({ invoiceId: invoice.id, target });
if (target === "download") openSsoLink(ssoLink.url, { newTab: false });
else openSsoLink(ssoLink.url, { newTab: true });
} catch (err) {
logger.error(err, "Failed to create SSO link");
} finally {
if (target === "download") setLoadingDownload(false);
else setLoadingPayment(false);
}
})();
};
const handleManagePaymentMethods = () => {
void (async () => {
setLoadingPaymentMethods(true);
try {
const response = await apiClient.POST<InvoiceSsoLink>("/auth/sso-link", {
body: { path: "index.php?rp=/account/paymentmethods" },
});
const sso = getDataOrThrow<InvoiceSsoLink>(
response,
"Failed to create payment methods SSO link"
);
openSsoLink(sso.url, { newTab: true });
} catch (err) {
logger.error(err, "Failed to create payment methods SSO link");
} finally {
setLoadingPaymentMethods(false);
}
})();
};
if (isLoading) {
return (
<PageLayout
icon={<DocumentTextIcon />}
title="Invoice"
description="Invoice details and actions"
>
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8 space-y-6">
<LoadingCard />
<div className="bg-white rounded-2xl border p-6 space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-28" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-28" />
</div>
</div>
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center justify-between">
<Skeleton className="h-4 w-64" />
<Skeleton className="h-4 w-24" />
</div>
))}
</div>
</div>
</div>
</PageLayout>
);
}
if (error || !invoice) {
return (
<PageLayout
icon={<DocumentTextIcon />}
title="Invoice"
description="Invoice details and actions"
>
<ErrorState
title="Error loading invoice"
message={error instanceof Error ? error.message : "Invoice not found"}
variant="page"
/>
<div className="mt-4">
<Link href="/billing/invoices" className="text-primary font-medium">
Back to invoices
</Link>
</div>
</PageLayout>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50/30 py-8">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Navigation */}
<div className="mb-8">
<Link
href="/billing/invoices"
className="inline-flex items-center gap-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors group"
>
<svg
className="w-4 h-4 transition-transform group-hover:-translate-x-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to Invoices
</Link>
</div>
{/* Main Invoice Card */}
<div className="bg-white/80 backdrop-blur-sm rounded-3xl shadow-xl border border-white/20 overflow-hidden">
<InvoiceSummaryBar
invoice={invoice}
loadingDownload={loadingDownload}
loadingPayment={loadingPayment}
onDownload={() => handleCreateSsoLink("download")}
onPay={() => handleCreateSsoLink("pay")}
/>
{/* Success Banner for Paid Invoices */}
{invoice.status === "Paid" && (
<div className="px-8 py-4 bg-gradient-to-r from-emerald-50 to-teal-50 border-b border-emerald-100">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<CheckCircleIcon className="w-6 h-6 text-emerald-600" />
</div>
<div>
<h3 className="text-sm font-semibold text-emerald-900">Payment Received</h3>
<p className="text-sm text-emerald-700">
Paid on {invoice.paidDate || invoice.issuedAt}
</p>
</div>
</div>
</div>
)}
{/* Content */}
<div className="p-8">
<div className="space-y-8">
{/* Invoice Items */}
<InvoiceItems items={invoice.items} currency={invoice.currency} />
{/* Invoice Summary - Full Width */}
<div className="border-t border-slate-200 pt-8">
<InvoiceTotals
subtotal={invoice.subtotal}
tax={invoice.tax}
total={invoice.total}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default InvoiceDetailContainer;