PDF View i Adminbereich
This commit is contained in:
@@ -304,8 +304,257 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PDF-Archiv (Timesheets)
|
||||
const pdfYearSelect = document.getElementById('pdfYearSelect');
|
||||
const pdfUserSelect = document.getElementById('pdfUserSelect');
|
||||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||||
const pdfRefreshBtn = document.getElementById('pdfRefreshBtn');
|
||||
|
||||
if (pdfYearSelect && pdfUserSelect && pdfFilesContainer) {
|
||||
if (pdfRefreshBtn) {
|
||||
pdfRefreshBtn.addEventListener('click', function() {
|
||||
clearPdfPreview();
|
||||
loadPdfYears();
|
||||
});
|
||||
}
|
||||
|
||||
pdfYearSelect.addEventListener('change', function() {
|
||||
clearPdfPreview();
|
||||
const year = this.value;
|
||||
if (year) loadPdfUsers(year);
|
||||
});
|
||||
|
||||
pdfUserSelect.addEventListener('change', function() {
|
||||
clearPdfPreview();
|
||||
const year = pdfYearSelect.value;
|
||||
const userId = this.value;
|
||||
if (year && userId) loadPdfFiles(year, userId);
|
||||
});
|
||||
|
||||
loadPdfYears();
|
||||
}
|
||||
});
|
||||
|
||||
function setPdfStatus(text, color) {
|
||||
const el = document.getElementById('pdfArchiveStatus');
|
||||
if (!el) return;
|
||||
el.textContent = text || '';
|
||||
el.style.color = color || '';
|
||||
}
|
||||
|
||||
function clearPdfPreview() {
|
||||
const container = document.getElementById('pdfPreviewContainer');
|
||||
const iframe = document.getElementById('pdfPreviewIframe');
|
||||
if (container) container.style.display = 'none';
|
||||
if (iframe) iframe.src = '';
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === null || bytes === undefined || !Number.isFinite(Number(bytes))) return '-';
|
||||
const num = Number(bytes);
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
let n = num;
|
||||
while (n >= 1024 && i < units.length - 1) {
|
||||
n /= 1024;
|
||||
i += 1;
|
||||
}
|
||||
const fixed = i === 0 ? 0 : 1;
|
||||
return `${n.toFixed(fixed)} ${units[i]}`;
|
||||
}
|
||||
|
||||
async function loadPdfYears() {
|
||||
const pdfYearSelect = document.getElementById('pdfYearSelect');
|
||||
const pdfUserSelect = document.getElementById('pdfUserSelect');
|
||||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||||
if (!pdfYearSelect || !pdfUserSelect || !pdfFilesContainer) return;
|
||||
|
||||
setPdfStatus('Lade PDF-Jahre...', 'blue');
|
||||
pdfYearSelect.innerHTML = '<option value="">-- Laden... --</option>';
|
||||
pdfUserSelect.innerHTML = '';
|
||||
pdfUserSelect.disabled = true;
|
||||
pdfFilesContainer.innerHTML = '';
|
||||
clearPdfPreview();
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/pdfs/years');
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result && result.error ? result.error : 'Fehler beim Laden der PDF-Jahre');
|
||||
}
|
||||
|
||||
const years = Array.isArray(result.years) ? result.years : [];
|
||||
if (years.length === 0) {
|
||||
pdfYearSelect.innerHTML = '<option value="">Keine PDFs gefunden</option>';
|
||||
setPdfStatus('Keine PDF-Dateien im Archiv gefunden.', '');
|
||||
return;
|
||||
}
|
||||
|
||||
pdfYearSelect.innerHTML = '';
|
||||
years.forEach(year => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = year;
|
||||
opt.textContent = year;
|
||||
pdfYearSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
const firstYear = years[0];
|
||||
pdfYearSelect.value = firstYear;
|
||||
setPdfStatus('Jahre geladen. Lade User...', '');
|
||||
await loadPdfUsers(firstYear);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der PDF-Jahre:', error);
|
||||
setPdfStatus('Fehler beim Laden der PDF-Jahre', 'red');
|
||||
pdfYearSelect.innerHTML = '<option value="">Fehler</option>';
|
||||
pdfUserSelect.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPdfUsers(year) {
|
||||
const pdfYearSelect = document.getElementById('pdfYearSelect');
|
||||
const pdfUserSelect = document.getElementById('pdfUserSelect');
|
||||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||||
if (!pdfYearSelect || !pdfUserSelect || !pdfFilesContainer) return;
|
||||
|
||||
setPdfStatus(`Lade PDFs für ${year}...`, 'blue');
|
||||
pdfUserSelect.disabled = true;
|
||||
pdfUserSelect.innerHTML = '<option value="">-- Laden... --</option>';
|
||||
pdfFilesContainer.innerHTML = '';
|
||||
clearPdfPreview();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/pdfs/users?year=${encodeURIComponent(year)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result && result.error ? result.error : 'Fehler beim Laden der User-Ordner');
|
||||
}
|
||||
|
||||
const users = Array.isArray(result.users)
|
||||
? result.users
|
||||
: (Array.isArray(result.userIds) ? result.userIds.map(id => ({ id, name: id })) : []);
|
||||
|
||||
const userIds = users.map(u => u.id);
|
||||
if (users.length === 0) {
|
||||
pdfUserSelect.innerHTML = '<option value="">Keine PDFs</option>';
|
||||
pdfUserSelect.disabled = true;
|
||||
setPdfStatus(`Keine PDF-Dateien für das Jahr ${year} gefunden.`, '');
|
||||
return;
|
||||
}
|
||||
|
||||
pdfUserSelect.disabled = false;
|
||||
pdfUserSelect.innerHTML = '';
|
||||
users.forEach(user => {
|
||||
const userId = user.id;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = userId;
|
||||
opt.textContent = user.name || userId;
|
||||
pdfUserSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
const firstUserId = userIds[0];
|
||||
pdfUserSelect.value = firstUserId;
|
||||
setPdfStatus('User geladen. Lade Dateien...', '');
|
||||
await loadPdfFiles(year, firstUserId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der PDF-User:', error);
|
||||
setPdfStatus('Fehler beim Laden der PDF-User', 'red');
|
||||
pdfUserSelect.innerHTML = '<option value="">Fehler</option>';
|
||||
pdfUserSelect.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPdfFiles(year, userId) {
|
||||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||||
if (!pdfFilesContainer) return;
|
||||
|
||||
setPdfStatus('Lade PDF-Dateien...', 'blue');
|
||||
pdfFilesContainer.innerHTML = '';
|
||||
clearPdfPreview();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/pdfs/files?year=${encodeURIComponent(year)}&userId=${encodeURIComponent(userId)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result && result.error ? result.error : 'Fehler beim Laden der PDF-Dateien');
|
||||
}
|
||||
|
||||
const files = Array.isArray(result.files) ? result.files : [];
|
||||
if (files.length === 0) {
|
||||
pdfFilesContainer.innerHTML = '<p style="color:#666;">Keine PDF-Dateien gefunden.</p>';
|
||||
setPdfStatus(`Keine PDFs für User ${userId} (${year}).`, '');
|
||||
return;
|
||||
}
|
||||
|
||||
const rowsHtml = files
|
||||
.map(file => {
|
||||
const mtimeLabel = file.mtime ? new Date(file.mtime).toLocaleString('de-DE') : '-';
|
||||
const sizeLabel = formatBytes(file.size);
|
||||
// name ist serverseitig streng gefiltert; hier daher direkt in JS-String verwenden.
|
||||
return `
|
||||
<tr>
|
||||
<td style="padding:4px; word-break:break-all;">
|
||||
<code>${file.name}</code>
|
||||
</td>
|
||||
<td style="padding:4px;">${sizeLabel}</td>
|
||||
<td style="padding:4px;">${mtimeLabel}</td>
|
||||
<td style="padding:4px; white-space:nowrap;">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="previewPdf('${year}','${userId}','${file.name}')">Vorschau</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" style="margin-left:6px;" onclick="downloadPdf('${year}','${userId}','${file.name}')">Download</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
pdfFilesContainer.innerHTML = `
|
||||
<table style="width: 100%; min-width: 980px; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left; padding:4px;">Datei</th>
|
||||
<th style="text-align:left; padding:4px;">Größe</th>
|
||||
<th style="text-align:left; padding:4px;">Änderung</th>
|
||||
<th style="text-align:left; padding:4px;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rowsHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
setPdfStatus(`${files.length} PDF(s) geladen.`, 'green');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der PDF-Dateien:', error);
|
||||
pdfFilesContainer.innerHTML = '<p style="color: red;">Fehler beim Laden der PDF-Dateien.</p>';
|
||||
setPdfStatus('Fehler beim Laden der PDF-Dateien', 'red');
|
||||
}
|
||||
}
|
||||
|
||||
function previewPdf(year, userId, name) {
|
||||
const iframe = document.getElementById('pdfPreviewIframe');
|
||||
const container = document.getElementById('pdfPreviewContainer');
|
||||
if (!iframe || !container) return;
|
||||
|
||||
setPdfStatus(`Vorschau: ${name}`, 'blue');
|
||||
container.style.display = 'block';
|
||||
iframe.src = `/admin/api/pdfs/file?year=${encodeURIComponent(year)}&userId=${encodeURIComponent(userId)}&name=${encodeURIComponent(name)}&inline=true`;
|
||||
}
|
||||
|
||||
function downloadPdf(year, userId, name) {
|
||||
const url = `/admin/api/pdfs/file?year=${encodeURIComponent(year)}&userId=${encodeURIComponent(userId)}&name=${encodeURIComponent(name)}&inline=false`;
|
||||
|
||||
setPdfStatus('Download gestartet...', '');
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener';
|
||||
link.click();
|
||||
}
|
||||
|
||||
async function loadTimesheetDuplicates() {
|
||||
const container = document.getElementById('timesheetDuplicatesContainer');
|
||||
if (!container) return;
|
||||
|
||||
129
rebuild-timesheet-pdfs.js
Normal file
129
rebuild-timesheet-pdfs.js
Normal file
@@ -0,0 +1,129 @@
|
||||
// scripts/rebuild-timesheet-pdfs.js
|
||||
|
||||
// Läuft im Projekt root: node scripts/rebuild-timesheet-pdfs.js
|
||||
// Erzeugt (bzw. speichert) fehlende PDFs für alle eingereichten weekly_timesheets,
|
||||
// basierend auf der bestehenden versionstreuen Generierungslogik.
|
||||
|
||||
const path = require('path');
|
||||
const { db } = require('../database');
|
||||
const { generatePDFToBuffer } = require('../services/pdf-service');
|
||||
const {
|
||||
resolvePdfLocationFromTimesheetRow,
|
||||
fileExists,
|
||||
savePdfBufferAtomic,
|
||||
} = require('../services/pdf-storage-service');
|
||||
|
||||
async function getAllSubmittedTimesheets() {
|
||||
const sql = `
|
||||
SELECT wt.id, wt.user_id, wt.week_start, wt.week_end, wt.version,
|
||||
wt.pdf_path,
|
||||
u.firstname, u.lastname, u.username
|
||||
FROM weekly_timesheets wt
|
||||
JOIN users u ON u.id = wt.user_id
|
||||
WHERE wt.status = 'eingereicht'
|
||||
ORDER BY wt.week_start, wt.user_id, wt.version
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, [], (err, rows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(rows || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function rebuildMissingPdfs() {
|
||||
const all = await getAllSubmittedTimesheets();
|
||||
if (!all.length) {
|
||||
console.log('Keine eingereichten weekly_timesheets gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Gefundene eingereichte weekly_timesheets: ${all.length}`);
|
||||
|
||||
let processed = 0;
|
||||
let created = 0;
|
||||
let skippedExists = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const row of all) {
|
||||
processed += 1;
|
||||
|
||||
try {
|
||||
const loc = resolvePdfLocationFromTimesheetRow(row);
|
||||
const absolutePath = loc.absolutePath;
|
||||
|
||||
const exists = await fileExists(absolutePath);
|
||||
if (exists) {
|
||||
skippedExists += 1;
|
||||
if (processed % 50 === 0) {
|
||||
console.log(
|
||||
`[${processed}/${all.length}] Timesheet #${row.id}: PDF existiert bereits -> übersprungen.`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${processed}/${all.length}] Erzeuge PDF für Timesheet #${row.id} (User ${row.user_id}, KW: ${row.week_start}..${row.week_end}, Version ${row.version})`,
|
||||
);
|
||||
|
||||
// Mock-Request für generatePDFToBuffer – analog zu verwaltung-routes bulk-download
|
||||
const mockReq = {
|
||||
session: {
|
||||
userId: null, // hier optional eine system-User-ID einsetzen, wenn benötigt
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const buffer = await generatePDFToBuffer(row.id, mockReq);
|
||||
await savePdfBufferAtomic(absolutePath, buffer);
|
||||
|
||||
// Optional: pdf_path in DB setzen, falls noch leer
|
||||
if (!row.pdf_path) {
|
||||
const relativePath = loc.relativePath;
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE weekly_timesheets SET pdf_path = ? WHERE id = ?',
|
||||
[relativePath, row.id],
|
||||
(err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
created += 1;
|
||||
} catch (err) {
|
||||
failed += 1;
|
||||
console.error(
|
||||
`Fehler beim Erzeugen der PDF für Timesheet #${row.id}:`,
|
||||
err && err.message ? err.message : err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('-----------------------------');
|
||||
console.log('Rebuild abgeschlossen.');
|
||||
console.log(`Gesamt: ${processed}`);
|
||||
console.log(`Neu erzeugt: ${created}`);
|
||||
console.log(`Bereits vorhanden: ${skippedExists}`);
|
||||
console.log(`Fehler: ${failed}`);
|
||||
}
|
||||
|
||||
// Direktstart, wenn Datei via node aufgerufen wird
|
||||
if (require.main === module) {
|
||||
rebuildMissingPdfs()
|
||||
.then(() => {
|
||||
console.log('Fertig.');
|
||||
// DB-Verbindung optional schließen, falls du willst:
|
||||
// db.close();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Unerwarteter Fehler beim Rebuild:', err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { rebuildMissingPdfs };
|
||||
@@ -1,9 +1,24 @@
|
||||
// Admin Routes
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fs = require('fs');
|
||||
const fsp = require('fs/promises');
|
||||
const path = require('path');
|
||||
const { db } = require('../database');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
const { testMssqlConnection } = require('../services/mssql-infra-service');
|
||||
const { getPdfBaseDir } = require('../helpers/pdf-paths');
|
||||
|
||||
const YEAR_RE = /^\d{4}$/;
|
||||
const USER_ID_RE = /^\d+$/;
|
||||
const PDF_NAME_RE = /^[A-Za-z0-9._-]+\.pdf$/i;
|
||||
|
||||
function resolveWithinBaseDir(baseDirResolved, ...parts) {
|
||||
const targetPath = path.resolve(baseDirResolved, ...parts);
|
||||
const prefix = baseDirResolved.endsWith(path.sep) ? baseDirResolved : baseDirResolved + path.sep;
|
||||
if (targetPath !== baseDirResolved && !targetPath.startsWith(prefix)) return null;
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
// Routes registrieren
|
||||
function registerAdminRoutes(app) {
|
||||
@@ -412,6 +427,164 @@ function registerAdminRoutes(app) {
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------
|
||||
// PDF-Archiv (Admin)
|
||||
// -----------------------
|
||||
|
||||
// Liste vorhandener Jahre unter PDF_BASE_DIR
|
||||
app.get('/admin/api/pdfs/years', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const baseDirResolved = path.resolve(getPdfBaseDir());
|
||||
const entries = await fsp.readdir(baseDirResolved, { withFileTypes: true }).catch(() => []);
|
||||
const years = entries
|
||||
.filter(d => d.isDirectory() && YEAR_RE.test(d.name))
|
||||
.map(d => d.name)
|
||||
.sort((a, b) => Number(b) - Number(a));
|
||||
res.json({ years });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der PDF-Jahre:', err);
|
||||
res.status(500).json({ error: 'Fehler beim Laden der PDF-Jahre' });
|
||||
}
|
||||
});
|
||||
|
||||
// Liste vorhandener User-Ordner für ein Jahr
|
||||
app.get('/admin/api/pdfs/users', requireAdmin, async (req, res) => {
|
||||
const year = String(req.query.year || '').trim();
|
||||
if (!YEAR_RE.test(year)) return res.status(400).json({ error: 'Ungültiges Jahr' });
|
||||
|
||||
try {
|
||||
const baseDirResolved = path.resolve(getPdfBaseDir());
|
||||
const yearDir = resolveWithinBaseDir(baseDirResolved, year);
|
||||
if (!yearDir) return res.status(400).json({ error: 'Ungültige Pfadanfrage' });
|
||||
|
||||
const entries = await fsp.readdir(yearDir, { withFileTypes: true }).catch(() => []);
|
||||
const userIds = entries
|
||||
.filter(d => d.isDirectory() && USER_ID_RE.test(d.name))
|
||||
.map(d => d.name)
|
||||
.sort((a, b) => Number(b) - Number(a));
|
||||
|
||||
// Zusätzlich Namen aus DB laden, damit der Dropdown menschenlesbar ist.
|
||||
let usersWithNames = [];
|
||||
if (userIds.length > 0) {
|
||||
const placeholders = userIds.map(() => '?').join(',');
|
||||
const params = userIds.map(id => parseInt(id, 10));
|
||||
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT id, firstname, lastname, username FROM users WHERE id IN (${placeholders})`,
|
||||
params,
|
||||
(err2, r) => {
|
||||
if (err2) return reject(err2);
|
||||
resolve(r || []);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const map = new Map();
|
||||
(rows || []).forEach(u => {
|
||||
const name = [u.firstname, u.lastname].filter(Boolean).join(' ').trim();
|
||||
map.set(String(u.id), name || u.username || String(u.id));
|
||||
});
|
||||
|
||||
usersWithNames = userIds.map(id => ({
|
||||
id,
|
||||
name: map.get(String(id)) || String(id),
|
||||
}));
|
||||
}
|
||||
|
||||
res.json({ userIds, users: usersWithNames });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der PDF-User-Ordner:', err);
|
||||
res.status(500).json({ error: 'Fehler beim Laden der PDF-User-Ordner' });
|
||||
}
|
||||
});
|
||||
|
||||
// Liste PDFs für year/userId
|
||||
app.get('/admin/api/pdfs/files', requireAdmin, async (req, res) => {
|
||||
const year = String(req.query.year || '').trim();
|
||||
const userId = String(req.query.userId || '').trim();
|
||||
|
||||
if (!YEAR_RE.test(year)) return res.status(400).json({ error: 'Ungültiges Jahr' });
|
||||
if (!USER_ID_RE.test(userId)) return res.status(400).json({ error: 'Ungültiger User' });
|
||||
|
||||
try {
|
||||
const baseDirResolved = path.resolve(getPdfBaseDir());
|
||||
const userDir = resolveWithinBaseDir(baseDirResolved, year, userId);
|
||||
if (!userDir) return res.status(400).json({ error: 'Ungültige Pfadanfrage' });
|
||||
|
||||
const entries = await fsp.readdir(userDir, { withFileTypes: true }).catch(() => []);
|
||||
const pdfEntries = entries.filter(d => d.isFile() && PDF_NAME_RE.test(d.name));
|
||||
|
||||
const filesWithMeta = await Promise.all(
|
||||
pdfEntries.map(async d => {
|
||||
const stat = await fsp.stat(path.join(userDir, d.name));
|
||||
return {
|
||||
name: d.name,
|
||||
size: stat.size,
|
||||
mtime: stat.mtime.toISOString()
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
filesWithMeta.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime());
|
||||
res.json({ files: filesWithMeta });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der PDF-Dateiliste:', err);
|
||||
res.status(500).json({ error: 'Fehler beim Laden der PDF-Dateiliste' });
|
||||
}
|
||||
});
|
||||
|
||||
// Streamt eine einzelne PDF (Vorschau oder Download)
|
||||
app.get('/admin/api/pdfs/file', requireAdmin, async (req, res) => {
|
||||
const year = String(req.query.year || '').trim();
|
||||
const userId = String(req.query.userId || '').trim();
|
||||
const nameRaw = String(req.query.name || '');
|
||||
const inline = String(req.query.inline || 'false') === 'true';
|
||||
|
||||
if (!YEAR_RE.test(year)) return res.status(400).json({ error: 'Ungültiges Jahr' });
|
||||
if (!USER_ID_RE.test(userId)) return res.status(400).json({ error: 'Ungültiger User' });
|
||||
|
||||
const safeName = path.basename(nameRaw);
|
||||
if (!safeName || safeName !== nameRaw || !PDF_NAME_RE.test(safeName)) {
|
||||
return res.status(400).json({ error: 'Ungültiger Dateiname' });
|
||||
}
|
||||
|
||||
try {
|
||||
const baseDirResolved = path.resolve(getPdfBaseDir());
|
||||
const absolutePath = resolveWithinBaseDir(baseDirResolved, year, userId, safeName);
|
||||
if (!absolutePath) return res.status(400).json({ error: 'Ungültige Pfadanfrage' });
|
||||
|
||||
let stat;
|
||||
try {
|
||||
stat = await fsp.stat(absolutePath);
|
||||
} catch (err) {
|
||||
if (err && err.code === 'ENOENT') return res.status(404).json({ error: 'PDF nicht gefunden' });
|
||||
throw err;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('Content-Length', stat.size);
|
||||
if (inline) {
|
||||
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${safeName}"`);
|
||||
} else {
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${safeName}"`);
|
||||
}
|
||||
|
||||
const stream = fs.createReadStream(absolutePath);
|
||||
stream.on('error', (streamErr) => {
|
||||
console.error('Fehler beim Streamen der PDF:', streamErr);
|
||||
if (!res.headersSent) res.status(500);
|
||||
res.end('Fehler beim Lesen der PDF-Datei');
|
||||
});
|
||||
return stream.pipe(res);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Streamen der PDF:', err);
|
||||
if (!res.headersSent) res.status(500).json({ error: 'Fehler beim Streamen der PDF' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = registerAdminRoutes;
|
||||
|
||||
@@ -461,6 +461,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pdf-archive-section" style="margin-top: 40px;">
|
||||
<div class="collapsible-header" onclick="togglePdfArchiveSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h2 style="margin: 0;">PDF-Archiv (Timesheets)</h2>
|
||||
<span id="pdfArchiveToggleIcon" style="font-size: 18px; transition: transform 0.3s;">▼</span>
|
||||
</div>
|
||||
|
||||
<div id="pdfArchiveContent" style="display: none; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff;">
|
||||
<div class="form-row" style="display: flex; gap: 20px; flex-wrap: wrap; align-items: flex-end;">
|
||||
<div class="form-group" style="min-width: 180px;">
|
||||
<label for="pdfYearSelect">Jahr</label>
|
||||
<select id="pdfYearSelect" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="min-width: 180px;">
|
||||
<label for="pdfUserSelect">User</label>
|
||||
<select id="pdfUserSelect" disabled style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="min-width: 180px;">
|
||||
<button id="pdfRefreshBtn" class="btn btn-secondary" type="button" style="width: 100%;">Aktualisieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pdfArchiveStatus" style="margin-top: 15px; color: #666;"></div>
|
||||
|
||||
<div id="pdfFilesContainer" style="margin-top: 15px; overflow-x: auto; max-width: 100%;"></div>
|
||||
|
||||
<div id="pdfPreviewContainer" style="display: none; margin-top: 25px;">
|
||||
<h3 style="margin-top: 0;">Vorschau</h3>
|
||||
<iframe id="pdfPreviewIframe" style="width: 100%; height: 650px; border: 1px solid #ddd; border-radius: 4px;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -549,6 +583,20 @@
|
||||
if (icon) icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
function togglePdfArchiveSection() {
|
||||
const content = document.getElementById('pdfArchiveContent');
|
||||
const icon = document.getElementById('pdfArchiveToggleIcon');
|
||||
if (!content) return;
|
||||
|
||||
if (content.style.display === 'none' || content.style.display === '') {
|
||||
content.style.display = 'block';
|
||||
if (icon) icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
if (icon) icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
// Rollenwechsel-Handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
Reference in New Issue
Block a user