Signature image in cover letter + fix per-paragraph capitalization
- Only the first body paragraph continues the salutation (lowercase safe opener); every following paragraph is a new sentence and is capitalized. Handled deterministically so it no longer depends on the model. - Signature: upload a signature image on the Vorlagen page (preview / replace / remove). It is embedded in the cover letter directly under the closing salutation, replacing the typed name; falls back to the name when absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,48 @@ const uploadBasisAnhang = multer({
|
||||
limits: { fileSize: 15 * 1024 * 1024 }, // 15 MB
|
||||
}).single('datei');
|
||||
|
||||
// Directory + uploader for the applicant's signature image (used in the letter)
|
||||
const signaturDir = path.join(dataDir, 'signatur');
|
||||
if (!fs.existsSync(signaturDir)) {
|
||||
fs.mkdirSync(signaturDir, { recursive: true });
|
||||
}
|
||||
const uploadSignatur = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, signaturDir),
|
||||
filename: (req, file, cb) => {
|
||||
const ext = (path.extname(file.originalname) || '.png').toLowerCase();
|
||||
cb(null, `signatur_${Date.now()}${ext}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
|
||||
fileFilter: (req, file, cb) => cb(null, /^image\/(png|jpe?g)$/.test(file.mimetype)),
|
||||
}).single('signatur');
|
||||
|
||||
// The single stored signature file, if any.
|
||||
function currentSignaturFile() {
|
||||
try {
|
||||
const files = fs.readdirSync(signaturDir).filter((f) => !f.startsWith('.'));
|
||||
return files.length ? path.join(signaturDir, files[0]) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the signature as a data URL + jsPDF format, for embedding in the letter.
|
||||
function loadSignatur() {
|
||||
const file = currentSignaturFile();
|
||||
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);
|
||||
@@ -184,6 +226,8 @@ async function runGeneration(bewerbungId) {
|
||||
zusatzAnlagen: basisAnhaenge.map((a) => a.name || a.dateiname),
|
||||
// Free-text notes (company address, contact person, extra context) for the LLM.
|
||||
llmNotizen: bewerbung.llm_notizen || '',
|
||||
// Signature image placed under the closing salutation (instead of the typed name).
|
||||
signatur: loadSignatur(),
|
||||
});
|
||||
|
||||
let seq = 0;
|
||||
@@ -749,6 +793,7 @@ initializeDatabase().then(() => {
|
||||
res.render('vorlagen', {
|
||||
basisDokumente,
|
||||
basisAnhaenge,
|
||||
hasSignatur: Boolean(currentSignaturFile()),
|
||||
basisTypOptions: BASIS_TYP_OPTIONS,
|
||||
hasApiKey: Boolean(process.env.OLLAMA_API_KEY),
|
||||
hideSettings: true,
|
||||
@@ -860,6 +905,42 @@ initializeDatabase().then(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// ----- Signature (Unterschrift) -----
|
||||
|
||||
// Serve the current signature image (for the preview on the Vorlagen page)
|
||||
app.get('/unterschrift', (req, res) => {
|
||||
const file = currentSignaturFile();
|
||||
if (!file) return res.status(404).send('Keine Unterschrift');
|
||||
res.sendFile(file);
|
||||
});
|
||||
|
||||
// Upload / replace the signature
|
||||
app.post('/unterschrift', (req, res) => {
|
||||
uploadSignatur(req, res, (err) => {
|
||||
try {
|
||||
if (err) console.error('Signature upload error:', err.message);
|
||||
if (req.file) {
|
||||
// keep only the newly uploaded file
|
||||
fs.readdirSync(signaturDir).forEach((f) => {
|
||||
if (f !== req.file.filename) fs.promises.unlink(path.join(signaturDir, f)).catch(() => {});
|
||||
});
|
||||
}
|
||||
res.redirect('/vorlagen');
|
||||
} catch (error) {
|
||||
console.error('Error saving signature:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete the signature
|
||||
app.post('/unterschrift/delete', (req, res) => {
|
||||
try {
|
||||
fs.readdirSync(signaturDir).forEach((f) => fs.promises.unlink(path.join(signaturDir, 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