116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
import Link from "next/link";
|
|
import { ShellPage } from "@/components/page-templates";
|
|
import { Badge, SectionCard } from "@/components/ui";
|
|
import { getSession } from "@/lib/auth";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { redirect } from "next/navigation";
|
|
|
|
function formatDate(value: Date | null) {
|
|
if (!value) {
|
|
return "-";
|
|
}
|
|
|
|
return new Intl.DateTimeFormat("id-ID", {
|
|
day: "2-digit",
|
|
month: "short",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit"
|
|
}).format(value);
|
|
}
|
|
|
|
function formatMoney(value: number) {
|
|
return new Intl.NumberFormat("id-ID", {
|
|
style: "currency",
|
|
currency: "IDR",
|
|
maximumFractionDigits: 0
|
|
}).format(value);
|
|
}
|
|
|
|
function statusTone(status: string) {
|
|
if (status === "PAID") {
|
|
return "success";
|
|
}
|
|
|
|
if (status === "OVERDUE") {
|
|
return "danger";
|
|
}
|
|
|
|
return "warning";
|
|
}
|
|
|
|
export default async function BillingInvoiceDetailPage({
|
|
params
|
|
}: {
|
|
params: Promise<{ invoiceId: string }>;
|
|
}) {
|
|
const session = await getSession();
|
|
if (!session) {
|
|
redirect("/login");
|
|
}
|
|
|
|
const { invoiceId } = await params;
|
|
const invoice = await prisma.billingInvoice.findFirst({
|
|
where: { id: invoiceId, tenantId: session.tenantId },
|
|
include: {
|
|
tenant: { select: { name: true, slug: true } },
|
|
plan: { select: { name: true, code: true } }
|
|
}
|
|
});
|
|
|
|
if (!invoice) {
|
|
redirect("/billing/history?error=invoice_not_found");
|
|
}
|
|
|
|
return (
|
|
<ShellPage shell="admin" title="Invoice Detail" description="Ringkasan invoice per tenant.">
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<SectionCard title="Invoice summary">
|
|
<div className="space-y-2 text-sm text-on-surface-variant">
|
|
<p>
|
|
<strong className="text-on-surface">Invoice:</strong> {invoice.invoiceNumber}
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Tenant:</strong>{" "}
|
|
<Link href="/billing/history" className="text-brand hover:underline">
|
|
{invoice.tenant.name} ({invoice.tenant.slug})
|
|
</Link>
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Plan:</strong> {invoice.plan.name} ({invoice.plan.code})
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Period:</strong> {formatDate(invoice.periodStart)} - {formatDate(invoice.periodEnd)}
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Subtotal:</strong> {formatMoney(invoice.subtotal)} | Tax: {formatMoney(invoice.taxAmount)}
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Total:</strong> {formatMoney(invoice.totalAmount)}
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Status:</strong> <Badge tone={statusTone(invoice.paymentStatus)}>{invoice.paymentStatus}</Badge>
|
|
</p>
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard title="Timeline">
|
|
<div className="space-y-2 text-sm text-on-surface-variant">
|
|
<p>
|
|
<strong className="text-on-surface">Issued:</strong> {formatDate(invoice.createdAt)}
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Due date:</strong> {formatDate(invoice.dueDate)}
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Paid at:</strong> {formatDate(invoice.paidAt)}
|
|
</p>
|
|
<p>
|
|
<strong className="text-on-surface">Updated:</strong> {formatDate(invoice.updatedAt)}
|
|
</p>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
</ShellPage>
|
|
);
|
|
}
|