- Invoice page with table, search, sort, CSV export, PDF download - Company-level access control (can_access_invoices + view_invoices permission) - Notifications page with unread alerts section and mark-as-read - Fix unread notification count in layout (was counting read markers) - Fix dashboard notification count to use per-user unread alerts - Configurable INVOICE_STORAGE_PATH for invoice PDF file serving Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { InvoiceTable } from '@/components/invoices/invoice-table';
|
|
import { FileText } from 'lucide-react';
|
|
import type { InvoiceEntry } from '@/types/invoices';
|
|
|
|
function InvoiceSkeleton() {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="space-y-4">
|
|
<div className="h-8 w-48 animate-pulse rounded bg-muted" />
|
|
<div className="h-10 w-full animate-pulse rounded bg-muted" />
|
|
<div className="space-y-2">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="h-12 w-full animate-pulse rounded bg-muted"
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default function InvoicesPage() {
|
|
const [invoices, setInvoices] = useState<InvoiceEntry[] | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [accessDenied, setAccessDenied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetch('/api/invoices')
|
|
.then((res) => {
|
|
if (res.status === 403) {
|
|
setAccessDenied(true);
|
|
return [];
|
|
}
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
return res.json();
|
|
})
|
|
.then((json) => {
|
|
if (!accessDenied) setInvoices(json);
|
|
})
|
|
.catch((err) => setError(err.message));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
if (accessDenied) {
|
|
return (
|
|
<div>
|
|
<h1 className="mb-6 text-3xl font-bold">Invoices</h1>
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
<FileText className="mb-4 h-12 w-12 text-muted-foreground/40" />
|
|
<p className="text-lg font-medium text-muted-foreground">
|
|
Invoice access is not available
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Your company does not have invoice viewing enabled. Please contact
|
|
Vorteq support if you need access to invoices.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="mb-6 text-3xl font-bold">Invoices</h1>
|
|
{error ? (
|
|
<Card>
|
|
<CardContent className="p-6 text-center text-destructive">
|
|
Failed to load invoices: {error}
|
|
</CardContent>
|
|
</Card>
|
|
) : invoices === null ? (
|
|
<InvoiceSkeleton />
|
|
) : (
|
|
<InvoiceTable data={invoices} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|