Build fixes, email preview system

This commit is contained in:
2025-07-29 19:45:38 -04:00
parent e6791f8cb8
commit 9370d5c935
78 changed files with 5798 additions and 10397 deletions
@@ -4,27 +4,68 @@ import { useState } from "react";
import { Button } from "~/components/ui/button";
import { toast } from "sonner";
import { api } from "~/trpc/react";
import { generateInvoicePDFBlob } from "~/lib/pdf-export";
import { Send, Loader2 } from "lucide-react";
interface SendInvoiceButtonProps {
invoiceId: string;
variant?: "default" | "outline" | "ghost" | "icon";
className?: string;
showResend?: boolean;
}
export function SendInvoiceButton({
invoiceId,
variant = "outline",
className,
showResend = false,
}: SendInvoiceButtonProps) {
const [isSending, setIsSending] = useState(false);
// Fetch invoice data when sending is triggered
const { refetch: fetchInvoice } = api.invoices.getById.useQuery(
{ id: invoiceId },
{ enabled: false },
);
// Get utils for cache invalidation
const utils = api.useUtils();
// Use the new email API mutation
const sendInvoiceMutation = api.email.sendInvoice.useMutation({
onSuccess: (data) => {
// Show detailed success message with delivery info
toast.success(data.message, {
description: `Email ID: ${data.emailId}`,
duration: 5000,
});
// Refresh invoice data to show updated status
void utils.invoices.getById.invalidate({ id: invoiceId });
},
onError: (error) => {
// Enhanced error handling with specific error types
console.error("Email send error:", error);
let errorMessage = "Failed to send invoice email";
let errorDescription = "";
if (error.message.includes("Invalid recipient")) {
errorMessage = "Invalid Email Address";
errorDescription =
"Please check the client's email address and try again.";
} else if (error.message.includes("domain not verified")) {
errorMessage = "Email Configuration Issue";
errorDescription = "Please contact support to configure email sending.";
} else if (error.message.includes("rate limit")) {
errorMessage = "Too Many Emails";
errorDescription = "Please wait a moment before sending another email.";
} else if (error.message.includes("no email address")) {
errorMessage = "No Email Address";
errorDescription = "This client doesn't have an email address on file.";
} else {
errorDescription = error.message;
}
toast.error(errorMessage, {
description: errorDescription,
duration: 6000,
});
},
});
const handleSendInvoice = async () => {
if (isSending) return;
@@ -32,88 +73,12 @@ export function SendInvoiceButton({
setIsSending(true);
try {
// Fetch fresh invoice data
const { data: invoice } = await fetchInvoice();
if (!invoice) {
throw new Error("Invoice not found");
}
// Generate PDF blob for potential attachment
const pdfBlob = await generateInvoicePDFBlob(invoice);
// Create a temporary download URL for the PDF
const pdfUrl = URL.createObjectURL(pdfBlob);
// Format currency
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
// Format date
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
};
// Calculate days until due
const today = new Date();
const dueDate = new Date(invoice.dueDate);
const daysUntilDue = Math.ceil(
(dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
);
// Create professional email template
const subject = `Invoice ${invoice.invoiceNumber} - ${formatCurrency(invoice.totalAmount)}`;
const body = `Dear ${invoice.client.name},
I hope this email finds you well. Please find attached invoice ${invoice.invoiceNumber} for the services provided.
Invoice Details:
• Invoice Number: ${invoice.invoiceNumber}
• Issue Date: ${formatDate(invoice.issueDate)}
• Due Date: ${formatDate(invoice.dueDate)}
• Amount Due: ${formatCurrency(invoice.totalAmount)}
${daysUntilDue > 0 ? `• Payment Due: In ${daysUntilDue} days` : daysUntilDue === 0 ? `• Payment Due: Today` : `• Status: ${Math.abs(daysUntilDue)} days overdue`}
${invoice.notes ? `\nAdditional Notes:\n${invoice.notes}\n` : ""}
Please review the attached invoice and remit payment by the due date. If you have any questions or concerns regarding this invoice, please don't hesitate to contact me.
Thank you for your business!
Best regards,
${invoice.business?.name ?? "Your Business Name"}
${invoice.business?.email ? `\n${invoice.business.email}` : ""}
${invoice.business?.phone ? `\n${invoice.business.phone}` : ""}`;
// Create mailto link
const mailtoLink = `mailto:${invoice.client.email ?? ""}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
// Create a temporary link element to trigger mailto
const link = document.createElement("a");
link.href = mailtoLink;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the PDF URL object
URL.revokeObjectURL(pdfUrl);
toast.success("Email client opened with invoice details");
await sendInvoiceMutation.mutateAsync({
invoiceId,
});
} catch (error) {
// Error is already handled by the mutation's onError
console.error("Send invoice error:", error);
toast.error(
error instanceof Error
? error.message
: "Failed to prepare invoice email",
);
} finally {
setIsSending(false);
}
@@ -149,12 +114,12 @@ ${invoice.business?.phone ? `\n${invoice.business.phone}` : ""}`;
{isSending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Preparing Email...</span>
<span>Sending Email...</span>
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
<span>Send Invoice</span>
<span>{showResend ? "Resend Invoice" : "Send Invoice"}</span>
</>
)}
</Button>
@@ -0,0 +1,511 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { useParams, useRouter } from "next/navigation";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import { Alert, AlertDescription } from "~/components/ui/alert";
import { Label } from "~/components/ui/label";
import { PageHeader } from "~/components/layout/page-header";
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { EmailComposer } from "~/components/forms/email-composer";
import { EmailPreview } from "~/components/forms/email-preview";
import { api } from "~/trpc/react";
import { toast } from "sonner";
import {
Mail,
Send,
Eye,
Edit3,
AlertTriangle,
ArrowLeft,
Loader2,
FileText,
} from "lucide-react";
function SendEmailPageSkeleton() {
return (
<div className="space-y-6 pb-32">
<PageHeader
title="Loading..."
description="Loading invoice email"
variant="gradient"
/>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-2">
<div className="bg-muted h-96 animate-pulse rounded-lg" />
</div>
<div className="space-y-6">
<div className="bg-muted h-64 animate-pulse rounded-lg" />
</div>
</div>
</div>
);
}
export default function SendEmailPage() {
const params = useParams();
const router = useRouter();
const invoiceId = params.id as string;
// State management
const [activeTab, setActiveTab] = useState("compose");
const [isSending, setIsSending] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
// Email content state
const [subject, setSubject] = useState("");
const [emailContent, setEmailContent] = useState("");
const [ccEmail, setCcEmail] = useState("");
const [bccEmail, setBccEmail] = useState("");
const [customMessage, setCustomMessage] = useState("");
// Fetch invoice data
const { data: invoiceData, isLoading: invoiceLoading } =
api.invoices.getById.useQuery({
id: invoiceId,
});
// Get utils for cache invalidation
const utils = api.useUtils();
// Email sending mutation
const sendEmailMutation = api.email.sendInvoice.useMutation({
onSuccess: (data) => {
toast.success("Email sent successfully!", {
description: data.message,
duration: 5000,
});
// Navigate back to invoice view
router.push(`/dashboard/invoices/${invoiceId}/view`);
// Refresh invoice data
void utils.invoices.getById.invalidate({ id: invoiceId });
},
onError: (error) => {
console.error("Email send error:", error);
let errorMessage = "Failed to send invoice email";
let errorDescription = error.message;
if (error.message.includes("Invalid recipient")) {
errorMessage = "Invalid Email Address";
errorDescription =
"Please check the client's email address and try again.";
} else if (error.message.includes("domain not verified")) {
errorMessage = "Email Configuration Issue";
errorDescription = "Please contact support to configure email sending.";
} else if (error.message.includes("rate limit")) {
errorMessage = "Too Many Emails";
errorDescription = "Please wait a moment before sending another email.";
} else if (error.message.includes("no email address")) {
errorMessage = "No Email Address";
errorDescription = "This client doesn't have an email address on file.";
}
toast.error(errorMessage, {
description: errorDescription,
duration: 6000,
});
setIsSending(false);
},
});
// Transform invoice data for components
const invoice = useMemo(() => {
return invoiceData
? {
id: invoiceData.id,
invoiceNumber: invoiceData.invoiceNumber,
issueDate: invoiceData.issueDate,
dueDate: invoiceData.dueDate,
status: invoiceData.status,
taxRate: invoiceData.taxRate,
client: invoiceData.client
? {
name: invoiceData.client.name,
email: invoiceData.client.email,
}
: undefined,
business: invoiceData.business
? {
name: invoiceData.business.name,
email: invoiceData.business.email,
}
: undefined,
items: invoiceData.items?.map((item) => ({
id: item.id,
hours: item.hours,
rate: item.rate,
})),
}
: undefined;
}, [invoiceData]);
// Initialize email content when invoice loads
useEffect(() => {
if (!invoice || isInitialized) return;
// Set default subject
const defaultSubject = `Invoice ${invoice.invoiceNumber} from ${invoice.business?.name ?? "Your Business"}`;
setSubject(defaultSubject);
// Set default content (empty since template handles everything)
const defaultContent = ``;
setEmailContent(defaultContent);
setIsInitialized(true);
}, [invoice, isInitialized]);
const handleSendEmail = async () => {
if (!invoice?.client?.email || invoice.client.email.trim() === "") {
toast.error("No email address", {
description: "This client doesn't have an email address on file.",
});
return;
}
if (!subject.trim()) {
toast.error("Subject required", {
description: "Please enter an email subject before sending.",
});
return;
}
// Email content is now optional since template handles default messaging
setIsSending(true);
try {
await sendEmailMutation.mutateAsync({
invoiceId,
customSubject: subject,
customContent: emailContent,
customMessage: customMessage?.trim() || undefined,
useHtml: true,
ccEmails: ccEmail.trim() || undefined,
bccEmails: bccEmail.trim() || undefined,
});
} catch (error) {
// Error handling is done in the mutation's onError
console.error("Send email error:", error);
}
};
const fromEmail = invoice?.business?.email ?? "noreply@yourdomain.com";
const toEmail = invoice?.client?.email ?? "";
const canSend =
!isSending && subject.trim() && toEmail && toEmail.trim() !== "";
if (invoiceLoading) {
return <SendEmailPageSkeleton />;
}
if (!invoice) {
return (
<div className="container mx-auto max-w-4xl p-6">
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>Invoice not found.</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto max-w-6xl space-y-6 pb-32">
<PageHeader
title={`Send Invoice ${invoice.invoiceNumber}`}
description={`Compose and send invoice email to ${invoice.client?.name ?? "client"}${new Intl.DateTimeFormat(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
},
).format(new Date())}`}
variant="gradient"
>
<Button
variant="outline"
onClick={() => router.push(`/dashboard/invoices/${invoiceId}/view`)}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Invoice
</Button>
</PageHeader>
{/* Warning for missing email */}
{(!toEmail || toEmail.trim() === "") && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
This client doesn&apos;t have an email address. Please add an email
address to the client before sending the invoice.
</AlertDescription>
</Alert>
)}
{/* Main Content */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="compose" className="flex items-center gap-2">
<Edit3 className="h-4 w-4" />
Compose
</TabsTrigger>
<TabsTrigger value="preview" className="flex items-center gap-2">
<Eye className="h-4 w-4" />
Preview
</TabsTrigger>
</TabsList>
<div className="mt-6">
<TabsContent value="compose" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Compose Email
</CardTitle>
</CardHeader>
<CardContent>
{isInitialized ? (
<EmailComposer
subject={subject}
onSubjectChange={setSubject}
content={emailContent}
onContentChange={setEmailContent}
customMessage={customMessage}
onCustomMessageChange={setCustomMessage}
fromEmail={fromEmail}
toEmail={toEmail}
ccEmail={ccEmail}
onCcEmailChange={setCcEmail}
bccEmail={bccEmail}
onBccEmailChange={setBccEmail}
/>
) : (
<div className="bg-muted flex h-[400px] items-center justify-center rounded-md border">
<div className="text-center">
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
<p className="text-muted-foreground text-sm">
Initializing email content...
</p>
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="preview" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Eye className="h-5 w-5" />
Email Preview
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<EmailPreview
subject={subject}
fromEmail={fromEmail}
toEmail={toEmail}
ccEmail={ccEmail}
bccEmail={bccEmail}
content={emailContent}
customMessage={customMessage}
invoice={invoice}
className="min-w-0 border-0"
/>
</div>
</CardContent>
</Card>
</TabsContent>
</div>
</Tabs>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Invoice Summary */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<FileText className="h-5 w-5 text-green-600" />
Invoice #{invoice.invoiceNumber}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label className="text-muted-foreground text-sm font-medium">
Client
</Label>
<p className="text-sm font-medium">
{invoice.client?.name ?? "Client"}
</p>
</div>
<div>
<Label className="text-muted-foreground text-sm font-medium">
Issue Date
</Label>
<p className="text-sm">
{new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(new Date(invoice.issueDate))}
</p>
</div>
<div>
<Label className="text-muted-foreground text-sm font-medium">
Status
</Label>
<Badge variant="outline">{invoice.status}</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Email Details</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label className="text-muted-foreground text-sm font-medium">
From
</Label>
<p className="font-mono text-sm break-all">{fromEmail}</p>
</div>
<div>
<Label className="text-muted-foreground text-sm font-medium">
To
</Label>
<p className="font-mono text-sm break-all">
{toEmail || "No email address"}
</p>
</div>
{ccEmail && (
<div>
<Label className="text-muted-foreground text-sm font-medium">
CC
</Label>
<p className="font-mono text-sm break-all">{ccEmail}</p>
</div>
)}
{bccEmail && (
<div>
<Label className="text-muted-foreground text-sm font-medium">
BCC
</Label>
<p className="font-mono text-sm break-all">{bccEmail}</p>
</div>
)}
<div>
<Label className="text-muted-foreground text-sm font-medium">
Subject
</Label>
<p className="text-sm break-words">{subject || "No subject"}</p>
</div>
<Separator />
<div>
<Label className="text-muted-foreground text-sm font-medium">
Attachment
</Label>
<div className="flex items-center gap-2 text-sm">
<FileText className="h-3 w-3" />
<span>invoice-{invoice.invoiceNumber}.pdf</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{activeTab === "compose" && (
<Button
onClick={() => setActiveTab("preview")}
disabled={!subject.trim()}
className="w-full"
variant="outline"
>
<Eye className="mr-2 h-4 w-4" />
Preview Email
</Button>
)}
{activeTab === "preview" && (
<Button
onClick={() => setActiveTab("compose")}
variant="outline"
className="w-full"
>
<Edit3 className="mr-2 h-4 w-4" />
Edit Email
</Button>
)}
</CardContent>
</Card>
</div>
</div>
{/* Floating Action Bar */}
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
<Send className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
Send Invoice
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
Email invoice to {invoice.client?.name ?? "client"}
</p>
</div>
</div>
}
>
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/dashboard/invoices/${invoiceId}/view`)}
>
Cancel
</Button>
<Button
onClick={handleSendEmail}
disabled={!canSend || isSending}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
size="sm"
>
{isSending ? (
<>
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
<span className="hidden sm:inline">Sending...</span>
</>
) : (
<>
<Send className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Send Email</span>
</>
)}
</Button>
</FloatingActionBar>
</div>
);
}
+77 -26
View File
@@ -1,17 +1,14 @@
"use client";
import { useState } from "react";
import { notFound, useRouter, useParams } from "next/navigation";
import { DollarSign, Edit, Loader2, Trash2 } from "lucide-react";
import Link from "next/link";
import { api } from "~/trpc/react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { notFound, useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { Separator } from "~/components/ui/separator";
import { PageHeader } from "~/components/layout/page-header";
import { PDFDownloadButton } from "../_components/pdf-download-button";
import { SendInvoiceButton } from "../_components/send-invoice-button";
import { InvoiceDetailsSkeleton } from "../_components/invoice-details-skeleton";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogContent,
@@ -20,19 +17,26 @@ import {
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { toast } from "sonner";
import { Separator } from "~/components/ui/separator";
import {
getEffectiveInvoiceStatus,
isInvoiceOverdue,
} from "~/lib/invoice-status";
import { api } from "~/trpc/react";
import type { StoredInvoiceStatus } from "~/types/invoice";
import { InvoiceDetailsSkeleton } from "../_components/invoice-details-skeleton";
import { PDFDownloadButton } from "../_components/pdf-download-button";
import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button";
import {
AlertTriangle,
Building,
Edit,
Check,
FileText,
Mail,
MapPin,
Phone,
User,
AlertTriangle,
Check,
Trash2,
} from "lucide-react";
function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
@@ -42,8 +46,8 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
const { data: invoice, isLoading } = api.invoices.getById.useQuery({
id: invoiceId,
});
const utils = api.useUtils();
// Delete mutation
const deleteInvoice = api.invoices.delete.useMutation({
onSuccess: () => {
toast.success("Invoice deleted successfully");
@@ -54,10 +58,27 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
},
});
const updateStatus = api.invoices.updateStatus.useMutation({
onSuccess: (data) => {
toast.success(data.message);
void utils.invoices.getById.invalidate({ id: invoiceId });
},
onError: (error) => {
toast.error(error.message ?? "Failed to update invoice status");
},
});
const handleDelete = () => {
setDeleteDialogOpen(true);
};
const handleMarkAsPaid = () => {
updateStatus.mutate({
id: invoiceId,
status: "paid" as StoredInvoiceStatus,
});
};
const confirmDelete = () => {
deleteInvoice.mutate({ id: invoiceId });
};
@@ -88,17 +109,17 @@ function InvoiceViewContent({ 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";
const effectiveStatus = getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
const isOverdue = isInvoiceOverdue(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
const getStatusType = (): StatusType => {
if (invoice.status === "paid") return "paid";
if (invoice.status === "draft") return "draft";
if (invoice.status === "overdue") return "overdue";
if (invoice.status === "sent") {
return isOverdue ? "overdue" : "sent";
}
return "draft";
return effectiveStatus as StatusType;
};
return (
@@ -401,8 +422,38 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
<PDFDownloadButton invoiceId={invoice.id} className="w-full" />
)}
{invoice.status === "draft" && (
<SendInvoiceButton invoiceId={invoice.id} className="w-full" />
{/* Send Invoice Button - Show for draft, sent, and overdue */}
{effectiveStatus === "draft" && (
<EnhancedSendInvoiceButton
invoiceId={invoice.id}
className="w-full"
/>
)}
{(effectiveStatus === "sent" ||
effectiveStatus === "overdue") && (
<EnhancedSendInvoiceButton
invoiceId={invoice.id}
className="w-full"
showResend={true}
/>
)}
{/* Manual Status Updates */}
{(effectiveStatus === "sent" ||
effectiveStatus === "overdue") && (
<Button
onClick={handleMarkAsPaid}
disabled={updateStatus.isPending}
className="w-full bg-green-600 text-white hover:bg-green-700"
>
{updateStatus.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<DollarSign className="mr-2 h-4 w-4" />
)}
Mark as Paid
</Button>
)}
<Button
@@ -19,6 +19,8 @@ import {
import { Eye, Edit, Trash2 } from "lucide-react";
import { api } from "~/trpc/react";
import { toast } from "sonner";
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
import type { StoredInvoiceStatus } from "~/types/invoice";
// Type for invoice data
interface Invoice {
@@ -65,14 +67,10 @@ interface InvoicesDataTableProps {
}
const getStatusType = (invoice: Invoice): StatusType => {
if (invoice.status === "paid") return "paid";
if (invoice.status === "draft") return "draft";
if (invoice.status === "overdue") return "overdue";
if (invoice.status === "sent") {
const dueDate = new Date(invoice.dueDate);
return dueDate < new Date() ? "overdue" : "sent";
}
return "draft";
return getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
) as StatusType;
};
const formatDate = (date: Date) => {
+13 -14
View File
@@ -1,21 +1,20 @@
import { Suspense } from "react";
import Link from "next/link";
import { HydrateClient } from "~/trpc/server";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { PageHeader } from "~/components/layout/page-header";
import { CSVImportPage } from "~/components/csv-import-page";
import {
ArrowLeft,
Upload,
FileText,
Download,
CheckCircle,
AlertCircle,
Info,
ArrowLeft,
CheckCircle,
Download,
FileSpreadsheet,
FileText,
Info,
Upload,
} from "lucide-react";
import Link from "next/link";
import { CSVImportPage } from "~/components/csv-import-page";
import { PageHeader } from "~/components/layout/page-header";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { HydrateClient } from "~/trpc/server";
// File Upload Instructions Component
function FormatInstructions() {