Files
jobbi-bewerbung/public/js/main.js
T
thomas c2a629e2c0 Refactor UI/views, rework Docker build, untrack local data
- Views umstrukturiert: einstellungen.ejs -> bewerbung.ejs, neues
  partials/head.ejs, header/footer/index angepasst
- CSS umbenannt: style.css -> styles.css
- server.js und public/js/main.js ueberarbeitet
- Dockerfile auf schlankes Multi-Stage-Setup umgestellt;
  docker-compose.yml und .dockerignore entfernt
- npm-Scripts docker:build/push/deploy ergaenzt
- SQLite-DB und .idea aus Git entfernt und via .gitignore ignoriert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 04:01:37 +02:00

625 lines
21 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.
// ============================================
// 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');