04e0911e9f
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>
181 lines
6.9 KiB
HTML
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 & 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>
|