202 lines
7.1 KiB
TypeScript
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;
|