feat(opportunities): Phasen des Vermittlungsprozesses, Kanban zuerst, Zahlen im Menue

Drei Dinge:

- Die Phasen sind jetzt die des Vermittlungsprozesses: Neu, Beworben,
  Erstgespraech Berater, Kundenvorstellung, Interview Kunde, Zusage, Beauftragt,
  Absage. Offen ist alles bis einschliesslich Zusage - dort ist der Einsatz
  bestaetigt, die Beauftragung steht aber noch aus. migrateOpportunityStages()
  stellt bestehende Datenbanken um und setzt alle vorhandenen Opportunities auf
  "Neu": in welcher der neuen Phasen ein Projekt steckt, sagt der alte Trichter
  nicht (so mit dem Nutzer entschieden).

  Der Datenbankwert heisst 'Erstgespraech Berater' ohne Umlaut - Projektkonvention
  fuer Datenbankwerte. Angezeigt wird er ueber STAGE_LABELS; dazu gibt es
  STAGE_HINTS mit der Erlaeuterung je Phase als Titelhinweis an Auswahl, Badge
  und Spaltenkopf.

- Kanban ist die erste Ansicht der Opportunities. Die Pipeline ist die
  Arbeitsansicht, Kachel und Liste sind zum Nachschauen.

- Hinter "Archiv", beiden "Papierkorb"-Punkten und "Unpassend" steht die Anzahl,
  wie schon bei "Aus Mails". Die Zahlen kommen aus einem eigenen Endpunkt
  (/api/badges), weil sie alle ins Menue gehoeren und zusammen kommen sollen;
  Inhalte laedt weiter jede Ansicht selbst. Der Scanner liegt in einer eigenen
  Datenbank und kann fehlen - sein Zaehler ist dann null und die Leiste zeigt
  dort einfach keine Zahl. Der Aufruf haengt bewusst hinter den Daten und nicht
  im Promise.all: faellt er aus, bleibt das Menue ohne Zahlen statt die Ansicht
  mit einem Fehler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-18 14:34:41 +02:00
co-authored by Claude Opus 5
parent 1ac0e2d2c7
commit 42639c65fa
17 changed files with 313 additions and 55 deletions
+60
View File
@@ -0,0 +1,60 @@
import express from 'express';
import { pool } from '../db.js';
import { requireAuth } from '../middleware/auth.js';
import { Match } from '../scanner/models/index.js';
const router = express.Router();
router.use(requireAuth);
/*
* Zaehler fuer die Seitenleiste: Akquise-Archiv, Papierkorb der Opportunities,
* Papierkorb der Mails und die unpassenden Scanner-Treffer.
*
* Ein Endpunkt statt vier: die Zahlen stehen alle im Menue und sollen zusammen
* kommen. Die Ansichten selbst laden weiter ihre eigenen Daten - hier gibt es
* ausschliesslich Zahlen, keine Inhalte.
*
* Der Scanner liegt in einer eigenen Datenbank und kann fehlen; sein Zaehler ist
* dann null und die Leiste zeigt an der Stelle einfach keine Zahl. Ein Menue
* 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,
(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]
);
let scannerRejected = null;
try {
scannerRejected = await Match.count({ where: { status: 'rejected' } });
} catch {
// Scanner-Datenbank nicht erreichbar - dann eben keine Zahl.
}
res.json({
acquisition_archive: Number(counts.acquisition_archive),
opportunity_trash: Number(counts.opportunity_trash),
mail_trash: Number(counts.mail_trash),
scanner_rejected: scannerRejected,
});
} catch (err) {
next(err);
}
});
export default router;