Refactor: clients → companies schema v2
This commit is contained in:
Executable
+8
@@ -0,0 +1,8 @@
|
||||
import { supabase } from './supabase';
|
||||
|
||||
export async function sendEmail(type, to, data) {
|
||||
const { error } = await supabase.functions.invoke('send-email', {
|
||||
body: { type, to, data },
|
||||
});
|
||||
if (error) console.error('Email error:', error);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateInvoicePDF(invoice, company, items) {
|
||||
const client = company; // alias for PDF layout vars below
|
||||
const doc = new jsPDF();
|
||||
const pageWidth = doc.internal.pageSize.width;
|
||||
|
||||
// Load logo first to know its height
|
||||
const logo = await loadImage('/fourge-logo.png');
|
||||
const logoW = 40;
|
||||
const logoH = logo ? (logoW / (logo.naturalWidth / logo.naturalHeight)) : 8;
|
||||
const headerH = logoH + 12; // tight padding around logo
|
||||
|
||||
// Black header band — sized to logo
|
||||
doc.setFillColor(20, 20, 20);
|
||||
doc.rect(0, 0, pageWidth, headerH, 'F');
|
||||
|
||||
// Logo
|
||||
if (logo) {
|
||||
doc.addImage(logo, 'PNG', 14, 6, logoW, logoH);
|
||||
} else {
|
||||
doc.setFontSize(13);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text('FOURGE BRANDING', 14, headerH / 2 + 4);
|
||||
}
|
||||
|
||||
// Contact info in header (right, small)
|
||||
doc.setFontSize(7);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(160, 160, 160);
|
||||
doc.text('1.855.FOURGE4 · 1855.368.7434 | hello@fourgebranding.com | www.fourgebranding.com', pageWidth - 14, headerH / 2 + 2, { align: 'right' });
|
||||
|
||||
// INVOICE title + number (below header, left-right)
|
||||
const afterHeader = headerH + 10;
|
||||
doc.setFontSize(20);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(30, 30, 30);
|
||||
doc.text('INVOICE', 14, afterHeader + 8);
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(100, 100, 100);
|
||||
doc.text(invoice.invoice_number, pageWidth - 14, afterHeader + 4, { align: 'right' });
|
||||
|
||||
// Invoice details (right column)
|
||||
const details = [
|
||||
['Date', new Date(invoice.invoice_date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })],
|
||||
['Due', new Date(invoice.due_date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })],
|
||||
['Terms', 'Net 30'],
|
||||
];
|
||||
let dy = afterHeader + 10;
|
||||
details.forEach(([label, val]) => {
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(130, 130, 130);
|
||||
doc.text(label, pageWidth - 60, dy);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(40, 40, 40);
|
||||
doc.text(val, pageWidth - 14, dy, { align: 'right' });
|
||||
dy += 6;
|
||||
});
|
||||
|
||||
// Status
|
||||
const statusColors = { draft: [150, 150, 150], sent: [37, 99, 235], paid: [22, 163, 74] };
|
||||
const sc = statusColors[invoice.status] || [150, 150, 150];
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(...sc);
|
||||
doc.text(invoice.status.toUpperCase(), pageWidth - 14, dy, { align: 'right' });
|
||||
|
||||
// Bill To (left column, same zone)
|
||||
const billStartY = afterHeader + 18;
|
||||
doc.setFontSize(7);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(150, 150, 150);
|
||||
doc.text('BILL TO', 14, billStartY);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(30, 30, 30);
|
||||
doc.text(client.name, 14, billStartY + 6);
|
||||
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(80, 80, 80);
|
||||
let billY = billStartY + 6;
|
||||
if (client.email) { billY += 5; doc.text(client.email, 14, billY); }
|
||||
|
||||
// Divider
|
||||
const tableStart = Math.max(billY, dy) + 8;
|
||||
doc.setDrawColor(220, 220, 220);
|
||||
doc.setLineWidth(0.4);
|
||||
doc.line(14, tableStart, pageWidth - 14, tableStart);
|
||||
|
||||
// Line items table — tight
|
||||
autoTable(doc, {
|
||||
startY: tableStart + 2,
|
||||
head: [['Description', 'Qty', 'Unit Price', 'Total']],
|
||||
body: items.map(item => [
|
||||
item.description,
|
||||
Number(item.quantity).toString(),
|
||||
`$${Number(item.unit_price).toFixed(2)}`,
|
||||
`$${(Number(item.quantity) * Number(item.unit_price)).toFixed(2)}`,
|
||||
]),
|
||||
headStyles: {
|
||||
fillColor: [30, 30, 30],
|
||||
textColor: [255, 255, 255],
|
||||
fontStyle: 'bold',
|
||||
fontSize: 8,
|
||||
cellPadding: 4,
|
||||
},
|
||||
bodyStyles: {
|
||||
fontSize: 9,
|
||||
textColor: [40, 40, 40],
|
||||
cellPadding: 4,
|
||||
fillColor: [255, 255, 255],
|
||||
},
|
||||
alternateRowStyles: { fillColor: [249, 249, 249] },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 'auto' },
|
||||
1: { cellWidth: 14, halign: 'center' },
|
||||
2: { cellWidth: 30, halign: 'right' },
|
||||
3: { cellWidth: 30, halign: 'right', fontStyle: 'bold' },
|
||||
},
|
||||
margin: { left: 14, right: 14 },
|
||||
theme: 'striped',
|
||||
tableLineColor: [225, 225, 225],
|
||||
tableLineWidth: 0.2,
|
||||
});
|
||||
|
||||
const finalY = doc.lastAutoTable.finalY;
|
||||
|
||||
// Total box
|
||||
doc.setFillColor(20, 20, 20);
|
||||
doc.rect(pageWidth - 70, finalY + 4, 56, 11, 'F');
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text('TOTAL', pageWidth - 66, finalY + 11);
|
||||
doc.setFontSize(9);
|
||||
doc.text(`$${Number(invoice.total).toFixed(2)}`, pageWidth - 16, finalY + 11, { align: 'right' });
|
||||
|
||||
// Notes
|
||||
if (invoice.notes) {
|
||||
const notesY = finalY + 22;
|
||||
doc.setFontSize(7);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(150, 150, 150);
|
||||
doc.text('NOTES', 14, notesY);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(80, 80, 80);
|
||||
const split = doc.splitTextToSize(invoice.notes, pageWidth / 2);
|
||||
doc.text(split, 14, notesY + 5);
|
||||
}
|
||||
|
||||
// Footer
|
||||
const footerY = doc.internal.pageSize.height - 14;
|
||||
doc.setDrawColor(220, 220, 220);
|
||||
doc.setLineWidth(0.4);
|
||||
doc.line(14, footerY - 4, pageWidth - 14, footerY - 4);
|
||||
doc.setFontSize(7);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(160, 160, 160);
|
||||
doc.text('Payment due within 30 days of invoice date. Thank you for your business!', pageWidth / 2, footerY, { align: 'center' });
|
||||
|
||||
doc.save(`${invoice.invoice_number}.pdf`);
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const url = import.meta.env.VITE_SUPABASE_URL;
|
||||
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!url || !key) throw new Error('Missing Supabase environment variables. Check VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.');
|
||||
|
||||
export const supabase = createClient(url, key);
|
||||
Reference in New Issue
Block a user