// ============================================ // Bewerbungs-Tracker - Client-side JavaScript // ============================================ // DOM Elements const body = document.getElementById('body'); const darkModeToggle = document.getElementById('darkModeToggle'); const sunIcon = document.getElementById('sunIcon'); const moonIcon = document.getElementById('moonIcon'); // Modal Elements const settingsModal = document.getElementById('settingsModal'); const applicationModal = document.getElementById('applicationModal'); const deleteModal = document.getElementById('deleteModal'); const pdfExportModal = document.getElementById('pdfExportModal'); // Form Elements const settingsForm = document.getElementById('settingsForm'); const applicationForm = document.getElementById('applicationForm'); const pdfExportForm = document.getElementById('pdfExportForm'); const filterForm = document.getElementById('filterForm'); // Button Elements const settingsBtn = document.getElementById('settingsBtn'); const addApplicationBtn = document.getElementById('addApplicationBtn'); const exportPdfBtn = document.getElementById('exportPdfBtn'); // Close Modal Buttons const closeSettingsModal = document.getElementById('closeSettingsModal'); const closeApplicationModal = document.getElementById('closeApplicationModal'); const closeDeleteModal = document.getElementById('closeDeleteModal'); const closePdfModal = document.getElementById('closePdfModal'); const cancelSettings = document.getElementById('cancelSettings'); const cancelApplication = document.getElementById('cancelApplication'); const cancelDelete = document.getElementById('cancelDelete'); const cancelPdfExport = document.getElementById('cancelPdfExport'); const confirmDelete = document.getElementById('confirmDelete'); // Global variables let currentApplicationId = null; let currentDeleteId = null; let pdfLibrariesLoaded = false; // ============================================ // Dark Mode // ============================================ function initializeDarkMode() { // Check localStorage for dark mode preference const darkMode = localStorage.getItem('darkMode'); if (darkMode === 'enabled' || (!darkMode && window.matchMedia('(prefers-color-scheme: dark)').matches)) { body.classList.add('dark'); sunIcon.classList.add('hidden'); moonIcon.classList.remove('hidden'); } else { body.classList.remove('dark'); sunIcon.classList.remove('hidden'); moonIcon.classList.add('hidden'); } } function toggleDarkMode() { body.classList.toggle('dark'); sunIcon.classList.toggle('hidden'); moonIcon.classList.toggle('hidden'); // Save preference to localStorage if (body.classList.contains('dark')) { localStorage.setItem('darkMode', 'enabled'); } else { localStorage.setItem('darkMode', 'disabled'); } } // ============================================ // Modal Functions // ============================================ function showModal(modal) { modal.classList.remove('hidden'); body.style.overflow = 'hidden'; } function hideModal(modal) { modal.classList.add('hidden'); body.style.overflow = ''; } function resetApplicationForm() { applicationForm.reset(); document.getElementById('modalTitle').textContent = 'Bewerbung hinzufügen'; currentApplicationId = null; } // ============================================ // Settings Management // ============================================ function loadSettings() { fetch('/api/settings') .then(response => response.json()) .then(settings => { if (settings) { document.getElementById('userName').value = settings.name || ''; document.getElementById('userAddress').value = settings.adresse || ''; document.getElementById('customerNumber').value = settings.kundennummer || ''; } }) .catch(error => console.error('Error loading settings:', error)); } function saveSettings(event) { event.preventDefault(); const formData = new FormData(settingsForm); const settings = { name: formData.get('name'), adresse: formData.get('adresse'), kundennummer: formData.get('kundennummer') }; fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }) .then(response => response.json()) .then(data => { if (data.success) { hideModal(settingsModal); // Refresh page to update settings in header location.reload(); } }) .catch(error => console.error('Error saving settings:', error)); } // ============================================ // Application Management (CRUD) // ============================================ function openAddApplicationModal() { resetApplicationForm(); // Set default date to today const today = new Date().toISOString().split('T')[0]; document.getElementById('applicationDatum').value = today; showModal(applicationModal); } function openEditApplicationModal(id) { // Fetch application data fetch(`/api/bewerbungen/${id}`) .then(response => response.json()) .then(application => { currentApplicationId = application.id; document.getElementById('modalTitle').textContent = 'Bewerbung bearbeiten'; document.getElementById('applicationId').value = application.id; document.getElementById('applicationDatum').value = application.datum; document.getElementById('applicationFirma').value = application.firma; document.getElementById('applicationStelle').value = application.stelle; document.getElementById('applicationArt').value = application.art || ''; document.getElementById('applicationStatus').value = application.status || ''; document.getElementById('applicationNotizen').value = application.notizen || ''; showModal(applicationModal); }) .catch(error => console.error('Error loading application:', error)); } function saveApplication(event) { event.preventDefault(); const formData = new FormData(applicationForm); const application = { datum: formData.get('datum'), firma: formData.get('firma'), stelle: formData.get('stelle'), art: formData.get('art'), status: formData.get('status'), notizen: formData.get('notizen'), interne_notizen: formData.get('interne_notizen'), kommentar: formData.get('kommentar') }; let url = '/api/bewerbungen'; let method = 'POST'; if (currentApplicationId) { url = `/api/bewerbungen/${currentApplicationId}`; method = 'PUT'; } fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(application) }) .then(response => response.json()) .then(data => { if (data.success) { hideModal(applicationModal); resetApplicationForm(); // Refresh the page to show updated data location.reload(); } }) .catch(error => console.error('Error saving application:', error)); } function openDeleteModal(id) { currentDeleteId = id; showModal(deleteModal); } function deleteApplication() { if (!currentDeleteId) return; fetch(`/api/bewerbungen/${currentDeleteId}`, { method: 'DELETE' }) .then(response => response.json()) .then(data => { if (data.success) { hideModal(deleteModal); currentDeleteId = null; // Refresh the page location.reload(); } }) .catch(error => console.error('Error deleting application:', error)); } // ============================================ // PDF Export // ============================================ function openPdfExportModal() { // Load PDF libraries if not already loaded loadPdfLibraries().then(() => { showModal(pdfExportModal); }); } function generatePDF(event) { event.preventDefault(); const month = document.getElementById('pdfMonth').value; const year = document.getElementById('pdfYear').value; // Fetch data for PDF let url = '/api/export?'; const params = []; if (month) params.push(`month=${month}`); if (year) params.push(`year=${year}`); if (params.length > 0) { url += params.join('&') + '&'; } Promise.all([ fetch('/api/settings').then(res => res.json()), fetch(url).then(res => res.json()) ]) .then(([settings, applications]) => { generatePdfDocument(settings, applications, month, year); hideModal(pdfExportModal); }) .catch(error => console.error('Error generating PDF:', error)); } // PDF Generation with jsPDF function generatePdfDocument(settings, applications, month, year) { // Check if jsPDF is loaded if (typeof jsPDF === 'undefined') { console.error('jsPDF not loaded, please wait for libraries to load'); alert('Bitte warten Sie einen Moment und versuchen Sie es erneut.'); return; } const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; const monthName = month ? monthNames[parseInt(month) - 1] : 'alle'; const title = `Bewerbungsaktivitäten - ${monthName} ${year || 'Jahre'}`; const dateStr = new Date().toLocaleDateString('de-DE'); // Page geometry const pageWidth = doc.internal.pageSize.getWidth(); // 210 mm const pageHeight = doc.internal.pageSize.getHeight(); // 297 mm const margin = 15; const contentWidth = pageWidth - margin * 2; // 180 mm const bottomLimit = pageHeight - margin; let yPos = 20; // Start a new page if the next block would not fit function ensureSpace(needed) { if (yPos + needed > bottomLimit) { doc.addPage(); yPos = margin; } } // Header with user data doc.setFont('helvetica', 'bold'); doc.setFontSize(16); doc.text(title, pageWidth / 2, yPos, { align: 'center' }); yPos += 12; // User information doc.setFontSize(12); doc.setFont('helvetica', 'normal'); if (settings && settings.name) { doc.text(`Name: ${settings.name}`, margin, yPos); yPos += 7; } if (settings && settings.adresse) { doc.text(`Adresse: ${settings.adresse}`, margin, yPos); yPos += 7; } if (settings && settings.kundennummer) { doc.text(`Kundennummer: ${settings.kundennummer}`, margin, yPos); yPos += 7; } yPos += 5; // Summary text const totalApplications = applications.length; const summaryText = year ? `Im Monat ${monthName} ${year} habe ich mich insgesamt auf ${totalApplications} Stellen beworben.` : `Insgesamt habe ich mich auf ${totalApplications} Stellen beworben.`; doc.setFont('helvetica', 'bold'); doc.text(summaryText, pageWidth / 2, yPos, { align: 'center' }); yPos += 12; // Group entries by status (keep this order in sync with views/index.ejs) const statusOrder = ['Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch', 'Einstellung', 'Absage', 'Keine Rückmeldung']; const groups = {}; applications.forEach((app) => { const key = (app.status && app.status.trim()) ? app.status.trim() : 'Ohne Status'; (groups[key] = groups[key] || []).push(app); }); const orderedKeys = [ ...statusOrder.filter((s) => groups[s]), ...Object.keys(groups).filter((k) => !statusOrder.includes(k)) ]; // One block per application — no table, so the note (which documents the // full Verlauf) gets the entire page width and is shown completely. orderedKeys.forEach((statusKey) => { const group = groups[statusKey]; // Status group heading (dark bar), kept together with its first entry ensureSpace(40); doc.setFillColor(55, 65, 81); doc.rect(margin, yPos, contentWidth, 10, 'F'); doc.setFont('helvetica', 'bold'); doc.setFontSize(12); doc.setTextColor(255, 255, 255); doc.text(`${statusKey} (${group.length})`, margin + 3, yPos + 6.8); yPos += 14; doc.setTextColor(0, 0, 0); group.forEach((app) => { const datum = new Date(app.datum).toLocaleDateString('de-DE'); const firma = app.firma || '—'; const stelle = app.stelle || '—'; const art = app.art || '—'; const notizen = (app.notizen || '').trim(); // Keep the heading together with the start of its content ensureSpace(28); // Heading bar: Firma – Stelle (left), Datum (right) doc.setFillColor(225, 232, 240); doc.rect(margin, yPos, contentWidth, 9, 'F'); doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.setTextColor(20, 20, 20); const heading = doc.splitTextToSize(`${firma} – ${stelle}`, contentWidth - 40)[0]; doc.text(heading, margin + 2, yPos + 6); doc.text(datum, pageWidth - margin - 2, yPos + 6, { align: 'right' }); yPos += 9; // Meta line: Art doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(90, 90, 90); doc.text(`Art: ${art}`, margin + 2, yPos + 5); yPos += 9; // Status-Verlauf timeline (chronological status changes with comments) const verlauf = Array.isArray(app.verlauf) ? app.verlauf : []; if (verlauf.length) { doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(20, 20, 20); ensureSpace(6); doc.text('Status-Verlauf:', margin + 2, yPos + 4); yPos += 6; doc.setFontSize(9); verlauf.forEach((v) => { const vDatum = new Date(v.datum).toLocaleDateString('de-DE'); doc.setFont('helvetica', 'bold'); doc.setTextColor(50, 50, 50); ensureSpace(5); doc.text(`${vDatum} — ${v.status || ''}`, margin + 5, yPos + 4); yPos += 5; const kommentar = (v.kommentar || '').trim(); if (kommentar) { doc.setFont('helvetica', 'normal'); doc.setTextColor(0, 0, 0); kommentar.split(/\r?\n/).forEach((paragraph) => { const lines = doc.splitTextToSize(paragraph.length ? paragraph : ' ', contentWidth - 12); lines.forEach((line) => { ensureSpace(4.5); doc.text(line, margin + 9, yPos + 3.5); yPos += 4.5; }); }); } }); yPos += 3; } // Notizen label doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(20, 20, 20); doc.text('Notizen:', margin + 2, yPos + 4); yPos += 6; // Notizen body — full width, complete, with page breaks line by line doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.setTextColor(0, 0, 0); const lineHeight = 5; const noteText = notizen || '(keine Notizen)'; noteText.split(/\r?\n/).forEach((paragraph) => { const lines = doc.splitTextToSize(paragraph.length ? paragraph : ' ', contentWidth - 4); lines.forEach((line) => { ensureSpace(lineHeight); doc.text(line, margin + 2, yPos + 4); yPos += lineHeight; }); }); // Separator before the next entry yPos += 4; ensureSpace(6); doc.setDrawColor(210, 210, 210); doc.line(margin, yPos, pageWidth - margin, yPos); yPos += 8; }); // Extra spacing after a status group yPos += 4; }); // Footer with confirmation ensureSpace(20); yPos += 6; doc.setFont('helvetica', 'italic'); doc.setFontSize(10); doc.setTextColor(0, 0, 0); doc.text('Ich versichere, dass die oben genannten Angaben der Wahrheit entsprechen.', pageWidth / 2, yPos, { align: 'center' }); yPos += 7; doc.text(`Datum: ${dateStr}`, pageWidth / 2, yPos, { align: 'center' }); // Save the PDF const fileName = `Bewerbungsaktivitaeten_${monthName}_${year || 'alle'}.pdf`; doc.save(fileName); } // Load PDF libraries dynamically function loadPdfLibraries() { return new Promise((resolve) => { if (pdfLibrariesLoaded) { resolve(); return; } // Check if already loading if (document.getElementById('jspdf-script')) { const checkLoaded = setInterval(() => { if (typeof jsPDF !== 'undefined') { clearInterval(checkLoaded); pdfLibrariesLoaded = true; resolve(); } }, 100); return; } // Load jsPDF from CDN const script1 = document.createElement('script'); script1.id = 'jspdf-script'; script1.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; script1.onload = () => { const script2 = document.createElement('script'); script2.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js'; script2.onload = () => { pdfLibrariesLoaded = true; resolve(); }; document.head.appendChild(script2); }; document.head.appendChild(script1); }); } // ============================================ // Event Listeners // ============================================ // Dark Mode Toggle darkModeToggle.addEventListener('click', toggleDarkMode); // Settings Modal settingsBtn.addEventListener('click', () => { loadSettings(); showModal(settingsModal); }); closeSettingsModal.addEventListener('click', () => hideModal(settingsModal)); cancelSettings.addEventListener('click', () => hideModal(settingsModal)); settingsForm.addEventListener('submit', saveSettings); // Application Modal addApplicationBtn.addEventListener('click', openAddApplicationModal); closeApplicationModal.addEventListener('click', () => { hideModal(applicationModal); resetApplicationForm(); }); cancelApplication.addEventListener('click', () => { hideModal(applicationModal); resetApplicationForm(); }); applicationForm.addEventListener('submit', saveApplication); // Delete Modal closeDeleteModal.addEventListener('click', () => hideModal(deleteModal)); cancelDelete.addEventListener('click', () => hideModal(deleteModal)); confirmDelete.addEventListener('click', deleteApplication); // PDF Export Modal exportPdfBtn.addEventListener('click', openPdfExportModal); closePdfModal.addEventListener('click', () => hideModal(pdfExportModal)); cancelPdfExport.addEventListener('click', () => hideModal(pdfExportModal)); pdfExportForm.addEventListener('submit', generatePDF); // Filter Form document.getElementById('filterForm').addEventListener('submit', function(e) { e.preventDefault(); const month = document.getElementById('filterMonth').value; const year = document.getElementById('filterYear').value; let url = '/'; if (month || year) { url += '?'; if (month) url += `month=${month}&`; if (year) url += `year=${year}&`; } window.location.href = url; }); // Delete buttons (event delegation). Editing opens its own page (/bewerbung/:id). document.addEventListener('click', (e) => { if (e.target.closest('.delete-btn')) { const id = e.target.closest('.delete-btn').dataset.id; openDeleteModal(id); } }); // Close modals when clicking outside document.addEventListener('click', (e) => { if (e.target === settingsModal) hideModal(settingsModal); if (e.target === applicationModal) { hideModal(applicationModal); resetApplicationForm(); } if (e.target === deleteModal) hideModal(deleteModal); if (e.target === pdfExportModal) hideModal(pdfExportModal); }); // Close modals with Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (!settingsModal.classList.contains('hidden')) hideModal(settingsModal); if (!applicationModal.classList.contains('hidden')) { hideModal(applicationModal); resetApplicationForm(); } if (!deleteModal.classList.contains('hidden')) hideModal(deleteModal); if (!pdfExportModal.classList.contains('hidden')) hideModal(pdfExportModal); } }); // Set current year in footer document.getElementById('currentYear').textContent = new Date().getFullYear(); // ============================================ // Initialize // ============================================ // Initialize dark mode initializeDarkMode(); console.log('Bewerbungs-Tracker initialized');