Files
jobbi-bewerbung/public/js/main.js
T
thomas c65c9f1751 Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 18:15:11 +02:00

268 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 = '<span style="opacity:.7">Wird erstellt …</span>';
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;
}