fix: crash bugs in TeamInvoices + faster load
Bug fixes: - TeamInvoices: add useAuth import/currentUser (invoice-create crash) - TeamInvoices: setChartYear -> setExportYear (year-dropdown crash) - TeamDashboard: drop redundant setState-in-effect (cascading renders) - Companies: delete dead _UnusedClientCompanies (illegal hook calls) - annotate intentional empty PDF-fallback catches Load speed: - preconnect/dns-prefetch to Supabase origin - lazy-load heic-to in Converters: page chunk 2737KB -> 9KB - split recharts into its own 'charts' vendor chunk Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Task Tracker PDF Generator</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, sans-serif; background: #111; color: #fff; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; flex-direction: column; gap: 16px; }
|
||||
button { background: #F5A523; color: #000; border: none; border-radius: 8px; padding: 12px 28px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
||||
button:disabled { opacity: 0.5; cursor: default; }
|
||||
#status { font-size: 13px; color: #888; }
|
||||
select { background: #222; color: #fff; border: 1px solid #333; border-radius: 8px; padding: 8px 12px; font-size: 13px; min-width: 220px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="font-size:22px;font-weight:700;letter-spacing:-0.5px">Task Tracker PDF</div>
|
||||
<select id="companyFilter"><option value="">All Companies</option></select>
|
||||
<button id="btn" onclick="generate()">Generate & Download PDF</button>
|
||||
<div id="status">Ready</div>
|
||||
|
||||
<script>
|
||||
const SUPABASE_URL = 'https://fqflxxqvennhvoeywrdw.supabase.co';
|
||||
const SUPABASE_ANON_KEY = 'sb_publishable_qNNIKtnu1dUIVKelq9aYYQ_TfHgzhyR';
|
||||
|
||||
const { createClient } = supabase;
|
||||
const sb = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||
|
||||
const STATUS_LABELS = {
|
||||
not_started: 'Not Started',
|
||||
in_progress: 'In Progress',
|
||||
on_hold: 'On Hold',
|
||||
client_review: 'In Review',
|
||||
client_approved: 'Approved',
|
||||
invoiced: 'Invoiced',
|
||||
paid: 'Paid',
|
||||
};
|
||||
|
||||
const versionLabel = (v) => `R${String(v ?? 0).padStart(2, '0')}`;
|
||||
const isInvoiced = (t) => t.invoiced || t.status === 'invoiced' || t.status === 'paid';
|
||||
|
||||
// Populate company dropdown on load
|
||||
(async () => {
|
||||
const { data } = await sb
|
||||
.from('tasks')
|
||||
.select('project:projects(company:companies(id, name))')
|
||||
.limit(1000);
|
||||
|
||||
const seen = new Map();
|
||||
for (const t of data || []) {
|
||||
const c = t.project?.company;
|
||||
if (c && !seen.has(c.id)) seen.set(c.id, c);
|
||||
}
|
||||
const sel = document.getElementById('companyFilter');
|
||||
[...seen.values()]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.forEach(c => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c.id;
|
||||
opt.textContent = c.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
})();
|
||||
|
||||
async function generate() {
|
||||
const btn = document.getElementById('btn');
|
||||
const status = document.getElementById('status');
|
||||
const selectedCompany = document.getElementById('companyFilter').value;
|
||||
const selectedCompanyName = document.getElementById('companyFilter').selectedOptions[0]?.text || 'All Companies';
|
||||
|
||||
btn.disabled = true;
|
||||
status.textContent = 'Fetching data from Supabase…';
|
||||
|
||||
try {
|
||||
const { data, error } = await sb
|
||||
.from('tasks')
|
||||
.select('id, title, status, current_version, invoiced, submitted_at, project:projects(id, name, company:companies(id, name))')
|
||||
.order('submitted_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
let rows = data || [];
|
||||
if (selectedCompany) {
|
||||
rows = rows.filter(t => t.project?.company?.id === selectedCompany);
|
||||
}
|
||||
|
||||
status.textContent = `Building PDF for ${rows.length} tasks…`;
|
||||
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'letter' });
|
||||
|
||||
// Header
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(18);
|
||||
doc.setTextColor(245, 165, 35);
|
||||
doc.text('Fourge Branding', 40, 48);
|
||||
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(120, 120, 120);
|
||||
doc.text('Task Tracker', 40, 64);
|
||||
|
||||
doc.setFontSize(9);
|
||||
const now = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
doc.text(`Generated ${now} · ${selectedCompanyName} · ${rows.length} tasks`, 40, 78);
|
||||
|
||||
// Table
|
||||
doc.autoTable({
|
||||
startY: 96,
|
||||
head: [['TASK NAME', 'VERSION', 'STATUS', 'INVOICED']],
|
||||
body: rows.map(t => [
|
||||
[
|
||||
t.title || '—',
|
||||
t.project?.company?.name && t.project?.name
|
||||
? `${t.project.company.name} · ${t.project.name}`
|
||||
: (t.project?.name || ''),
|
||||
],
|
||||
versionLabel(t.current_version),
|
||||
STATUS_LABELS[t.status] || t.status || '—',
|
||||
isInvoiced(t) ? 'Yes' : '—',
|
||||
]),
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: { top: 5, right: 8, bottom: 5, left: 8 },
|
||||
overflow: 'linebreak',
|
||||
textColor: [30, 30, 30],
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [245, 165, 35],
|
||||
textColor: [0, 0, 0],
|
||||
fontStyle: 'bold',
|
||||
fontSize: 8,
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [248, 248, 248],
|
||||
},
|
||||
columnStyles: {
|
||||
0: { cellWidth: 220 },
|
||||
1: { cellWidth: 55, halign: 'center' },
|
||||
2: { cellWidth: 90 },
|
||||
3: { cellWidth: 60, halign: 'center' },
|
||||
},
|
||||
willDrawCell(data) {
|
||||
if (data.column.index === 0 && data.section === 'body') {
|
||||
const [title, sub] = Array.isArray(data.cell.raw) ? data.cell.raw : [data.cell.raw, ''];
|
||||
const x = data.cell.x + 8;
|
||||
let y = data.cell.y + 13;
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(30, 30, 30);
|
||||
doc.text(String(title), x, y, { maxWidth: 204 });
|
||||
if (sub) {
|
||||
const titleLines = doc.splitTextToSize(String(title), 204).length;
|
||||
y += titleLines * 11;
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(140, 140, 140);
|
||||
doc.text(String(sub), x, y, { maxWidth: 204 });
|
||||
}
|
||||
data.cell.text = [];
|
||||
}
|
||||
},
|
||||
margin: { left: 40, right: 40 },
|
||||
});
|
||||
|
||||
const slug = selectedCompany ? selectedCompanyName.replace(/\s+/g, '-').toLowerCase() : 'all';
|
||||
const filename = `task-tracker-${slug}-${new Date().toISOString().slice(0, 10)}.pdf`;
|
||||
doc.save(filename);
|
||||
status.textContent = `Downloaded: ${filename}`;
|
||||
} catch (err) {
|
||||
status.textContent = `Error: ${err.message}`;
|
||||
console.error(err);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user