/* * 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. * * `NOT_DELETED` gehoert zu jeder dieser Abfragen dazu: der Papierkorb ist eine * dritte Liste, und was dort liegt, hat in Eingang, Archiv und Auswertung * nichts zu suchen. */ /** 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; /** So lange liegt ein von Hand geloeschter Eintrag im Papierkorb. */ export const TRASH_DELETE_AFTER_DAYS = 90; /** * Nicht im Papierkorb. Steht in jeder Abfrage von Eingang, Archiv, Zaehlern und * Auswertung: ein geloeschter Eintrag ist ueberall weg, nur nicht in der * Datenbank. */ export const NOT_DELETED = 'a.deleted_at IS NULL'; /** Liegt im Papierkorb. */ export const IN_TRASH = 'a.deleted_at IS NOT NULL'; /** * 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 noch nicht abgearbeitet ist: nie uebernommen, oder die * Opportunity ist weg. Abgeleitet statt gespeichert - wird die Opportunity * zurueckgeholt, gilt der Eintrag von selbst wieder als uebernommen. * * Fuer die Listen entscheidet das nichts mehr (uebernommene Eintraege bleiben * stehen und zeigen dort "Uebernommen"), wohl aber fuer den Loeschlauf: was zu * einer lebenden Opportunity gehoert, wird nie weggeraeumt. */ export const IS_OPEN = `(a.converted_at IS NULL OR ${OPPORTUNITY_GONE})`; /** * Rueckfaeller: schon einmal uebernommen, aber die Opportunity ist weg. Ein * solcher Eintrag gehoert ins Archiv - unabhaengig vom Alter, er ist * abgearbeitet, aber nicht verschwunden. */ export const RETURNED = `(a.converted_at IS NOT NULL AND ${OPPORTUNITY_GONE})`; /** * Steht im Eingang: alles innerhalb der Archivfrist, ausser Rueckfaellern. * Uebernommene Eintraege bleiben hier stehen: verschwaenden sie sofort, bliebe * beim Durchgehen offen, ob ein Fund schon angefasst wurde. Erwartet die * Archivfrist als Platzhalter. */ export const IN_INBOX = `(NOT ${RETURNED} AND a.found_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY))`; /** * Steht im Archiv: jeder Rueckfaeller (unabhaengig vom Alter), plus alles, was * aelter als die Archivfrist ist. Erwartet die Frist als Platzhalter. */ export const IN_ARCHIVE = `(${RETURNED} 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)'; /* * Mehrere Berater in einem Feld. * * Ein Fund passt oft zu mehr als einem Berater - fuer jeden davon geht eine * Bewerbung heraus. Die Namen stehen kommagetrennt in `crm_acquisitions.consultant` * und nicht in einer eigenen Tabelle: sie sind reiner Text (die Auswahlliste in * crm_acquisition_options ist eine Pflegehilfe, kein Fremdschluessel), und Suche, * Sortierung und Anzeige der Liste arbeiten weiter auf einem Feld. * * Der Verlauf (crm_acquisition_history) und die Auswertung zerlegen die Liste * dagegen wieder: dort steht ein Berater je Zeile, sonst waere "Nico, Lisa" ein * eigener Balken neben "Nico" und "Lisa". */ /** Trennzeichen der gespeicherten Liste - mit Leerzeichen, das ist auch die Anzeige. */ export const CONSULTANT_SEPARATOR = ', '; /** Grenze der Spalte `consultant`; die Liste wird als Ganzes geprueft. */ export const CONSULTANT_MAX_LENGTH = 500; /** * Zerlegt das gespeicherte Feld in einzelne Namen. Leere Stuecke fallen weg * (ein Komma zu viel ist kein Berater), Dubletten ebenso - beim Vergleich ohne * Ruecksicht auf Gross- und Kleinschreibung, damit "nico" und "Nico" nicht * zwei Balken werden. Erhalten bleibt die erste Schreibweise. */ export function splitConsultants(value) { if (value === null || value === undefined) return []; const seen = new Set(); const names = []; for (const part of String(value).split(',')) { const name = part.trim(); if (name === '') continue; const key = name.toLowerCase(); if (seen.has(key)) continue; seen.add(key); names.push(name); } return names; } /** Setzt die Namen wieder zur gespeicherten Form zusammen; leer wird zu null. */ export function joinConsultants(names) { const list = names.filter((name) => String(name).trim() !== ''); return list.length === 0 ? null : list.join(CONSULTANT_SEPARATOR); } /** Vereinheitlicht, was aus dem Formular kommt: getrimmt, ohne Dubletten, ohne Leere. */ export function normalizeConsultants(value) { return joinConsultants(splitConsultants(value)); }