Refactor: clients → companies schema v2

This commit is contained in:
Krao Hasanee
2026-03-26 23:42:06 -04:00
commit 719209fa25
61 changed files with 8192 additions and 0 deletions
+8
View File
@@ -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);
}
+180
View File
@@ -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`);
}
+8
View File
@@ -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);