Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 18:15:11 +02:00
commit c65c9f1751
19 changed files with 3633 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
'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;
}