Files
fourge-portal/scripts/generate-task-pdf.html
Krao Hasanee 04e0911e9f 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>
2026-06-08 12:59:11 -04:00

181 lines
6.9 KiB
HTML

<!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 &amp; 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>