diff --git a/server/src/acquisitionRules.js b/server/src/acquisitionRules.js new file mode 100644 index 0000000..cef4e41 --- /dev/null +++ b/server/src/acquisitionRules.js @@ -0,0 +1,44 @@ +/* + * Regeln der Akquise an einer Stelle: Fristen und die Frage, was ueberhaupt in + * Eingang oder Archiv gehoert. + * + * Sie werden von routes/acquisitions.js (Listen, Zaehler, Aufraeumlauf) und von + * routes/badges.js (Zahl am Menuepunkt) gebraucht. Lagen sie in beiden Dateien, + * wuerde eine Aenderung die andere Stelle vergessen - genau das ist beim + * Zurueckfallen einer Bewerbung passiert: die Liste zeigte den Eintrag, der + * Zaehler am Menue nicht. + * + * Die Zeichenketten sind SQL-Bedingungen und erwarten die Aliase `a` fuer + * crm_acquisitions und `opp` fuer die per LEFT JOIN angehaengte Opportunity. + */ + +/** Grenze zwischen Eingang und Archiv, in Tagen. */ +export const ARCHIVE_AFTER_DAYS = 14; + +/** Ab hier loescht das Archiv selbst. */ +export const DELETE_AFTER_DAYS = 90; + +/** + * Die Opportunity zu einem Eintrag ist verschwunden - im Papierkorb oder + * endgueltig geloescht. `opp.id IS NULL` deckt beides ab: nie uebernommen + * (opportunity_id ist NULL) und endgueltig geloescht (der Fremdschluessel wurde + * dabei auf NULL gesetzt). + */ +export const OPPORTUNITY_GONE = '(opp.id IS NULL OR opp.deleted_at IS NOT NULL)'; + +/** + * Bewerbung, die (wieder) in Eingang oder Archiv gehoert: nie uebernommen, oder + * die Opportunity ist weg. Abgeleitet statt gespeichert - wird die Opportunity + * zurueckgeholt, verschwindet der Eintrag von selbst wieder. + */ +export const IS_OPEN = `(a.converted_at IS NULL OR ${OPPORTUNITY_GONE})`; + +/** + * Steht im Archiv: alles Uebernommene, dessen Opportunity weg ist (unabhaengig + * vom Alter - es ist abgearbeitet, aber nicht verschwunden), plus alles, was + * aelter als die Archivfrist ist. Erwartet die Frist als Platzhalter. + */ +export const IN_ARCHIVE = '(a.converted_at IS NOT NULL OR a.found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY))'; + +/** Untere Grenze: was schon faellig ist, wird nicht mehr gezeigt. */ +export const NOT_DUE = 'a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)'; diff --git a/server/src/routes/acquisitions.js b/server/src/routes/acquisitions.js index 8b293fd..e2cffe6 100644 --- a/server/src/routes/acquisitions.js +++ b/server/src/routes/acquisitions.js @@ -1,6 +1,14 @@ import express from 'express'; import { pool } from '../db.js'; import { requireAuth } from '../middleware/auth.js'; +import { + ARCHIVE_AFTER_DAYS, + DELETE_AFTER_DAYS, + IN_ARCHIVE, + IS_OPEN, + NOT_DUE, + OPPORTUNITY_GONE, +} from '../acquisitionRules.js'; const router = express.Router(); router.use(requireAuth); @@ -19,31 +27,6 @@ router.use(requireAuth); * woher eine Opportunity kommt. Nur der Eingang und das Archiv zeigen sie nicht. */ -/* - * Ist die Opportunity zu einem Eintrag verschwunden - im Papierkorb oder - * endgueltig geloescht -, faellt die Bewerbung ins Archiv zurueck. Sonst wuerde - * sie einfach nicht mehr existieren: aus der Akquise ist sie mit der Uebernahme - * heraus, und die Opportunity gibt es auch nicht mehr. - * - * Abgeleitet statt gespeichert: `converted_at` bleibt stehen. Wird die - * Opportunity aus dem Papierkorb zurueckgeholt, verschwindet der Eintrag von - * selbst wieder aus dem Archiv - es gibt keinen Zustand, der nachgezogen werden - * muesste. - * - * `opp.id IS NULL` deckt zwei Faelle ab: nie uebernommen (opportunity_id ist - * NULL) und endgueltig geloescht (der Fremdschluessel wurde dabei auf NULL - * gesetzt). Zusammen mit converted_at ergibt das den Unterschied. - */ -const OPPORTUNITY_GONE = '(opp.id IS NULL OR opp.deleted_at IS NOT NULL)'; - -/** Bewerbung, die (wieder) in Eingang oder Archiv gehoert. */ -const IS_OPEN = `(a.converted_at IS NULL OR ${OPPORTUNITY_GONE})`; - -/** Grenze zwischen Eingang und Archiv, in Tagen. */ -const ARCHIVE_AFTER_DAYS = 14; -/** Ab hier loescht das Archiv selbst. */ -const DELETE_AFTER_DAYS = 90; - /* * Felder, deren Inhalte als Auswahlliste in den Einstellungen gepflegt werden. * Die Werte selbst stehen in crm_acquisition_options, nicht hier - deshalb @@ -501,11 +484,8 @@ router.get('/', async (req, res, next) => { } else if (view === 'archiv') { // Die untere Grenze steht bewusst auch hier: der Loeschlauf raeumt nur // stuendlich, angezeigt werden soll nichts, was schon faellig ist. - where.push( - `(a.converted_at IS NOT NULL - OR a.found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY))` - ); - where.push('a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)'); + where.push(IN_ARCHIVE); + where.push(NOT_DUE); params.push(ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS); } else if (view !== 'alle') { return res.status(400).json({ error: 'Unbekannte Ansicht.' }); @@ -521,14 +501,12 @@ router.get('/', async (req, res, next) => { // muesste sonst zwei Listen laden, um eine Zahl zu kennen. const [[counts]] = await pool.query( `SELECT - SUM(a.converted_at IS NULL - AND a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)) AS eingang, - SUM((a.converted_at IS NOT NULL - OR a.found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY)) - AND a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)) AS archiv + SUM(a.converted_at IS NULL AND ${NOT_DUE}) AS eingang, + SUM(${IN_ARCHIVE} AND ${NOT_DUE}) AS archiv FROM crm_acquisitions a LEFT JOIN crm_opportunities opp ON opp.id = a.opportunity_id WHERE ${IS_OPEN}`, + // Reihenfolge der Platzhalter: Eingangsfrist, Archivfrist, Loeschfrist. [ARCHIVE_AFTER_DAYS, ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS] ); diff --git a/server/src/routes/badges.js b/server/src/routes/badges.js index 531026a..d179097 100644 --- a/server/src/routes/badges.js +++ b/server/src/routes/badges.js @@ -2,6 +2,13 @@ import express from 'express'; import { pool } from '../db.js'; import { requireAuth } from '../middleware/auth.js'; import { Match } from '../scanner/models/index.js'; +import { + ARCHIVE_AFTER_DAYS, + DELETE_AFTER_DAYS, + IN_ARCHIVE, + IS_OPEN, + NOT_DUE, +} from '../acquisitionRules.js'; const router = express.Router(); router.use(requireAuth); @@ -19,24 +26,21 @@ router.use(requireAuth); * ohne Zahl ist besser als ein Menue mit Fehlermeldung. */ -/** Grenze zwischen Eingang und Archiv der Akquise - wie in routes/acquisitions.js. */ -const ACQUISITION_ARCHIVE_AFTER_DAYS = 14; -const ACQUISITION_DELETE_AFTER_DAYS = 90; - // GET /api/badges router.get('/', async (req, res, next) => { try { const [[counts]] = await pool.query( `SELECT - (SELECT COUNT(*) FROM crm_acquisitions - WHERE converted_at IS NULL - AND found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY) - AND found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)) AS acquisition_archive, + /* Dieselbe Regel wie die Liste - sie steht in acquisitionRules.js, + damit Zahl und Liste nicht auseinanderlaufen. */ + (SELECT COUNT(*) FROM crm_acquisitions a + LEFT JOIN crm_opportunities opp ON opp.id = a.opportunity_id + WHERE ${IS_OPEN} AND ${IN_ARCHIVE} AND ${NOT_DUE}) AS acquisition_archive, (SELECT COUNT(*) FROM crm_opportunities WHERE deleted_at IS NOT NULL) AS opportunity_trash, (SELECT COUNT(*) FROM crm_mail_messages WHERE crm_deleted_at IS NOT NULL AND crm_purged = 0) AS mail_trash`, - [ACQUISITION_ARCHIVE_AFTER_DAYS, ACQUISITION_DELETE_AFTER_DAYS] + [ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS] ); let scannerRejected = null;