mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-11-02 16:02:50 +00:00
108 files: README, marketing copy, page titles, meta tags, footer text, email templates, .env.example, UI strings. URLs swapped from papermark.com/papermark.io to dataroom.hanzo.ai. Internal identifiers left intact per repo policy: localStorage keys (papermark.email/papermark.name/last_papermark_login — changing them logs users out), type field names (papermarkUserId), internal helper symbols (PAPERMARK_HEADERS, PapermarkSparkle component, isPapermarkUrl helper), internal-only filenames. LLM.md retains 'Upstream: Papermark' attribution line. Per-locale .po translations regenerate from source on the next lingui-extract CI run. DB migrations untouched (append-only history; rewriting them breaks installed schemas). LICENSE stays AGPL-3.0.
195 lines
6.4 KiB
TypeScript
195 lines
6.4 KiB
TypeScript
import { useRouter } from "next/router";
|
|
|
|
import { useEffect } from "react";
|
|
|
|
import { Download, FileText, Loader2 } from "lucide-react";
|
|
|
|
import { useIsAdmin } from "@/lib/hooks/use-is-admin";
|
|
import { useInvoices } from "@/lib/swr/use-invoices";
|
|
|
|
import AppLayout from "@/components/layouts/app";
|
|
import { SettingsHeader } from "@/components/settings/settings-header";
|
|
import { TabMenu } from "@/components/tab-menu";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
|
|
export default function Invoices() {
|
|
const router = useRouter();
|
|
const { invoices, loading, error } = useInvoices();
|
|
const { isAdmin, loading: isAdminLoading } = useIsAdmin();
|
|
|
|
// Redirect non-admin users to general settings
|
|
useEffect(() => {
|
|
if (!isAdminLoading && !isAdmin) {
|
|
router.replace("/settings/general");
|
|
}
|
|
}, [isAdmin, isAdminLoading, router]);
|
|
|
|
const formatCurrency = (amount: number, currency: string) => {
|
|
return new Intl.NumberFormat("en-US", {
|
|
style: "currency",
|
|
currency: currency.toUpperCase(),
|
|
}).format(amount / 100);
|
|
};
|
|
|
|
const formatDate = (timestamp: number) => {
|
|
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
|
|
const handleDownload = (invoicePdf: string | null) => {
|
|
if (invoicePdf) {
|
|
window.open(invoicePdf, "_blank");
|
|
}
|
|
};
|
|
|
|
const handleViewInvoice = (hostedInvoiceUrl: string | null) => {
|
|
if (hostedInvoiceUrl) {
|
|
window.open(hostedInvoiceUrl, "_blank");
|
|
}
|
|
};
|
|
|
|
// Show nothing while checking admin status
|
|
if (isAdminLoading || !isAdmin) {
|
|
return (
|
|
<AppLayout>
|
|
<div />
|
|
</AppLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AppLayout>
|
|
<main className="relative mx-2 mb-10 mt-4 space-y-8 overflow-hidden px-1 sm:mx-3 md:mx-5 md:mt-5 lg:mx-7 lg:mt-8 xl:mx-10">
|
|
<SettingsHeader />
|
|
|
|
<TabMenu
|
|
navigation={[
|
|
{
|
|
label: "Subscription",
|
|
href: "/settings/billing",
|
|
value: "subscription",
|
|
currentValue: "invoices",
|
|
},
|
|
{
|
|
label: "Invoices",
|
|
href: "/settings/billing/invoices",
|
|
value: "invoices",
|
|
currentValue: "invoices",
|
|
},
|
|
]}
|
|
/>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<h3 className="text-2xl font-semibold tracking-tight text-foreground">
|
|
Invoices
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
A history of all your invoices
|
|
</p>
|
|
</div>
|
|
|
|
<div className="rounded-lg border bg-white">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : error ? (
|
|
<div className="flex flex-col items-center justify-center gap-2 py-12">
|
|
<p className="text-sm text-muted-foreground">
|
|
Failed to load invoices
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => router.reload()}
|
|
>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
) : invoices.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center gap-4 py-12">
|
|
<div className="rounded-full border border-white bg-gradient-to-t from-gray-100 p-3">
|
|
<FileText className="size-6" />
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-sm font-medium">No invoices yet</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Your invoices will appear here once you have a subscription
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Description</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead>Total</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{invoices.map((invoice) => (
|
|
<TableRow key={invoice.id}>
|
|
<TableCell>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">
|
|
Hanzo Dataroom Subscription
|
|
</span>
|
|
{invoice.number && (
|
|
<span className="text-xs text-muted-foreground">
|
|
{invoice.number}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{formatDate(invoice.created)}</TableCell>
|
|
<TableCell>
|
|
{formatCurrency(invoice.amount, invoice.currency)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() =>
|
|
handleViewInvoice(invoice.hostedInvoiceUrl)
|
|
}
|
|
disabled={!invoice.hostedInvoiceUrl}
|
|
>
|
|
View invoice
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleDownload(invoice.invoicePdf)}
|
|
disabled={!invoice.invoicePdf}
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</AppLayout>
|
|
);
|
|
}
|