'use strict'; // ── Dark mode toggle ────────────────────────────────────────────────────────── document.getElementById('darkModeToggle').addEventListener('click', () => { const isDark = document.documentElement.classList.toggle('dark'); localStorage.setItem('darkMode', isDark ? 'true' : 'false'); }); // ── Stat counter animation ──────────────────────────────────────────────────── (function animateStats() { const DURATION = 700; document.querySelectorAll('.stat-value[data-count]').forEach((el, i) => { const target = parseInt(el.dataset.count, 10) || 0; if (target === 0) return; const delay = 80 + i * 60; setTimeout(() => { const start = performance.now(); const tick = (now) => { const p = Math.min((now - start) / DURATION, 1); // ease-out expo const eased = p === 1 ? 1 : 1 - Math.pow(2, -10 * p); el.textContent = Math.round(eased * target); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, delay); }); })(); // ── Load bewerbungen from embedded JSON ─────────────────────────────────────── let BEWERBUNGEN = []; const dataEl = document.getElementById('bewerbungenData'); if (dataEl) { try { BEWERBUNGEN = JSON.parse(dataEl.textContent); } catch (_) {} } // ── Modal system ────────────────────────────────────────────────────────────── function openModal(id) { const el = document.getElementById(id); el.classList.add('is-open'); document.body.style.overflow = 'hidden'; setTimeout(() => { const first = el.querySelector( 'input[type="date"], input[type="text"]:not([type="hidden"]), select, textarea' ); if (first) first.focus(); }, 60); } function closeModal(id) { const el = document.getElementById(id); el.classList.remove('is-open'); document.body.style.overflow = ''; } function closeOnBackdrop(event, id) { if (event.target === event.currentTarget) closeModal(id); } document.addEventListener('keydown', (e) => { if (e.key !== 'Escape') return; ['bewerbungModal', 'deleteModal'].forEach(id => { const el = document.getElementById(id); if (el && el.classList.contains('is-open')) closeModal(id); }); }); // ── Add modal ───────────────────────────────────────────────────────────────── function openAddModal() { const form = document.getElementById('bewerbungForm'); form.reset(); form.action = '/bewerbungen'; document.getElementById('modalTitle').textContent = 'Neue Bewerbung'; document.getElementById('datum').value = todayISO(); openModal('bewerbungModal'); } // ── Edit modal ──────────────────────────────────────────────────────────────── function openEditModal(id) { const b = BEWERBUNGEN.find(x => x.id === id); if (!b) return; const form = document.getElementById('bewerbungForm'); form.action = `/bewerbungen/${id}?_method=PUT`; document.getElementById('modalTitle').textContent = 'Bewerbung bearbeiten'; document.getElementById('datum').value = b.datum || ''; document.getElementById('firma').value = b.firma || ''; document.getElementById('stelle').value = b.stelle || ''; document.getElementById('art').value = b.art || ''; document.getElementById('status').value = b.status || ''; document.getElementById('notizen').value = b.notizen || ''; openModal('bewerbungModal'); } // ── Delete modal ────────────────────────────────────────────────────────────── function openDeleteModal(id, firma, stelle) { document.getElementById('deleteInfo').textContent = `${firma} – ${stelle}`; document.getElementById('deleteForm').action = `/bewerbungen/${id}?_method=DELETE`; openModal('deleteModal'); } // ── PDF generation ──────────────────────────────────────────────────────────── async function generatePDF(monat, jahr) { const btn = event.currentTarget; const saved = btn.innerHTML; btn.disabled = true; btn.innerHTML = 'Wird erstellt …'; try { const res = await fetch(`/api/pdf-daten?monat=${monat}&jahr=${jahr}`); if (!res.ok) throw new Error(`Server ${res.status}`); buildPDF(await res.json(), monat, jahr); } catch (err) { alert('PDF-Erstellung fehlgeschlagen:\n' + err.message); } finally { btn.disabled = false; btn.innerHTML = saved; } } function buildPDF({ bewerbungen, settings }, monat, jahr) { const { jsPDF } = window.jspdf; const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); const PW = doc.internal.pageSize.getWidth(); // 210 mm const PH = doc.internal.pageSize.getHeight(); // 297 mm const monatName = MONATE_DE[monat - 1]; const count = bewerbungen.length; // Accent stripe doc.setFillColor(224, 123, 0); doc.rect(0, 0, PW, 2.5, 'F'); // ── User info (top right) ────────────────────────────────────────────────── doc.setFontSize(9); doc.setTextColor(120, 120, 120); let uy = 10; const ux = PW - 12; if (settings?.name) { doc.setFont('helvetica', 'bold'); doc.text(settings.name, ux, uy, { align: 'right' }); uy += 5; doc.setFont('helvetica', 'normal'); } if (settings?.adresse) { settings.adresse.split('\n').forEach(line => { doc.text(line.trim(), ux, uy, { align: 'right' }); uy += 4.5; }); } if (settings?.kundennummer) { uy += 1; doc.text(`Kundennr.: ${settings.kundennummer}`, ux, uy, { align: 'right' }); } // ── Title ────────────────────────────────────────────────────────────────── doc.setFontSize(17); doc.setFont('helvetica', 'bold'); doc.setTextColor(24, 32, 47); const titleText = `Bewerbungsaktivitäten – ${monatName} ${jahr}`; doc.text(titleText, 12, 14); // Divider — only under the title, never touching the address block on the right const titleWidth = doc.getTextWidth(titleText); doc.setDrawColor(220, 226, 237); doc.setLineWidth(0.35); doc.line(12, 18.5, 12 + titleWidth + 6, 18.5); // ── Summary ──────────────────────────────────────────────────────────────── doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.setTextColor(74, 85, 107); const pl = count !== 1 ? 'n' : ''; doc.text( `Im Monat ${monatName} ${jahr} habe ich mich insgesamt auf ${count} Stelle${pl} beworben.`, 12, 25 ); // ── Table ────────────────────────────────────────────────────────────────── doc.autoTable({ startY: 31, head: [['Datum', 'Firma / Unternehmen', 'Stelle / Position', 'Art', 'Status', 'Notizen']], body: bewerbungen.map(b => [ formatDateDE(b.datum), b.firma || '', b.stelle || '', b.art || '', b.status || '', b.notizen ? b.notizen.slice(0, 120) : '' ]), margin: { left: 12, right: 12 }, styles: { fontSize: 7, cellPadding: 2.8, textColor: [24, 32, 47], lineColor: [221, 226, 239], lineWidth: 0.25, font: 'helvetica' }, headStyles: { fillColor: [224, 123, 0], textColor: [255, 255, 255], fontStyle: 'bold', halign: 'left', fontSize: 7 }, alternateRowStyles: { fillColor: [248, 250, 254] }, columnStyles: { 0: { cellWidth: 26, overflow: 'hidden' }, 1: { cellWidth: 38 }, 2: { cellWidth: 38 }, 3: { cellWidth: 28 }, 4: { cellWidth: 32 }, 5: { cellWidth: 'auto' } }, didDrawPage: ({ pageNumber }) => { doc.setFontSize(7.5); doc.setTextColor(160, 170, 185); doc.text(`Seite ${pageNumber}`, PW / 2, PH - 4.5, { align: 'center' }); } }); // ── Declaration line ─────────────────────────────────────────────────────── const finalY = doc.lastAutoTable.finalY; const declY = finalY + 10; doc.setFontSize(8.5); doc.setTextColor(100, 110, 130); doc.setFont('helvetica', 'italic'); doc.text( 'Ich erkläre hiermit, dass die vorstehenden Angaben vollständig und wahrheitsgemäß sind.', 12, declY ); doc.save(`Bewerbungen_${monatName}_${jahr}.pdf`); } // ── Utilities ───────────────────────────────────────────────────────────────── function todayISO() { const d = new Date(); return [ d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0') ].join('-'); } function formatDateDE(s) { if (!s) return ''; const p = s.split('-'); return p.length === 3 ? `${p[2]}.${p[1]}.${p[0]}` : s; }