- Deleted loading and page components for invoices and payment methods to streamline the billing section. - Updated AnimatedContainer, InlineToast, and other components to utilize framer-motion for improved animations. - Refactored AppShell and Sidebar components to enhance layout and integrate new animation features. - Adjusted various sections across the portal to ensure consistent animation behavior and visual appeal.
146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
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 "@/core/logger";
|
|
import { openSsoLink } from "@/features/billing/utils/sso";
|
|
import { useInvoice, useCreateInvoiceSsoLink } from "@/features/billing/hooks";
|
|
import {
|
|
InvoiceItems,
|
|
InvoiceTotals,
|
|
InvoiceSummaryBar,
|
|
} from "@/features/billing/components/InvoiceDetail";
|
|
|
|
function InvoiceDetailSkeleton() {
|
|
return (
|
|
<PageLayout icon={<DocumentTextIcon />} title="Invoice">
|
|
<div className="space-y-6">
|
|
<LoadingCard />
|
|
<div className="bg-card text-card-foreground rounded-xl border border-border p-6 space-y-4 shadow-[var(--cp-shadow-1)]">
|
|
<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>
|
|
);
|
|
}
|
|
|
|
export function InvoiceDetailContainer() {
|
|
const params = useParams();
|
|
const [loadingDownload, setLoadingDownload] = useState(false);
|
|
const [loadingPayment, setLoadingPayment] = useState(false);
|
|
|
|
const rawInvoiceParam = params["id"];
|
|
const invoiceIdParam = Array.isArray(rawInvoiceParam) ? rawInvoiceParam[0] : rawInvoiceParam;
|
|
const createSsoLinkMutation = useCreateInvoiceSsoLink();
|
|
const { data: invoice, error } = useInvoice(invoiceIdParam ?? "");
|
|
const isLoading = !invoice && !error;
|
|
|
|
const handleCreateSsoLink = (target: "view" | "download" | "pay" = "view") => {
|
|
void (async () => {
|
|
if (!invoice) return;
|
|
const isDownload = target === "download";
|
|
if (isDownload) setLoadingDownload(true);
|
|
else setLoadingPayment(true);
|
|
try {
|
|
const ssoLink = await createSsoLinkMutation.mutateAsync({ invoiceId: invoice.id, target });
|
|
openSsoLink(ssoLink.url, { newTab: !isDownload });
|
|
} catch (err) {
|
|
logger.error("Failed to create SSO link", err);
|
|
} finally {
|
|
if (isDownload) setLoadingDownload(false);
|
|
else setLoadingPayment(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
if (isLoading) return <InvoiceDetailSkeleton />;
|
|
|
|
if (error || !invoice) {
|
|
return (
|
|
<PageLayout
|
|
icon={<DocumentTextIcon />}
|
|
title="Invoice"
|
|
backLink={{ label: "Back to Billing", href: "/account/billing" }}
|
|
>
|
|
<ErrorState
|
|
title="Error loading invoice"
|
|
message={error instanceof Error ? error.message : "Invoice not found"}
|
|
variant="page"
|
|
/>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageLayout
|
|
icon={<DocumentTextIcon />}
|
|
title={`Invoice #${invoice.id}`}
|
|
backLink={{ label: "Back to Billing", href: "/account/billing" }}
|
|
>
|
|
<div>
|
|
<div className="bg-card text-card-foreground rounded-xl shadow-[var(--cp-shadow-1)] border border-border overflow-hidden">
|
|
<InvoiceSummaryBar
|
|
invoice={invoice}
|
|
loadingDownload={loadingDownload}
|
|
loadingPayment={loadingPayment}
|
|
onDownload={() => handleCreateSsoLink("download")}
|
|
onPay={() => handleCreateSsoLink("pay")}
|
|
/>
|
|
|
|
{invoice.status === "Paid" && (
|
|
<div className="px-8 py-4 bg-success-soft border-b border-success/25">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex-shrink-0">
|
|
<CheckCircleIcon className="w-6 h-6 text-success" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-foreground">Payment received</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Paid on {invoice.paidDate || invoice.issuedAt}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-8">
|
|
<div className="space-y-8">
|
|
<InvoiceItems items={invoice.items ?? []} currency={invoice.currency} />
|
|
<div className="border-t border-border pt-8">
|
|
<InvoiceTotals
|
|
subtotal={invoice.subtotal}
|
|
tax={invoice.tax}
|
|
total={invoice.total}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
export default InvoiceDetailContainer;
|