fix(akquise): Zahl am Menuepunkt Archiv folgte der alten Regel
Die Liste zeigte die zurueckgefallene Bewerbung, der Zaehler am Menue nicht: er filterte weiter auf "nie uebernommen". Genau der Fehler, den zwei Kopien derselben Regel machen. Fristen und Bedingungen stehen jetzt in server/src/acquisitionRules.js und werden von der Liste, den Zaehlern, dem Aufraeumlauf und der Menuezahl gemeinsam benutzt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c380d05b42
commit
c59115c728
@@ -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)';
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { pool } from '../db.js';
|
import { pool } from '../db.js';
|
||||||
import { requireAuth } from '../middleware/auth.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();
|
const router = express.Router();
|
||||||
router.use(requireAuth);
|
router.use(requireAuth);
|
||||||
@@ -19,31 +27,6 @@ router.use(requireAuth);
|
|||||||
* woher eine Opportunity kommt. Nur der Eingang und das Archiv zeigen sie nicht.
|
* 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.
|
* Felder, deren Inhalte als Auswahlliste in den Einstellungen gepflegt werden.
|
||||||
* Die Werte selbst stehen in crm_acquisition_options, nicht hier - deshalb
|
* 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') {
|
} else if (view === 'archiv') {
|
||||||
// Die untere Grenze steht bewusst auch hier: der Loeschlauf raeumt nur
|
// Die untere Grenze steht bewusst auch hier: der Loeschlauf raeumt nur
|
||||||
// stuendlich, angezeigt werden soll nichts, was schon faellig ist.
|
// stuendlich, angezeigt werden soll nichts, was schon faellig ist.
|
||||||
where.push(
|
where.push(IN_ARCHIVE);
|
||||||
`(a.converted_at IS NOT NULL
|
where.push(NOT_DUE);
|
||||||
OR a.found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY))`
|
|
||||||
);
|
|
||||||
where.push('a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)');
|
|
||||||
params.push(ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS);
|
params.push(ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS);
|
||||||
} else if (view !== 'alle') {
|
} else if (view !== 'alle') {
|
||||||
return res.status(400).json({ error: 'Unbekannte Ansicht.' });
|
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.
|
// muesste sonst zwei Listen laden, um eine Zahl zu kennen.
|
||||||
const [[counts]] = await pool.query(
|
const [[counts]] = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
SUM(a.converted_at IS NULL
|
SUM(a.converted_at IS NULL AND ${NOT_DUE}) AS eingang,
|
||||||
AND a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)) AS eingang,
|
SUM(${IN_ARCHIVE} AND ${NOT_DUE}) AS archiv
|
||||||
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
|
|
||||||
FROM crm_acquisitions a
|
FROM crm_acquisitions a
|
||||||
LEFT JOIN crm_opportunities opp ON opp.id = a.opportunity_id
|
LEFT JOIN crm_opportunities opp ON opp.id = a.opportunity_id
|
||||||
WHERE ${IS_OPEN}`,
|
WHERE ${IS_OPEN}`,
|
||||||
|
// Reihenfolge der Platzhalter: Eingangsfrist, Archivfrist, Loeschfrist.
|
||||||
[ARCHIVE_AFTER_DAYS, ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS]
|
[ARCHIVE_AFTER_DAYS, ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import express from 'express';
|
|||||||
import { pool } from '../db.js';
|
import { pool } from '../db.js';
|
||||||
import { requireAuth } from '../middleware/auth.js';
|
import { requireAuth } from '../middleware/auth.js';
|
||||||
import { Match } from '../scanner/models/index.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();
|
const router = express.Router();
|
||||||
router.use(requireAuth);
|
router.use(requireAuth);
|
||||||
@@ -19,24 +26,21 @@ router.use(requireAuth);
|
|||||||
* ohne Zahl ist besser als ein Menue mit Fehlermeldung.
|
* 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
|
// GET /api/badges
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const [[counts]] = await pool.query(
|
const [[counts]] = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
(SELECT COUNT(*) FROM crm_acquisitions
|
/* Dieselbe Regel wie die Liste - sie steht in acquisitionRules.js,
|
||||||
WHERE converted_at IS NULL
|
damit Zahl und Liste nicht auseinanderlaufen. */
|
||||||
AND found_at < DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
(SELECT COUNT(*) FROM crm_acquisitions a
|
||||||
AND found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)) AS acquisition_archive,
|
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
|
(SELECT COUNT(*) FROM crm_opportunities
|
||||||
WHERE deleted_at IS NOT NULL) AS opportunity_trash,
|
WHERE deleted_at IS NOT NULL) AS opportunity_trash,
|
||||||
(SELECT COUNT(*) FROM crm_mail_messages
|
(SELECT COUNT(*) FROM crm_mail_messages
|
||||||
WHERE crm_deleted_at IS NOT NULL AND crm_purged = 0) AS mail_trash`,
|
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;
|
let scannerRejected = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user