Add Turso/Vercel deployment configuration
- Updated database connection to support Turso auth token - Added vercel.json with bun build configuration - Updated environment schema for production deployment - Added new features and components for production readiness
This commit is contained in:
+354
-215
@@ -4,30 +4,56 @@ import { useState } from "react";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { format } from "date-fns";
|
||||
import { Calendar, FileText, User, DollarSign, Trash2, Edit, Download, Send } from "lucide-react";
|
||||
import {
|
||||
Calendar,
|
||||
FileText,
|
||||
User,
|
||||
DollarSign,
|
||||
Trash2,
|
||||
Edit,
|
||||
Download,
|
||||
Send,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
MapPin,
|
||||
Mail,
|
||||
Phone,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { generateInvoicePDF } from "~/lib/pdf-export";
|
||||
import { InvoiceViewSkeleton } from "~/components/ui/skeleton";
|
||||
|
||||
interface InvoiceViewProps {
|
||||
invoiceId: string;
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
draft: "bg-gray-100 text-gray-800",
|
||||
sent: "bg-blue-100 text-blue-800",
|
||||
paid: "bg-green-100 text-green-800",
|
||||
overdue: "bg-red-100 text-red-800",
|
||||
} as const;
|
||||
|
||||
const statusLabels = {
|
||||
draft: "Draft",
|
||||
sent: "Sent",
|
||||
paid: "Paid",
|
||||
overdue: "Overdue",
|
||||
const statusConfig = {
|
||||
draft: { label: "Draft", color: "bg-gray-100 text-gray-800", icon: FileText },
|
||||
sent: { label: "Sent", color: "bg-blue-100 text-blue-800", icon: Send },
|
||||
paid: {
|
||||
label: "Paid",
|
||||
color: "bg-green-100 text-green-800",
|
||||
icon: DollarSign,
|
||||
},
|
||||
overdue: {
|
||||
label: "Overdue",
|
||||
color: "bg-red-100 text-red-800",
|
||||
icon: AlertCircle,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
@@ -36,7 +62,11 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
const [isExportingPDF, setIsExportingPDF] = useState(false);
|
||||
|
||||
// Fetch invoice data
|
||||
const { data: invoice, isLoading, refetch } = api.invoices.getById.useQuery({ id: invoiceId });
|
||||
const {
|
||||
data: invoice,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = api.invoices.getById.useQuery({ id: invoiceId });
|
||||
|
||||
// Delete mutation
|
||||
const deleteInvoice = api.invoices.delete.useMutation({
|
||||
@@ -69,13 +99,15 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
deleteInvoice.mutate({ id: invoiceId });
|
||||
};
|
||||
|
||||
const handleStatusUpdate = (newStatus: "draft" | "sent" | "paid" | "overdue") => {
|
||||
const handleStatusUpdate = (
|
||||
newStatus: "draft" | "sent" | "paid" | "overdue",
|
||||
) => {
|
||||
updateStatus.mutate({ id: invoiceId, status: newStatus });
|
||||
};
|
||||
|
||||
const handlePDFExport = async () => {
|
||||
if (!invoice) return;
|
||||
|
||||
|
||||
setIsExportingPDF(true);
|
||||
try {
|
||||
await generateInvoicePDF(invoice);
|
||||
@@ -99,31 +131,26 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
return format(new Date(date), "MMM dd, yyyy");
|
||||
};
|
||||
|
||||
const isOverdue =
|
||||
invoice &&
|
||||
new Date(invoice.dueDate) < new Date() &&
|
||||
invoice.status !== "paid";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<div className="h-8 bg-gray-200 rounded animate-pulse"></div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
|
||||
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
|
||||
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return <InvoiceViewSkeleton />;
|
||||
}
|
||||
|
||||
if (!invoice) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Invoice not found</h3>
|
||||
<p className="text-gray-500 mb-4">The invoice you're looking for doesn't exist or has been deleted.</p>
|
||||
<div className="py-12 text-center">
|
||||
<FileText className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
||||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
||||
Invoice not found
|
||||
</h3>
|
||||
<p className="mb-4 text-gray-500">
|
||||
The invoice you're looking for doesn't exist or has been
|
||||
deleted.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/dashboard/invoices">Back to Invoices</Link>
|
||||
</Button>
|
||||
@@ -131,99 +158,151 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const StatusIcon =
|
||||
statusConfig[invoice.status as keyof typeof statusConfig].icon;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Invoice Header */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
||||
<FileText className="h-6 w-6 text-emerald-600" />
|
||||
{invoice.invoiceNumber}
|
||||
</CardTitle>
|
||||
<p className="text-gray-600 mt-1">Created on {formatDate(invoice.createdAt)}</p>
|
||||
{/* Status Alert */}
|
||||
{isOverdue && (
|
||||
<Card className="border-red-200 bg-red-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-red-700">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
<span className="font-medium">This invoice is overdue</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${statusColors[invoice.status as keyof typeof statusColors]}`}>
|
||||
{statusLabels[invoice.status as keyof typeof statusLabels]}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleStatusUpdate("sent")}
|
||||
disabled={invoice.status === "sent" || updateStatus.isLoading}
|
||||
>
|
||||
<Send className="h-4 w-4 mr-1" />
|
||||
Mark Sent
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleStatusUpdate("paid")}
|
||||
disabled={invoice.status === "paid" || updateStatus.isLoading}
|
||||
>
|
||||
<DollarSign className="h-4 w-4 mr-1" />
|
||||
Mark Paid
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePDFExport}
|
||||
disabled={isExportingPDF}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
{isExportingPDF ? "Generating..." : "Export PDF"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Main Content */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Invoice Header Card */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardContent>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-2">
|
||||
<FileText className="h-6 w-6 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{invoice.invoiceNumber}
|
||||
</h2>
|
||||
<p className="text-gray-600">Professional Invoice</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Issue Date</span>
|
||||
<p className="font-medium text-gray-900">
|
||||
{formatDate(invoice.issueDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Due Date</span>
|
||||
<p className="font-medium text-gray-900">
|
||||
{formatDate(invoice.dueDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-right">
|
||||
<Badge
|
||||
className={`${statusConfig[invoice.status as keyof typeof statusConfig].color} px-3 py-1 text-sm font-medium`}
|
||||
>
|
||||
<StatusIcon className="mr-1 h-3 w-3" />
|
||||
{
|
||||
statusConfig[invoice.status as keyof typeof statusConfig]
|
||||
.label
|
||||
}
|
||||
</Badge>
|
||||
<div className="text-3xl font-bold text-emerald-600">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handlePDFExport}
|
||||
disabled={isExportingPDF}
|
||||
className="transform-none bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg transition-shadow duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
|
||||
>
|
||||
{isExportingPDF ? (
|
||||
<>
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
Generating PDF...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download PDF
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice Details */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Main Details */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Client Information */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-emerald-600" />
|
||||
Client Information
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700">
|
||||
<User className="h-5 w-5" />
|
||||
Bill To
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">Client Name</label>
|
||||
<p className="text-gray-900 font-medium">{invoice.client?.name}</p>
|
||||
</div>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{invoice.client?.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 text-sm md:grid-cols-2">
|
||||
{invoice.client?.email && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">Email</label>
|
||||
<p className="text-gray-900">{invoice.client.email}</p>
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Mail className="h-4 w-4 text-gray-400" />
|
||||
{invoice.client.email}
|
||||
</div>
|
||||
)}
|
||||
{invoice.client?.phone && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">Phone</label>
|
||||
<p className="text-gray-900">{invoice.client.phone}</p>
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Phone className="h-4 w-4 text-gray-400" />
|
||||
{invoice.client.phone}
|
||||
</div>
|
||||
)}
|
||||
{(invoice.client?.addressLine1 || invoice.client?.city || invoice.client?.state) && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">Address</label>
|
||||
<p className="text-gray-900">
|
||||
{[
|
||||
invoice.client?.addressLine1,
|
||||
invoice.client?.addressLine2,
|
||||
invoice.client?.city,
|
||||
invoice.client?.state,
|
||||
invoice.client?.postalCode,
|
||||
].filter(Boolean).join(", ")}
|
||||
</p>
|
||||
{(invoice.client?.addressLine1 ??
|
||||
invoice.client?.city ??
|
||||
invoice.client?.state) && (
|
||||
<div className="flex items-start gap-2 text-gray-600 md:col-span-2">
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400" />
|
||||
<div>
|
||||
{invoice.client?.addressLine1 && (
|
||||
<div>{invoice.client.addressLine1}</div>
|
||||
)}
|
||||
{invoice.client?.addressLine2 && (
|
||||
<div>{invoice.client.addressLine2}</div>
|
||||
)}
|
||||
{(invoice.client?.city ??
|
||||
invoice.client?.state ??
|
||||
invoice.client?.postalCode) && (
|
||||
<div>
|
||||
{[
|
||||
invoice.client?.city,
|
||||
invoice.client?.state,
|
||||
invoice.client?.postalCode,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
{invoice.client?.country && (
|
||||
<div>{invoice.client.country}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -231,122 +310,178 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Invoice Items */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">Invoice Items</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700">
|
||||
<Clock className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-700">Date</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-700">Description</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-700">Hours</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-700">Rate</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-700">Amount</th>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
|
||||
Hours
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
|
||||
Rate
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
|
||||
Amount
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoice.items?.map((item, index) => (
|
||||
<tr key={index} className="border-b border-gray-100 hover:bg-emerald-50/30 transition-colors">
|
||||
<td className="py-3 px-4 text-gray-900">{formatDate(item.date)}</td>
|
||||
<td className="py-3 px-4 text-gray-900">{item.description}</td>
|
||||
<td className="py-3 px-4 text-gray-900 text-right">{item.hours}</td>
|
||||
<td className="py-3 px-4 text-gray-900 text-right">{formatCurrency(item.rate)}</td>
|
||||
<td className="py-3 px-4 text-gray-900 font-semibold text-right">{formatCurrency(item.amount)}</td>
|
||||
<tr
|
||||
key={item.id || index}
|
||||
className="border-t border-gray-100 hover:bg-gray-50"
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{formatDate(item.date)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{item.description}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
{item.hours}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
{formatCurrency(item.rate)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
|
||||
{formatCurrency(item.amount)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t-2 border-emerald-200 bg-emerald-50/50">
|
||||
<td colSpan={4} className="py-4 px-4 text-right font-semibold text-gray-900">Total:</td>
|
||||
<td className="py-4 px-4 text-right font-bold text-emerald-600 text-lg">{formatCurrency(invoice.totalAmount)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Invoice Summary */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5 text-emerald-600" />
|
||||
Invoice Summary
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">Issue Date</label>
|
||||
<p className="text-gray-900">{formatDate(invoice.issueDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">Due Date</label>
|
||||
<p className="text-gray-900">{formatDate(invoice.dueDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">Status</label>
|
||||
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${statusColors[invoice.status as keyof typeof statusColors]}`}>
|
||||
{statusLabels[invoice.status as keyof typeof statusLabels]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<label className="text-lg font-semibold text-gray-900">Total Amount</label>
|
||||
<p className="text-2xl font-bold text-emerald-600">{formatCurrency(invoice.totalAmount)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
{invoice.notes && (
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">Notes</CardTitle>
|
||||
<CardTitle className="text-emerald-700">Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-700 whitespace-pre-wrap">{invoice.notes}</p>
|
||||
<p className="whitespace-pre-wrap text-gray-700">
|
||||
{invoice.notes}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Status Actions */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">Actions</CardTitle>
|
||||
<CardTitle className="text-emerald-700">Status Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{invoice.status === "draft" && (
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("sent")}
|
||||
disabled={updateStatus.isPending}
|
||||
className="w-full bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
Mark as Sent
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{invoice.status === "sent" && (
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("paid")}
|
||||
disabled={updateStatus.isPending}
|
||||
className="w-full bg-green-600 text-white hover:bg-green-700"
|
||||
>
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
Mark as Paid
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{invoice.status === "overdue" && (
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("paid")}
|
||||
disabled={updateStatus.isPending}
|
||||
className="w-full bg-green-600 text-white hover:bg-green-700"
|
||||
>
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
Mark as Paid
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{invoice.status === "paid" && (
|
||||
<div className="py-4 text-center">
|
||||
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600" />
|
||||
<p className="font-medium text-green-600">Invoice Paid</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice Summary */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-700">Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Subtotal</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Tax</span>
|
||||
<span className="font-medium">$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total</span>
|
||||
<span className="text-emerald-600">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-4 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
{invoice.items?.length ?? 0} item
|
||||
{invoice.items?.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="border-0 border-red-200 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-700">Danger Zone</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Button asChild className="w-full bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium">
|
||||
<Link href={`/dashboard/invoices/${invoiceId}/edit`}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit Invoice
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
onClick={handlePDFExport}
|
||||
disabled={isExportingPDF}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{isExportingPDF ? "Generating PDF..." : "Download PDF"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full bg-red-600 hover:bg-red-700"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Invoice
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
variant="outline"
|
||||
className="w-full border-red-200 text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Invoice
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -354,32 +489,36 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="bg-white/95 backdrop-blur-sm border-0 shadow-2xl">
|
||||
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-gray-800">Delete Invoice</DialogTitle>
|
||||
<DialogTitle className="text-xl font-bold text-gray-800">
|
||||
Delete Invoice
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-600">
|
||||
Are you sure you want to delete this invoice? This action cannot be undone.
|
||||
Are you sure you want to delete this invoice? This action cannot
|
||||
be undone and will permanently remove the invoice and all its
|
||||
data.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
disabled={deleteInvoice.isPending}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={deleteInvoice.isLoading}
|
||||
>
|
||||
{deleteInvoice.isLoading ? "Deleting..." : "Delete"}
|
||||
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user