feat(scanner): Schwellen im Dashboard pro Benutzer

"Anzeige ab %" und "Hinweis ab %" liegen jetzt am CRM-Benutzer
(crm_users.scanner_display_threshold/scanner_match_threshold) statt global
in den Scanner-Einstellungen. NULL = globaler Standard. Die Mail-Schwelle
bleibt global, die KI-Suche nimmt die Anzeigeschwelle des Suchenden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-10-27 08:50:40 +01:00
co-authored by Claude Opus 5.5
parent e1b69fffbd
commit 69001e9687
11 changed files with 114 additions and 24 deletions
+46
View File
@@ -4,6 +4,7 @@ import fs from 'fs';
import path from 'path';
import { Op } from 'sequelize';
import { Agent, Portal, Project, Match, Setting } from '../scanner/models/index.js';
import { pool } from '../db.js';
import { parsePdfCv } from '../scanner/services/pdfParser.js';
import { encrypt } from '../scanner/utils/crypto.js';
import { ensureUploadDir, resolveCvPath } from '../scanner/uploadDir.js';
@@ -518,6 +519,51 @@ router.post('/settings', async (req, res) => {
}
});
/*
* Persoenliche Schwellen fuers Dashboard. Sie liegen am CRM-Benutzer und nicht
* in den Scanner-Einstellungen, damit jeder seine eigene Ansicht hat. null =
* nichts eigenes gesetzt, dann gilt der globale Standard.
*/
function parseThreshold(value) {
if (value === null || value === undefined || value === '') return null;
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed)) return undefined;
return Math.max(0, Math.min(100, parsed));
}
router.get('/my-thresholds', async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT scanner_display_threshold, scanner_match_threshold FROM crm_users WHERE id = ?',
[req.user.id]
);
const row = rows[0] || {};
res.json({
display_threshold: row.scanner_display_threshold ?? null,
match_threshold: row.scanner_match_threshold ?? null,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.put('/my-thresholds', async (req, res) => {
try {
const display = parseThreshold(req.body?.display_threshold);
const match = parseThreshold(req.body?.match_threshold);
if (display === undefined || match === undefined) {
return res.status(400).json({ error: 'Schwellen müssen Zahlen zwischen 0 und 100 sein.' });
}
await pool.query(
'UPDATE crm_users SET scanner_display_threshold = ?, scanner_match_threshold = ? WHERE id = ?',
[display, match, req.user.id]
);
res.json({ display_threshold: display, match_threshold: match });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ==========================================
// MANUELLER SCAN
// ==========================================
+12 -4
View File
@@ -200,7 +200,7 @@ router.post('/ai', async (req, res, next) => {
// "dazu steht nichts im Bestand", obwohl die Person in der Liste steht.
const icon = await fetchIconContacts();
const scannerMatches = await loadScannerMatches();
const scannerMatches = await loadScannerMatches(req.user.id);
const truncated = [leads, opportunities, acquisitions, events, scannerMatches].some(
(list) => list.length >= AI_LIMIT
@@ -248,10 +248,18 @@ router.post('/ai', async (req, res, next) => {
* Anzeigeschwelle, ohne Projektbeschreibung - die ist lang und wuerde den
* Kontext sprengen. Faellt der Scanner aus, fehlt nur dieser Teil.
*/
async function loadScannerMatches() {
async function loadScannerMatches(userId) {
try {
const setting = await ScannerSetting.findOne({ where: { keyName: 'display_threshold' } });
const threshold = Number(setting?.value) || 0;
// Die eigene Schwelle des Suchenden geht vor, sonst der globale Standard.
const [users] = await pool.query(
'SELECT scanner_display_threshold FROM crm_users WHERE id = ?',
[userId]
);
let threshold = users[0]?.scanner_display_threshold;
if (threshold === null || threshold === undefined) {
const setting = await ScannerSetting.findOne({ where: { keyName: 'display_threshold' } });
threshold = Number(setting?.value) || 0;
}
const [rows] = await scannerDb.query(
`SELECT m.id, m.match_percentage, m.status, m.created_at AS found_at,
a.name AS agent, p.title AS project_title, p.company, p.portal, p.location,
+6
View File
@@ -34,6 +34,12 @@ ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS avatar LONGTEXT NULL AFTER color;
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme VARCHAR(32) NULL AFTER avatar;
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme_mode VARCHAR(16) NULL AFTER theme;
-- Eigene Schwellen im Scanner-Dashboard ("Anzeige ab %", "Hinweis ab %").
-- NULL = der Standard aus den Scanner-Einstellungen gilt. Die Schwelle fuer
-- Mail-Benachrichtigungen bleibt global, sie laeuft serverseitig ohne Benutzer.
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS scanner_display_threshold TINYINT UNSIGNED NULL AFTER theme_mode;
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS scanner_match_threshold TINYINT UNSIGNED NULL AFTER scanner_display_threshold;
-- Objekt-ID des Microsoft-Kontos (Entra), gesetzt bei der ersten Anmeldung per
-- SSO. Sie ist die stabile Zuordnung: die E-Mail-Adresse eines Menschen kann
-- sich aendern, die Objekt-ID nicht. NULL = noch nie per Microsoft angemeldet;