fix(ui): echte Umlaute in allen sichtbaren Texten
Menüs, Dialoge, Schaltflächen, Platzhalter, Hinweise und die
Fehlermeldungen der API standen in Ersatzschreibweise (oe/ae/ue). Sie
erscheinen als Toast oder Beschriftung direkt in der Oberfläche und
gehören damit richtig geschrieben; sechs sichtbare Texte mit ss statt ß
sind gleich mit erledigt ("Schließen", "Straße", "zu groß").
Unangetastet bleibt bewusst:
- Kommentare im Quelltext - die sind durchgängig ASCII, und der Diff wäre
dreimal so groß ohne Nutzen für den Benutzer
- Protokollzeilen (console.*) - die landen im journal, nicht in der App
- Datenbankwerte und interne Kennungen: 'uebernommen', 'empfaenger',
'vorschlaege', 'laeuft-bereits' stecken in Enums, SQL und
Zustandsvergleichen. Ein Umlaut darin hätte still falsche Ergebnisse
geliefert
- server/src/mail/extract.js - die Muster decken absichtlich beide
Schreibweisen ab: /gr(ü|ue)(ß|ss)e/
- scanner/utils/crypto.js - der Ersatzschlüssel darf sich nicht ändern,
sonst sind gespeicherte Portal-Passwörter nicht mehr lesbar
Geprüft mit einem Rundgang durch die Ansichten und Dialoge: sichtbarer
Text plus title/aria-label/placeholder enthalten keine
Ersatzschreibweisen mehr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ea55acdb5c
commit
8b7ce448a6
@@ -71,7 +71,7 @@ async function fetchAccessToken() {
|
||||
}
|
||||
|
||||
if (!data?.access_token) {
|
||||
throw new GraphError('Anmeldung lieferte kein Token zurueck.', { status: response.status });
|
||||
throw new GraphError('Anmeldung lieferte kein Token zurück.', { status: response.status });
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -189,7 +189,7 @@ export async function testConnection() {
|
||||
ok: false,
|
||||
stage: 'token',
|
||||
error: err.message,
|
||||
hint: 'Tenant-ID, Client-ID und Client-Secret in der .env pruefen.',
|
||||
hint: 'Tenant-ID, Client-ID und Client-Secret in der .env prüfen.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -211,8 +211,8 @@ export async function testConnection() {
|
||||
err.code === 'ErrorAccessDenied' || err.status === 403
|
||||
? 'Zugriff verweigert: Anwendungsberechtigung Mail.Read erteilt und Administratorzustimmung gegeben? Erlaubt die ApplicationAccessPolicy dieses Postfach?'
|
||||
: err.code === 'ResourceNotFound' || err.status === 404
|
||||
? `Postfach ${MAILBOX} wurde nicht gefunden - Adresse pruefen.`
|
||||
: 'Berechtigungen und Zugriffsrichtlinie in Exchange Online pruefen.';
|
||||
? `Postfach ${MAILBOX} wurde nicht gefunden - Adresse prüfen.`
|
||||
: 'Berechtigungen und Zugriffsrichtlinie in Exchange Online prüfen.';
|
||||
return { ok: false, stage: 'mailbox', error: err.message, code: err.code ?? null, hint };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ router.patch('/me', requireAuth, async (req, res, next) => {
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return res.status(400).json({ error: 'Keine Aenderungen uebergeben.' });
|
||||
return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -51,7 +51,7 @@ router.post('/', async (req, res, next) => {
|
||||
const toId = Number(req.body?.to_lead_id);
|
||||
|
||||
if (!Number.isInteger(fromId) || !Number.isInteger(toId)) {
|
||||
return res.status(400).json({ error: 'Beide Kontakte muessen angegeben werden.' });
|
||||
return res.status(400).json({ error: 'Beide Kontakte müssen angegeben werden.' });
|
||||
}
|
||||
if (fromId === toId) {
|
||||
return res.status(400).json({ error: 'Ein Kontakt kann nicht mit sich selbst verbunden werden.' });
|
||||
@@ -90,13 +90,13 @@ router.post('/', async (req, res, next) => {
|
||||
// PATCH /api/connections/:id
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Verbindungs-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Verbindungs-ID.' });
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (req.body?.type !== undefined) {
|
||||
if (!TYPES.has(req.body.type)) return res.status(400).json({ error: 'Ungueltiger Verbindungstyp.' });
|
||||
if (!TYPES.has(req.body.type)) return res.status(400).json({ error: 'Ungültiger Verbindungstyp.' });
|
||||
fields.push('type = ?');
|
||||
values.push(req.body.type);
|
||||
}
|
||||
@@ -107,13 +107,13 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (req.body?.strength !== undefined) {
|
||||
const strength = Number(req.body.strength);
|
||||
if (![1, 2, 3].includes(strength)) {
|
||||
return res.status(400).json({ error: 'Verbindungsstaerke muss 1, 2 oder 3 sein.' });
|
||||
return res.status(400).json({ error: 'Verbindungsstärke muss 1, 2 oder 3 sein.' });
|
||||
}
|
||||
fields.push('strength = ?');
|
||||
values.push(strength);
|
||||
}
|
||||
|
||||
if (fields.length === 0) return res.status(400).json({ error: 'Keine Aenderungen uebergeben.' });
|
||||
if (fields.length === 0) return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
|
||||
|
||||
try {
|
||||
values.push(id);
|
||||
@@ -138,7 +138,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
// DELETE /api/connections/:id
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Verbindungs-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Verbindungs-ID.' });
|
||||
|
||||
try {
|
||||
const [result] = await pool.query('DELETE FROM crm_lead_connections WHERE id = ?', [id]);
|
||||
|
||||
+10
-10
@@ -47,11 +47,11 @@ function emptyToNull(value) {
|
||||
/** Teilnehmer-IDs aus dem Request pruefen und deduplizieren. */
|
||||
async function normalizeParticipants(raw) {
|
||||
if (raw === undefined) return undefined;
|
||||
if (!Array.isArray(raw)) return { error: 'Teilnehmer muessen als Liste uebergeben werden.' };
|
||||
if (!Array.isArray(raw)) return { error: 'Teilnehmer müssen als Liste übergeben werden.' };
|
||||
|
||||
const ids = [...new Set(raw.map((value) => Number(value)))];
|
||||
if (ids.some((id) => !Number.isInteger(id) || id <= 0)) {
|
||||
return { error: 'Ungueltige Teilnehmer-ID.' };
|
||||
return { error: 'Ungültige Teilnehmer-ID.' };
|
||||
}
|
||||
if (ids.length === 0) return { ids: [] };
|
||||
|
||||
@@ -142,7 +142,7 @@ router.post('/', async (req, res, next) => {
|
||||
const startsAt = normalizeDateTime(req.body?.starts_at);
|
||||
const endsAt = normalizeDateTime(req.body?.ends_at);
|
||||
if (!startsAt || !endsAt) {
|
||||
return res.status(400).json({ error: 'Start und Ende muessen gueltige Zeitpunkte sein.' });
|
||||
return res.status(400).json({ error: 'Start und Ende müssen gültige Zeitpunkte sein.' });
|
||||
}
|
||||
if (endsAt < startsAt) {
|
||||
return res.status(400).json({ error: 'Das Ende darf nicht vor dem Start liegen.' });
|
||||
@@ -186,7 +186,7 @@ function mayModify(user, event) {
|
||||
// PATCH /api/events/:id
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Termin-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Termin-ID.' });
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
@@ -196,7 +196,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (!mayModify(req.user, existing)) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: 'Diesen Termin darf nur der Ersteller oder ein Administrator aendern.' });
|
||||
.json({ error: 'Diesen Termin darf nur der Ersteller oder ein Administrator ändern.' });
|
||||
}
|
||||
|
||||
const fields = [];
|
||||
@@ -217,7 +217,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
let startsAt = existing.starts_at;
|
||||
if (req.body?.starts_at !== undefined) {
|
||||
startsAt = normalizeDateTime(req.body.starts_at);
|
||||
if (!startsAt) return res.status(400).json({ error: 'Ungueltiger Startzeitpunkt.' });
|
||||
if (!startsAt) return res.status(400).json({ error: 'Ungültiger Startzeitpunkt.' });
|
||||
fields.push('starts_at = ?');
|
||||
values.push(startsAt);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
let endsAt = existing.ends_at;
|
||||
if (req.body?.ends_at !== undefined) {
|
||||
endsAt = normalizeDateTime(req.body.ends_at);
|
||||
if (!endsAt) return res.status(400).json({ error: 'Ungueltiger Endzeitpunkt.' });
|
||||
if (!endsAt) return res.status(400).json({ error: 'Ungültiger Endzeitpunkt.' });
|
||||
fields.push('ends_at = ?');
|
||||
values.push(endsAt);
|
||||
}
|
||||
@@ -243,7 +243,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (participants?.error) return res.status(400).json({ error: participants.error });
|
||||
|
||||
if (fields.length === 0 && !participants) {
|
||||
return res.status(400).json({ error: 'Keine Aenderungen uebergeben.' });
|
||||
return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
|
||||
}
|
||||
|
||||
await conn.beginTransaction();
|
||||
@@ -265,7 +265,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
// DELETE /api/events/:id - Teilnehmer entfernt die DB per ON DELETE CASCADE
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Termin-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Termin-ID.' });
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT created_by FROM crm_events WHERE id = ? LIMIT 1', [id]);
|
||||
@@ -274,7 +274,7 @@ router.delete('/:id', async (req, res, next) => {
|
||||
if (!mayModify(req.user, existing)) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: 'Diesen Termin darf nur der Ersteller oder ein Administrator loeschen.' });
|
||||
.json({ error: 'Diesen Termin darf nur der Ersteller oder ein Administrator löschen.' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM crm_events WHERE id = ?', [id]);
|
||||
|
||||
+11
-11
@@ -115,7 +115,7 @@ router.post('/', async (req, res, next) => {
|
||||
: Number(req.body.owner_id);
|
||||
|
||||
if (Number.isNaN(ownerId) || !(await userExists(ownerId))) {
|
||||
return res.status(400).json({ error: 'Zustaendiger Benutzer existiert nicht.' });
|
||||
return res.status(400).json({ error: 'Zuständiger Benutzer existiert nicht.' });
|
||||
}
|
||||
|
||||
const columns = [...TEXT_FIELDS, 'status', 'tags', 'owner_id', 'created_by'];
|
||||
@@ -283,7 +283,7 @@ router.get('/mail-pool/sources', async (req, res, next) => {
|
||||
*/
|
||||
router.post('/mail-pool/promote', async (req, res, next) => {
|
||||
const ids = Array.isArray(req.body?.ids) ? req.body.ids.map(Number).filter(Number.isInteger) : [];
|
||||
if (ids.length === 0) return res.status(400).json({ error: 'Keine Eintraege uebergeben.' });
|
||||
if (ids.length === 0) return res.status(400).json({ error: 'Keine Einträge übergeben.' });
|
||||
|
||||
const keepPool = req.body?.keep_pool === true;
|
||||
|
||||
@@ -323,7 +323,7 @@ router.post('/mail-pool/promote', async (req, res, next) => {
|
||||
*/
|
||||
router.post('/:id/promote', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Lead-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Lead-ID.' });
|
||||
|
||||
const keepPool = req.body?.keep_pool === true;
|
||||
const targetLeadId =
|
||||
@@ -331,7 +331,7 @@ router.post('/:id/promote', async (req, res, next) => {
|
||||
? null
|
||||
: Number(req.body.target_lead_id);
|
||||
if (targetLeadId !== null && !Number.isInteger(targetLeadId)) {
|
||||
return res.status(400).json({ error: 'Ungueltige Ziel-Kontakt-ID.' });
|
||||
return res.status(400).json({ error: 'Ungültige Ziel-Kontakt-ID.' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -345,7 +345,7 @@ router.post('/:id/promote', async (req, res, next) => {
|
||||
? poolLead.owner_id ?? req.user.id
|
||||
: Number(req.body.owner_id);
|
||||
if (!Number.isInteger(ownerId) || !(await userExists(ownerId))) {
|
||||
return res.status(400).json({ error: 'Zustaendiger Benutzer existiert nicht.' });
|
||||
return res.status(400).json({ error: 'Zuständiger Benutzer existiert nicht.' });
|
||||
}
|
||||
|
||||
const leadId = await copyPoolLeadToContacts(poolLead, { userId: req.user.id, ownerId });
|
||||
@@ -403,7 +403,7 @@ router.post('/:id/promote', async (req, res, next) => {
|
||||
// PATCH /api/leads/:id
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Lead-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Lead-ID.' });
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
@@ -412,14 +412,14 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (req.body?.[field] === undefined) continue;
|
||||
const value = emptyToNull(req.body[field]);
|
||||
if ((field === 'first_name' || field === 'last_name') && !value) {
|
||||
return res.status(400).json({ error: 'Vorname und Nachname duerfen nicht leer sein.' });
|
||||
return res.status(400).json({ error: 'Vorname und Nachname dürfen nicht leer sein.' });
|
||||
}
|
||||
fields.push(`${field} = ?`);
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
if (req.body?.status !== undefined) {
|
||||
if (!STATUSES.has(req.body.status)) return res.status(400).json({ error: 'Ungueltiger Status.' });
|
||||
if (!STATUSES.has(req.body.status)) return res.status(400).json({ error: 'Ungültiger Status.' });
|
||||
fields.push('status = ?');
|
||||
values.push(req.body.status);
|
||||
}
|
||||
@@ -433,13 +433,13 @@ router.patch('/:id', async (req, res, next) => {
|
||||
const ownerId =
|
||||
req.body.owner_id === null || req.body.owner_id === '' ? null : Number(req.body.owner_id);
|
||||
if (ownerId !== null && (Number.isNaN(ownerId) || !(await userExists(ownerId)))) {
|
||||
return res.status(400).json({ error: 'Zustaendiger Benutzer existiert nicht.' });
|
||||
return res.status(400).json({ error: 'Zuständiger Benutzer existiert nicht.' });
|
||||
}
|
||||
fields.push('owner_id = ?');
|
||||
values.push(ownerId);
|
||||
}
|
||||
|
||||
if (fields.length === 0) return res.status(400).json({ error: 'Keine Aenderungen uebergeben.' });
|
||||
if (fields.length === 0) return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
|
||||
|
||||
try {
|
||||
values.push(id);
|
||||
@@ -457,7 +457,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
// DELETE /api/leads/:id - Verbindungen entfernt die DB per ON DELETE CASCADE
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Lead-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Lead-ID.' });
|
||||
|
||||
try {
|
||||
const [result] = await pool.query('DELETE FROM crm_leads WHERE id = ?', [id]);
|
||||
|
||||
+14
-14
@@ -128,7 +128,7 @@ router.post('/settings', requireAdmin, async (req, res, next) => {
|
||||
const allowed = Object.keys(MAIL_SETTING_DEFAULTS);
|
||||
const incoming = Object.entries(req.body ?? {}).filter(([key]) => allowed.includes(key));
|
||||
if (incoming.length === 0) {
|
||||
return res.status(400).json({ error: 'Keine bekannten Einstellungen uebergeben.' });
|
||||
return res.status(400).json({ error: 'Keine bekannten Einstellungen übergeben.' });
|
||||
}
|
||||
|
||||
for (const [key, value] of incoming) {
|
||||
@@ -180,7 +180,7 @@ router.post('/sync', requireAdmin, async (req, res, next) => {
|
||||
return res.status(409).json({ error: `Zugangsdaten fehlen: ${missingCredentials().join(', ')}` });
|
||||
}
|
||||
if (isMailSyncRunning()) {
|
||||
return res.status(409).json({ error: 'Ein Abgleich laeuft bereits. Bitte kurz warten.' });
|
||||
return res.status(409).json({ error: 'Ein Abgleich läuft bereits. Bitte kurz warten.' });
|
||||
}
|
||||
|
||||
// Vollstaendiger Neuaufbau auf Wunsch: Delta-Verweis verwerfen. Vorhandene
|
||||
@@ -188,7 +188,7 @@ router.post('/sync', requireAdmin, async (req, res, next) => {
|
||||
if (req.body?.reset === true) await resetDelta();
|
||||
|
||||
const result = await runMailSync({ trigger: 'manuell' });
|
||||
if (result.skipped) return res.status(409).json({ error: `Abgleich uebersprungen: ${result.skipped}` });
|
||||
if (result.skipped) return res.status(409).json({ error: `Abgleich übersprungen: ${result.skipped}` });
|
||||
if (!result.ok) return res.status(502).json({ error: result.error, result });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
@@ -259,7 +259,7 @@ router.get('/messages', async (req, res, next) => {
|
||||
// GET /api/mail/messages/:id
|
||||
router.get('/messages/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Mail-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Mail-ID.' });
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM crm_mail_messages WHERE id = ? LIMIT 1', [id]);
|
||||
@@ -323,7 +323,7 @@ router.get('/messages/:id', async (req, res, next) => {
|
||||
// POST /api/mail/messages/:id/links - Mail an Kontakt und/oder Opportunity haengen
|
||||
router.post('/messages/:id/links', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Mail-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Mail-ID.' });
|
||||
|
||||
const leadId = req.body?.lead_id === undefined || req.body.lead_id === null ? null : Number(req.body.lead_id);
|
||||
const opportunityId =
|
||||
@@ -335,10 +335,10 @@ router.post('/messages/:id/links', async (req, res, next) => {
|
||||
return res.status(400).json({ error: 'Kontakt oder Opportunity angeben.' });
|
||||
}
|
||||
if (leadId !== null && !Number.isInteger(leadId)) {
|
||||
return res.status(400).json({ error: 'Ungueltige Kontakt-ID.' });
|
||||
return res.status(400).json({ error: 'Ungültige Kontakt-ID.' });
|
||||
}
|
||||
if (opportunityId !== null && !Number.isInteger(opportunityId)) {
|
||||
return res.status(400).json({ error: 'Ungueltige Opportunity-ID.' });
|
||||
return res.status(400).json({ error: 'Ungültige Opportunity-ID.' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -378,7 +378,7 @@ router.post('/messages/:id/links', async (req, res, next) => {
|
||||
}
|
||||
|
||||
if (created.length === 0) {
|
||||
return res.status(409).json({ error: 'Diese Verknuepfung besteht bereits.' });
|
||||
return res.status(409).json({ error: 'Diese Verknüpfung besteht bereits.' });
|
||||
}
|
||||
|
||||
res.status(201).json({ ok: true, created });
|
||||
@@ -390,11 +390,11 @@ router.post('/messages/:id/links', async (req, res, next) => {
|
||||
// DELETE /api/mail/links/:id
|
||||
router.delete('/links/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Verknuepfungs-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Verknüpfungs-ID.' });
|
||||
|
||||
try {
|
||||
const [result] = await pool.query('DELETE FROM crm_mail_links WHERE id = ?', [id]);
|
||||
if (result.affectedRows === 0) return res.status(404).json({ error: 'Verknuepfung nicht gefunden.' });
|
||||
if (result.affectedRows === 0) return res.status(404).json({ error: 'Verknüpfung nicht gefunden.' });
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
@@ -416,7 +416,7 @@ router.get('/candidates', async (req, res, next) => {
|
||||
const where = [];
|
||||
const params = [];
|
||||
if (status !== 'alle') {
|
||||
if (!CANDIDATE_STATUSES.has(status)) return res.status(400).json({ error: 'Ungueltiger Status.' });
|
||||
if (!CANDIDATE_STATUSES.has(status)) return res.status(400).json({ error: 'Ungültiger Status.' });
|
||||
where.push('c.status = ?');
|
||||
params.push(status);
|
||||
}
|
||||
@@ -465,7 +465,7 @@ async function loadCandidate(id) {
|
||||
// POST /api/mail/candidates/:id/adopt - Vorschlag zum Kontakt machen
|
||||
router.post('/candidates/:id/adopt', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Vorschlags-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Vorschlags-ID.' });
|
||||
|
||||
try {
|
||||
const candidate = await loadCandidate(id);
|
||||
@@ -479,7 +479,7 @@ router.post('/candidates/:id/adopt', async (req, res, next) => {
|
||||
req.body?.owner_id === undefined || req.body.owner_id === null
|
||||
? req.user.id
|
||||
: Number(req.body.owner_id);
|
||||
if (!Number.isInteger(ownerId)) return res.status(400).json({ error: 'Ungueltige Benutzer-ID.' });
|
||||
if (!Number.isInteger(ownerId)) return res.status(400).json({ error: 'Ungültige Benutzer-ID.' });
|
||||
|
||||
/*
|
||||
* Von Hand uebernommen heisst: richtiger Kontakt. Der Vorbereich "Aus Mails"
|
||||
@@ -505,7 +505,7 @@ router.post('/candidates/:id/adopt', async (req, res, next) => {
|
||||
// POST /api/mail/candidates/:id/reject
|
||||
router.post('/candidates/:id/reject', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Vorschlags-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Vorschlags-ID.' });
|
||||
|
||||
try {
|
||||
const candidate = await loadCandidate(id);
|
||||
|
||||
@@ -28,7 +28,7 @@ function emptyToNull(value) {
|
||||
function normalizeDate(value) {
|
||||
if (value === null || value === undefined || String(value).trim() === '') return { value: null };
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(value).trim());
|
||||
if (!match) return { error: 'Ungueltiges Datum (erwartet JJJJ-MM-TT).' };
|
||||
if (!match) return { error: 'Ungültiges Datum (erwartet JJJJ-MM-TT).' };
|
||||
|
||||
const [, year, month, day] = match;
|
||||
const probe = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
@@ -140,13 +140,13 @@ async function userExists(id) {
|
||||
/** Prueft die Ansprechpartner-Liste `[{ lead_id, role }]`. */
|
||||
async function normalizeContacts(raw) {
|
||||
if (raw === undefined) return undefined;
|
||||
if (!Array.isArray(raw)) return { error: 'Ansprechpartner muessen als Liste uebergeben werden.' };
|
||||
if (!Array.isArray(raw)) return { error: 'Ansprechpartner müssen als Liste übergeben werden.' };
|
||||
|
||||
const seen = new Map();
|
||||
for (const entry of raw) {
|
||||
const leadId = Number(entry?.lead_id ?? entry);
|
||||
if (!Number.isInteger(leadId) || leadId <= 0) {
|
||||
return { error: 'Ungueltige Kontakt-ID bei den Ansprechpartnern.' };
|
||||
return { error: 'Ungültige Kontakt-ID bei den Ansprechpartnern.' };
|
||||
}
|
||||
const role = emptyToNull(entry?.role);
|
||||
if (role !== null && role.length > 120) {
|
||||
@@ -209,7 +209,7 @@ router.post('/', async (req, res, next) => {
|
||||
// etwas anderes gesetzt.
|
||||
const stageGiven = req.body?.stage !== undefined && req.body.stage !== null && req.body.stage !== '';
|
||||
if (stageGiven && !STAGES.has(req.body.stage)) {
|
||||
return res.status(400).json({ error: 'Ungueltige Phase.' });
|
||||
return res.status(400).json({ error: 'Ungültige Phase.' });
|
||||
}
|
||||
const stage = stageGiven ? req.body.stage : 'Neu';
|
||||
|
||||
@@ -241,7 +241,7 @@ router.post('/', async (req, res, next) => {
|
||||
? req.user.id
|
||||
: Number(req.body.owner_id);
|
||||
if (Number.isNaN(ownerId) || !(await userExists(ownerId))) {
|
||||
return res.status(400).json({ error: 'Zustaendiger Benutzer existiert nicht.' });
|
||||
return res.status(400).json({ error: 'Zuständiger Benutzer existiert nicht.' });
|
||||
}
|
||||
|
||||
const contacts = await normalizeContacts(req.body?.contacts ?? []);
|
||||
@@ -289,7 +289,7 @@ router.post('/', async (req, res, next) => {
|
||||
// PATCH /api/opportunities/:id
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Opportunity-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Opportunity-ID.' });
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
@@ -301,7 +301,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (!existing) return res.status(404).json({ error: 'Opportunity nicht gefunden.' });
|
||||
if (!mayModify(req.user, existing)) {
|
||||
return res.status(403).json({
|
||||
error: 'Diese Opportunity darf nur der Ersteller, der Zustaendige oder ein Administrator aendern.',
|
||||
error: 'Diese Opportunity darf nur der Ersteller, der Zuständige oder ein Administrator ändern.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
}
|
||||
|
||||
if (req.body?.stage !== undefined) {
|
||||
if (!STAGES.has(req.body.stage)) return res.status(400).json({ error: 'Ungueltige Phase.' });
|
||||
if (!STAGES.has(req.body.stage)) return res.status(400).json({ error: 'Ungültige Phase.' });
|
||||
fields.push('stage = ?');
|
||||
values.push(req.body.stage);
|
||||
}
|
||||
@@ -367,7 +367,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
const ownerId =
|
||||
req.body.owner_id === null || req.body.owner_id === '' ? null : Number(req.body.owner_id);
|
||||
if (ownerId !== null && (Number.isNaN(ownerId) || !(await userExists(ownerId)))) {
|
||||
return res.status(400).json({ error: 'Zustaendiger Benutzer existiert nicht.' });
|
||||
return res.status(400).json({ error: 'Zuständiger Benutzer existiert nicht.' });
|
||||
}
|
||||
fields.push('owner_id = ?');
|
||||
values.push(ownerId);
|
||||
@@ -377,7 +377,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (contacts?.error) return res.status(400).json({ error: contacts.error });
|
||||
|
||||
if (fields.length === 0 && !contacts) {
|
||||
return res.status(400).json({ error: 'Keine Aenderungen uebergeben.' });
|
||||
return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
|
||||
}
|
||||
|
||||
await conn.beginTransaction();
|
||||
@@ -402,7 +402,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
// DELETE /api/opportunities/:id - Ansprechpartner raeumt die DB per CASCADE
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Opportunity-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Opportunity-ID.' });
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
@@ -413,7 +413,7 @@ router.delete('/:id', async (req, res, next) => {
|
||||
if (!existing) return res.status(404).json({ error: 'Opportunity nicht gefunden.' });
|
||||
if (!mayModify(req.user, existing)) {
|
||||
return res.status(403).json({
|
||||
error: 'Diese Opportunity darf nur der Ersteller, der Zustaendige oder ein Administrator loeschen.',
|
||||
error: 'Diese Opportunity darf nur der Ersteller, der Zuständige oder ein Administrator löschen.',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ router.post('/portals/sync-session', async (req, res) => {
|
||||
try {
|
||||
parsedState = typeof sessionState === 'string' ? JSON.parse(sessionState) : sessionState;
|
||||
} catch {
|
||||
return res.status(400).json({ error: 'Ungueltiges Format - es muss JSON sein.' });
|
||||
return res.status(400).json({ error: 'Ungültiges Format - es muss JSON sein.' });
|
||||
}
|
||||
|
||||
// Drei Formen sind erlaubt: Playwright storageState, reines Cookie-Array
|
||||
@@ -289,7 +289,7 @@ router.post('/portals/sync-session', async (req, res) => {
|
||||
|
||||
const cookies = rawCookies.map(normalizeCookie).filter(Boolean);
|
||||
if (cookies.length === 0) {
|
||||
return res.status(400).json({ error: 'Keine gueltigen Cookies gefunden.' });
|
||||
return res.status(400).json({ error: 'Keine gültigen Cookies gefunden.' });
|
||||
}
|
||||
|
||||
portal.sessionState = JSON.stringify({ cookies, origins });
|
||||
|
||||
@@ -120,7 +120,7 @@ router.post('/', async (req, res, next) => {
|
||||
// PATCH /api/users/:id - bearbeitet Benutzer inkl. Admin
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Benutzer-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Benutzer-ID.' });
|
||||
|
||||
try {
|
||||
const [existingRows] = await pool.query('SELECT * FROM crm_users WHERE id = ? LIMIT 1', [id]);
|
||||
@@ -156,7 +156,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
|
||||
const nextRole = req.body?.role !== undefined ? String(req.body.role) : existing.role;
|
||||
if (req.body?.role !== undefined) {
|
||||
if (!ROLES.has(nextRole)) return res.status(400).json({ error: 'Ungueltige Rolle.' });
|
||||
if (!ROLES.has(nextRole)) return res.status(400).json({ error: 'Ungültige Rolle.' });
|
||||
fields.push('role = ?');
|
||||
values.push(nextRole);
|
||||
}
|
||||
@@ -187,7 +187,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return res.status(400).json({ error: 'Keine Aenderungen uebergeben.' });
|
||||
return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
@@ -209,9 +209,9 @@ router.patch('/:id', async (req, res, next) => {
|
||||
// DELETE /api/users/:id
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Benutzer-ID.' });
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Benutzer-ID.' });
|
||||
if (id === req.user.id) {
|
||||
return res.status(400).json({ error: 'Das eigene Konto kann nicht geloescht werden.' });
|
||||
return res.status(400).json({ error: 'Das eigene Konto kann nicht gelöscht werden.' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -224,7 +224,7 @@ router.delete('/:id', async (req, res, next) => {
|
||||
if (target.role === 'admin' && target.is_active && (await countOtherActiveAdmins(id)) === 0) {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ error: 'Der letzte aktive Administrator kann nicht geloescht werden.' });
|
||||
.json({ error: 'Der letzte aktive Administrator kann nicht gelöscht werden.' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM crm_users WHERE id = ?', [id]);
|
||||
|
||||
@@ -69,7 +69,7 @@ app.use((err, req, res, next) => {
|
||||
// nicht als unbehandelter Serverfehler im Log landen.
|
||||
const status = err?.status || err?.statusCode;
|
||||
if (err?.type === 'entity.parse.failed' || status === 400) {
|
||||
return res.status(400).json({ error: 'Ungueltige Anfrage (JSON konnte nicht gelesen werden).' });
|
||||
return res.status(400).json({ error: 'Ungültige Anfrage (JSON konnte nicht gelesen werden).' });
|
||||
}
|
||||
if (err?.type === 'entity.too.large' || status === 413) {
|
||||
return res.status(413).json({ error: 'Die Anfrage ist zu gross (max. 6 MB).' });
|
||||
|
||||
@@ -18,7 +18,7 @@ const AVATAR_MAX_LENGTH = 3_000_000;
|
||||
export function normalizeColor(value) {
|
||||
if (value === null || value === undefined || String(value).trim() === '') return { value: null };
|
||||
const color = String(value).trim().toLowerCase();
|
||||
if (!COLOR_RE.test(color)) return { error: 'Ungueltige Farbe (erwartet z. B. #6ba04a).' };
|
||||
if (!COLOR_RE.test(color)) return { error: 'Ungültige Farbe (erwartet z. B. #6ba04a).' };
|
||||
return { value: color };
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function normalizeAvatar(value) {
|
||||
return { error: 'Das Foto ist zu gross. Bitte ein kleineres Bild verwenden.' };
|
||||
}
|
||||
if (!AVATAR_RE.test(avatar)) {
|
||||
return { error: 'Ungueltiges Foto (erwartet eine Bild-Data-URL oder eine http(s)-Adresse).' };
|
||||
return { error: 'Ungültiges Foto (erwartet eine Bild-Data-URL oder eine http(s)-Adresse).' };
|
||||
}
|
||||
return { value: avatar };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user