Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 18 additions & 182 deletions apps/web/app/(dashboard)/invoices/new/new-invoice-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState, useMemo, useRef, useCallback, useEffect } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { getSavedLineItems, type SavedLineItemData } from '@/lib/saved-items/actions';
import {
Expand Down Expand Up @@ -336,10 +336,6 @@ export function NewInvoiceForm({
const [previewTab, setPreviewTab] = useState<PreviewTab>('payment');
const [showPreviewDetails, setShowPreviewDetails] = useState(true);
const [templateName, setTemplateName] = useState<TemplateName>('oreko');
const [pdfGenerating, setPdfGenerating] = useState(false);

// Refs
const pdfRef = useRef<HTMLDivElement>(null);

// Pre-fill from quote conversion
useEffect(() => {
Expand Down Expand Up @@ -508,33 +504,6 @@ export function NewInvoiceForm({
}
};

// ─── PDF Download Handler ─────────────────────────────
const handleDownloadPdf = useCallback(async () => {
if (!pdfRef.current) return;
setPdfGenerating(true);
try {
const html2canvas = (await import('html2canvas')).default;
const jsPDF = (await import('jspdf')).default;

const canvas = await html2canvas(pdfRef.current, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
width: 595,
height: 842,
});

const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });
pdf.addImage(imgData, 'PNG', 0, 0, 595, 842);
pdf.save(`Invoice-${invoiceNumber}.pdf`);
} catch (err) {
console.error('PDF generation failed:', err);
toast.error('Failed to generate PDF. Please try again.');
} finally {
setPdfGenerating(false);
}
}, [invoiceNumber]);

return (
<div className="flex flex-col h-[calc(100vh-64px)]">
Expand Down Expand Up @@ -1678,29 +1647,22 @@ export function NewInvoiceForm({
</>
)}

{/* ─── Download Button ─── */}
{/* ─── Download Button (disabled during creation) ─── */}
<div className="px-6 pb-6 pt-2">
<button
onClick={handleDownloadPdf}
disabled={pdfGenerating}
disabled
className={cn(
'w-full h-12 rounded-lg font-medium text-sm flex items-center justify-center gap-2 transition-colors',
tpl.buttonColor,
pdfGenerating && 'opacity-70 cursor-not-allowed'
'bg-muted text-muted-foreground cursor-not-allowed opacity-60'
)}
title="Save the invoice first to download PDF"
>
{pdfGenerating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Generating...
</>
) : (
<>
<Download className="h-4 w-4" />
Download Invoice
</>
)}
<Download className="h-4 w-4" />
Download Invoice
</button>
<p className="text-[11px] text-muted-foreground/70 text-center mt-1.5">
Available after saving
</p>
</div>

{/* ─── Footer ─── */}
Expand Down Expand Up @@ -1736,21 +1698,13 @@ export function NewInvoiceForm({
}}
className="bg-white shadow-2xl rounded-sm border border-border/40 flex-shrink-0 relative"
>
{/* Download floating button */}
{/* Download floating button (disabled during creation) */}
<button
onClick={handleDownloadPdf}
disabled={pdfGenerating}
className={cn(
'absolute top-2.5 right-2.5 h-7 w-7 rounded-full bg-muted/80 hover:bg-muted transition-colors flex items-center justify-center z-20',
pdfGenerating && 'opacity-70 cursor-not-allowed'
)}
title="Download PDF"
disabled
className="absolute top-2.5 right-2.5 h-7 w-7 rounded-full bg-muted/80 flex items-center justify-center z-20 opacity-50 cursor-not-allowed"
title="Save the invoice first to download PDF"
>
{pdfGenerating ? (
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
) : (
<Download className="h-3.5 w-3.5 text-muted-foreground" />
)}
<Download className="h-3.5 w-3.5 text-muted-foreground" />
</button>

{/* A4 Page Content */}
Expand Down Expand Up @@ -1897,7 +1851,9 @@ export function NewInvoiceForm({
Pay this Invoice
</button>
<button
className="flex-1 h-11 rounded-lg font-medium text-sm flex items-center justify-center gap-2 border border-border hover:bg-muted transition-colors"
disabled
className="flex-1 h-11 rounded-lg font-medium text-sm flex items-center justify-center gap-2 border border-border opacity-50 cursor-not-allowed"
title="Save the invoice first to download PDF"
>
Download PDF
</button>
Expand Down Expand Up @@ -1980,126 +1936,6 @@ export function NewInvoiceForm({
</div>
)}

{/* ═══ HIDDEN A4 RENDER DIV (for PDF capture) ═══ */}
<div
ref={pdfRef}
className="fixed"
style={{
left: '-9999px',
top: 0,
width: '595px',
height: '842px',
background: '#ffffff',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#111111',
zIndex: -1,
}}
>
{/* Top accent bar */}
{tpl.topBorder && (
<div style={{ width: '100%', height: '4px', background: tpl.topBorder }} />
)}

{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '32px 40px 24px' }}>
<div>
<p style={{ fontSize: '18px', fontWeight: 700 }}>{businessName}</p>
<p style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>hello@company.com</p>
</div>
<div style={{ textAlign: 'right' }}>
<p style={{ fontSize: '22px', fontWeight: 700, color: tpl.accent, letterSpacing: '0.05em' }}>INVOICE</p>
<p style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>#{invoiceNumber}</p>
<p style={{ fontSize: '11px', color: '#666' }}>
Date: {dueDate ? format(dueDate, 'MMM dd, yyyy') : 'Not set'}
</p>
</div>
</div>

{/* Bill To */}
<div style={{ padding: '0 40px 24px' }}>
<p style={{ fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, color: '#999', marginBottom: '4px' }}>Bill To</p>
<p style={{ fontSize: '13px', fontWeight: 500 }}>
{selectedClient?.name || 'Customer Name'}
</p>
{selectedClient?.company && (
<p style={{ fontSize: '11px', color: '#666' }}>{selectedClient.company}</p>
)}
</div>

{/* Items Table */}
<div style={{ padding: '0 40px' }}>
<table style={{ width: '100%', fontSize: '11px', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '2px solid #e5e7eb' }}>
<th style={{ textAlign: 'left', padding: '8px 0', fontWeight: 600, color: '#333' }}>Description</th>
<th style={{ textAlign: 'center', padding: '8px 0', fontWeight: 600, color: '#333' }}>Qty</th>
<th style={{ textAlign: 'right', padding: '8px 0', fontWeight: 600, color: '#333' }}>Rate</th>
<th style={{ textAlign: 'right', padding: '8px 0', fontWeight: 600, color: '#333' }}>Amount</th>
</tr>
</thead>
<tbody>
{lineItems.length > 0 ? lineItems.map((item) => (
<tr key={item.id} style={{ borderBottom: '1px solid #f3f4f6' }}>
<td style={{ padding: '8px 0' }}>
<p style={{ fontWeight: 500 }}>{item.name || 'Untitled'}</p>
{item.description && <p style={{ color: '#888', marginTop: '2px' }}>{item.description}</p>}
</td>
<td style={{ textAlign: 'center', padding: '8px 0', color: '#333' }}>{item.quantity}</td>
<td style={{ textAlign: 'right', padding: '8px 0', color: '#333' }}>{formatMoney(item.rate, selectedCurrency)}</td>
<td style={{ textAlign: 'right', padding: '8px 0', fontWeight: 500 }}>{formatMoney(item.quantity * item.rate, selectedCurrency)}</td>
</tr>
)) : (
<tr>
<td colSpan={4} style={{ padding: '24px 0', textAlign: 'center', color: '#999' }}>
No items added
</td>
</tr>
)}
</tbody>
</table>
</div>

{/* Totals */}
<div style={{ padding: '16px 40px', marginTop: 'auto' }}>
<div style={{ marginLeft: 'auto', width: '200px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: '11px', color: '#666' }}>
<span>Subtotal</span>
<span>{formatMoney(subtotal, selectedCurrency)}</span>
</div>
{discountAmount > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: '11px', color: '#22c55e' }}>
<span>Discount</span>
<span>-{formatMoney(discountAmount, selectedCurrency)}</span>
</div>
)}
{taxAmount > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: '11px', color: '#666' }}>
<span>Tax ({parsedTaxPercent}%)</span>
<span>{formatMoney(taxAmount, selectedCurrency)}</span>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: '11px', borderTop: '1px solid #e5e7eb', color: '#333', fontWeight: 500 }}>
<span>Total</span>
<span>{formatMoney(total, selectedCurrency)}</span>
</div>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
padding: '8px', marginTop: '4px', borderRadius: '4px',
background: `${tpl.accent}15`,
}}>
<span style={{ fontSize: '11px', fontWeight: 700 }}>Balance Due</span>
<span style={{ fontSize: '14px', fontWeight: 700, color: tpl.accent }}>{formatMoney(total, selectedCurrency)}</span>
</div>
</div>
</div>

{/* Notes */}
{notes && (
<div style={{ padding: '0 40px 16px' }}>
<p style={{ fontSize: '10px', color: '#999' }}>{notes}</p>
</div>
)}
</div>
</div>
</div>
</div>
Expand Down
Loading
Loading