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:
co-authored by
Claude Opus 5.5
parent
e1b69fffbd
commit
69001e9687
@@ -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
|
||||
// ==========================================
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user