mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 17:48:55 -04:00
Add clickable rows and standardize action button styles
The changes add row click functionality and consistent action button styling across data tables. Main updates: - Add `onRowClick` handler to make rows clickable and navigate to details pages - Add `data-action-button` attribute to exclude action buttons from row click - Fix TypeScript errors and types
This commit is contained in:
@@ -11,7 +11,6 @@ import { PDFDownloadButton } from "./_components/pdf-download-button";
|
||||
import { SendInvoiceButton } from "./_components/send-invoice-button";
|
||||
import { InvoiceDetailsSkeleton } from "./_components/invoice-details-skeleton";
|
||||
import { InvoiceActionsDropdown } from "./_components/invoice-actions-dropdown";
|
||||
import { InvoiceItemsTable } from "./_components/invoice-items-table";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Building,
|
||||
@@ -25,6 +24,12 @@ import {
|
||||
User,
|
||||
AlertTriangle,
|
||||
Trash2,
|
||||
DollarSign,
|
||||
Clock,
|
||||
Eye,
|
||||
Download,
|
||||
Send,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
|
||||
interface InvoicePageProps {
|
||||
@@ -55,6 +60,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
|
||||
|
||||
const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0);
|
||||
const taxAmount = (subtotal * invoice.taxRate) / 100;
|
||||
const total = subtotal + taxAmount;
|
||||
const isOverdue =
|
||||
new Date(invoice.dueDate) < new Date() && invoice.status !== "paid";
|
||||
|
||||
@@ -68,103 +74,116 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Overdue Alert */}
|
||||
{isOverdue && (
|
||||
<Card className="border-red-200 bg-red-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-red-700">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<span className="font-medium">
|
||||
This invoice is{" "}
|
||||
{Math.ceil(
|
||||
(new Date().getTime() - new Date(invoice.dueDate).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)}{" "}
|
||||
days overdue
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<div className="space-y-6 pb-24">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">
|
||||
Invoice Details
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
View and manage invoice information
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<InvoiceActionsDropdown invoiceId={invoice.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-4 xl:grid-cols-3">
|
||||
{/* Main Content */}
|
||||
<div className="space-y-6 lg:col-span-3 xl:col-span-2">
|
||||
{/* Content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Column */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Invoice Header */}
|
||||
<Card className="shadow-lg">
|
||||
<CardContent className="p-4 sm:p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-bold sm:text-2xl">
|
||||
Invoice #{invoice.invoiceNumber}
|
||||
</h1>
|
||||
<StatusBadge status={getStatusType()} />
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
{invoice.invoiceNumber}
|
||||
</h2>
|
||||
<StatusBadge status={getStatusType()} />
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Issued {formatDate(invoice.issueDate)} • Due {formatDate(invoice.dueDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-muted-foreground">Total Amount</p>
|
||||
<p className="text-3xl font-bold text-primary">
|
||||
{formatCurrency(total)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm sm:text-base">
|
||||
Issued {formatDate(invoice.issueDate)} • Due{" "}
|
||||
{formatDate(invoice.dueDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-left sm:text-right">
|
||||
<p className="text-muted-foreground text-sm sm:text-base">
|
||||
Total Amount
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-emerald-600 sm:text-3xl">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Client & Business Information */}
|
||||
<div className="grid gap-4 sm:gap-6 md:grid-cols-2">
|
||||
{/* Overdue Alert */}
|
||||
{isOverdue && (
|
||||
<Card className="border-destructive/20 bg-destructive/5 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 text-destructive">
|
||||
<AlertTriangle className="h-5 w-5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">Invoice Overdue</p>
|
||||
<p className="text-sm">
|
||||
{Math.ceil(
|
||||
(new Date().getTime() - new Date(invoice.dueDate).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)}{" "}
|
||||
days past due date
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Client & Business Info */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* Client Information */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-600">
|
||||
<User className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" />
|
||||
Bill To
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 sm:space-y-4">
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-foreground text-lg font-semibold sm:text-xl">
|
||||
<h3 className="text-xl font-semibold text-foreground">
|
||||
{invoice.client.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
<div className="space-y-3">
|
||||
{invoice.client.email && (
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
|
||||
<Mail className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<Mail className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<span className="text-sm break-all sm:text-base">
|
||||
{invoice.client.email}
|
||||
</span>
|
||||
<span className="text-sm break-all">{invoice.client.email}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.client.phone && (
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
|
||||
<Phone className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<Phone className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<span className="text-sm sm:text-base">
|
||||
{invoice.client.phone}
|
||||
</span>
|
||||
<span className="text-sm">{invoice.client.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(invoice.client.addressLine1 ?? invoice.client.city) && (
|
||||
<div className="flex items-start gap-2 sm:gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
|
||||
<MapPin className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<MapPin className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="text-sm sm:text-base">
|
||||
<div className="text-sm space-y-1">
|
||||
{invoice.client.addressLine1 && (
|
||||
<div>{invoice.client.addressLine1}</div>
|
||||
)}
|
||||
@@ -196,40 +215,36 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
|
||||
|
||||
{/* Business Information */}
|
||||
{invoice.business && (
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-600">
|
||||
<Building className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building className="h-5 w-5" />
|
||||
From
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 sm:space-y-4">
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-foreground text-lg font-semibold sm:text-xl">
|
||||
<h3 className="text-xl font-semibold text-foreground">
|
||||
{invoice.business.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
<div className="space-y-3">
|
||||
{invoice.business.email && (
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
|
||||
<Mail className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<Mail className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<span className="text-sm break-all sm:text-base">
|
||||
{invoice.business.email}
|
||||
</span>
|
||||
<span className="text-sm break-all">{invoice.business.email}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.business.phone && (
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-1.5 sm:p-2">
|
||||
<Phone className="h-3 w-3 text-emerald-600 sm:h-4 sm:w-4" />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<Phone className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<span className="text-sm sm:text-base">
|
||||
{invoice.business.phone}
|
||||
</span>
|
||||
<span className="text-sm">{invoice.business.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -239,43 +254,53 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
|
||||
</div>
|
||||
|
||||
{/* Invoice Items */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-3">
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<InvoiceItemsTable items={invoice.items} />
|
||||
<CardContent className="space-y-4">
|
||||
{invoice.items.map((item, index) => (
|
||||
<div key={item.id} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-foreground text-base font-medium mb-2">{item.description}</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{formatDate(item.date)}</span>
|
||||
<span>•</span>
|
||||
<span>{item.hours} hours</span>
|
||||
<span>•</span>
|
||||
<span>@ ${item.rate}/hr</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-semibold text-primary">
|
||||
{formatCurrency(item.amount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Totals */}
|
||||
<div className="mt-6 border-t pt-4">
|
||||
<div className="flex justify-end">
|
||||
<div className="w-full space-y-2 sm:max-w-64">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Subtotal:</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
{invoice.taxRate > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
Tax ({invoice.taxRate}%):
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(taxAmount)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex justify-between text-base font-bold sm:text-lg">
|
||||
<span>Total:</span>
|
||||
<span className="text-emerald-600">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Subtotal:</span>
|
||||
<span className="font-medium">{formatCurrency(subtotal)}</span>
|
||||
</div>
|
||||
{invoice.taxRate > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Tax ({invoice.taxRate}%):</span>
|
||||
<span className="font-medium">{formatCurrency(taxAmount)}</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total:</span>
|
||||
<span className="text-primary">{formatCurrency(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,103 +309,47 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
|
||||
|
||||
{/* Notes */}
|
||||
{invoice.notes && (
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Notes
|
||||
</CardTitle>
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<p className="text-muted-foreground leading-relaxed whitespace-pre-wrap">
|
||||
{invoice.notes}
|
||||
</p>
|
||||
<CardContent>
|
||||
<p className="text-foreground whitespace-pre-wrap">{invoice.notes}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-4 sm:space-y-6 lg:col-span-1">
|
||||
{/* Actions */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base sm:text-lg">Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full border-0 shadow-sm"
|
||||
size="default"
|
||||
>
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
<span>Edit Invoice</span>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<PDFDownloadButton invoiceId={invoice.id} />
|
||||
|
||||
<SendInvoiceButton invoiceId={invoice.id} />
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-0 shadow-sm"
|
||||
size="default"
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
<span>Duplicate</span>
|
||||
</Button>
|
||||
|
||||
<Button variant="destructive" size="default" className="w-full">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>Delete Invoice</span>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice Details */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base sm:text-lg">
|
||||
<Calendar className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
Details
|
||||
{/* Right Column - Actions */}
|
||||
<div className="space-y-6">
|
||||
<Card className="sticky top-6 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Check className="h-5 w-5" />
|
||||
Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">Invoice #</p>
|
||||
<p className="font-medium break-all">
|
||||
{invoice.invoiceNumber}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">Status</p>
|
||||
<div className="mt-1">
|
||||
<StatusBadge status={getStatusType()} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">Issue Date</p>
|
||||
<p className="font-medium">{formatDate(invoice.issueDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">Due Date</p>
|
||||
<p className="font-medium">{formatDate(invoice.dueDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">Tax Rate</p>
|
||||
<p className="font-medium">{invoice.taxRate}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">Total</p>
|
||||
<p className="font-medium text-emerald-600">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="space-y-3">
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Invoice
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{invoice.items && invoice.client && (
|
||||
<PDFDownloadButton
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{invoice.status === "draft" && (
|
||||
<SendInvoiceButton
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -393,35 +362,10 @@ export default async function InvoicePage({ params }: InvoicePageProps) {
|
||||
const { id } = await params;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Invoice Details"
|
||||
description="View and manage invoice information"
|
||||
variant="gradient"
|
||||
>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="border-0 shadow-sm"
|
||||
size="default"
|
||||
>
|
||||
<Link href="/dashboard/invoices">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
<span className="hidden sm:inline">Back to Invoices</span>
|
||||
<span className="sm:hidden">Back</span>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<InvoiceActionsDropdown invoiceId={id} />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<HydrateClient>
|
||||
<Suspense fallback={<InvoiceDetailsSkeleton />}>
|
||||
<InvoiceContent invoiceId={id} />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
</>
|
||||
<HydrateClient>
|
||||
<Suspense fallback={<InvoiceDetailsSkeleton />}>
|
||||
<InvoiceContent invoiceId={id} />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user