Optional applicant photo in CV header
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,48 @@ function loadSignatur() {
|
||||
}
|
||||
}
|
||||
|
||||
// Directory + uploader for the applicant's portrait photo (used in the CV)
|
||||
const fotoDir = path.join(dataDir, 'bewerberfoto');
|
||||
if (!fs.existsSync(fotoDir)) {
|
||||
fs.mkdirSync(fotoDir, { recursive: true });
|
||||
}
|
||||
const uploadFoto = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, fotoDir),
|
||||
filename: (req, file, cb) => {
|
||||
const ext = (path.extname(file.originalname) || '.png').toLowerCase();
|
||||
cb(null, `foto_${Date.now()}${ext}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
|
||||
fileFilter: (req, file, cb) => cb(null, /^image\/(png|jpe?g)$/.test(file.mimetype)),
|
||||
}).single('foto');
|
||||
|
||||
// The single stored photo file, if any.
|
||||
function currentFotoFile() {
|
||||
try {
|
||||
const files = fs.readdirSync(fotoDir).filter((f) => !f.startsWith('.'));
|
||||
return files.length ? path.join(fotoDir, files[0]) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the photo as a data URL + jsPDF format, for embedding in the CV.
|
||||
function loadFoto() {
|
||||
const file = currentFotoFile();
|
||||
if (!file) return null;
|
||||
try {
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
const format = (ext === '.jpg' || ext === '.jpeg') ? 'JPEG' : 'PNG';
|
||||
const mime = format === 'JPEG' ? 'image/jpeg' : 'image/png';
|
||||
const b64 = fs.readFileSync(file).toString('base64');
|
||||
return { dataUrl: `data:${mime};base64,${b64}`, format };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Database setup
|
||||
const dbPath = path.join(dataDir, 'bewerbungen.db');
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
@@ -228,6 +270,8 @@ async function runGeneration(bewerbungId) {
|
||||
llmNotizen: bewerbung.llm_notizen || '',
|
||||
// Signature image placed under the closing salutation (instead of the typed name).
|
||||
signatur: loadSignatur(),
|
||||
// Applicant photo placed in the CV header (top-right), optional.
|
||||
bewerbungsfoto: loadFoto(),
|
||||
});
|
||||
|
||||
let seq = 0;
|
||||
@@ -794,6 +838,7 @@ initializeDatabase().then(() => {
|
||||
basisDokumente,
|
||||
basisAnhaenge,
|
||||
hasSignatur: Boolean(currentSignaturFile()),
|
||||
hasFoto: Boolean(currentFotoFile()),
|
||||
basisTypOptions: BASIS_TYP_OPTIONS,
|
||||
hasApiKey: Boolean(process.env.OLLAMA_API_KEY),
|
||||
hideSettings: true,
|
||||
@@ -941,6 +986,42 @@ initializeDatabase().then(() => {
|
||||
res.redirect('/vorlagen');
|
||||
});
|
||||
|
||||
// ----- Applicant photo (Bewerberfoto, used in the CV) -----
|
||||
|
||||
// Serve the current photo (for the preview on the Vorlagen page)
|
||||
app.get('/bewerbungsfoto', (req, res) => {
|
||||
const file = currentFotoFile();
|
||||
if (!file) return res.status(404).send('Kein Bewerberfoto');
|
||||
res.sendFile(file);
|
||||
});
|
||||
|
||||
// Upload / replace the photo
|
||||
app.post('/bewerbungsfoto', (req, res) => {
|
||||
uploadFoto(req, res, (err) => {
|
||||
try {
|
||||
if (err) console.error('Photo upload error:', err.message);
|
||||
if (req.file) {
|
||||
// keep only the newly uploaded file
|
||||
fs.readdirSync(fotoDir).forEach((f) => {
|
||||
if (f !== req.file.filename) fs.promises.unlink(path.join(fotoDir, f)).catch(() => {});
|
||||
});
|
||||
}
|
||||
res.redirect('/vorlagen');
|
||||
} catch (error) {
|
||||
console.error('Error saving photo:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete the photo
|
||||
app.post('/bewerbungsfoto/delete', (req, res) => {
|
||||
try {
|
||||
fs.readdirSync(fotoDir).forEach((f) => fs.promises.unlink(path.join(fotoDir, f)).catch(() => {}));
|
||||
} catch (e) { /* ignore */ }
|
||||
res.redirect('/vorlagen');
|
||||
});
|
||||
|
||||
// Download a generated attachment
|
||||
app.get('/anhaenge/:id/download', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user