Bisher verschwand ein Fund mit der Uebernahme aus Eingang und Archiv. Beim Durchgehen blieb damit offen, ob er schon angefasst wurde. Jetzt bleibt der Eintrag stehen und zeigt statt "Opportunity" den blauen Knopf "Uebernommen" mit Haken; ein Klick springt zur Opportunity. Dafuer entscheidet nicht mehr IS_OPEN ueber die Listen, sondern die neuen Regeln IN_INBOX und RETURNED: ein Rueckfaeller (Opportunity im Papierkorb oder geloescht) gehoert weiter ins Archiv, alles andere bleibt in seiner Liste. Zaehler und Menuezahl folgen derselben Regel. IS_OPEN bleibt fuer den Loeschlauf: was zu einer lebenden Opportunity gehoert, wird nicht weggeraeumt. Solange die Opportunity lebt, sind Bearbeiten, Loeschen und "Zurueck in die Akquise" am Eintrag gesperrt - er ist ihr Herkunftsnachweis, und der Rueckweg von der Opportunity braucht ihn. Das Loeschen weist auch das Backend ab, nicht nur die Oberflaeche. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
5.2 KiB
JavaScript
132 lines
5.2 KiB
JavaScript
/*
|
|
* 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));
|
|
}
|