c65c9f1751
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
268 lines
9.9 KiB
JavaScript
268 lines
9.9 KiB
JavaScript
'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;
|
||
}
|