feat: Interviews, Mail-Papierkorb, Kontaktart

Drei Aenderungen aus dem Alltagsgebrauch:

- Der Kalender heisst jetzt "Interviews" - dafuer wird er benutzt. Der Wert in
  ViewState wandert mit, damit Menue und Kopfzeile nicht auseinanderlaufen.
  Sichtbare Reste ("Kalenderfarbe") heissen jetzt Terminfarbe.

- Mails haben einen Papierkorb, der unter dem Menuepunkt aufklappt. Loeschen
  heisst dabei ausschliesslich: im CRM. Im Postfach crm@perinco.de bleibt die
  Mail unberuehrt - Schreibrechte darauf hat die Anwendung nicht. Damit der
  Delta-Abgleich geloeschte Mails nicht wieder einliest, bleibt nach dem Leeren
  eine leere Zeile als Merker stehen (crm_purged); runMailSync() ueberspringt
  alles, was im CRM geloescht ist. Zurueckholen und Leeren darf jeder
  Angemeldete, Leeren nach einer Warnung.

- Der Kontaktstatus (Neu .. Verloren) wird zur Kontaktart: Recruiter, Partner,
  Endkunde. Die Spalte heisst jetzt kind, weil "Status" das Feld falsch
  benannte. migrateLeadKind() stellt bestehende Datenbanken um und setzt alle
  vorhandenen Kontakte auf Recruiter - welche Art jemand ist, sagt der alte
  Status nicht (so mit dem Nutzer entschieden).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-18 11:12:49 +02:00
co-authored by Claude Opus 5
parent 025390e18a
commit 8741a9ae0c
39 changed files with 719 additions and 199 deletions
+22 -15
View File
@@ -1,9 +1,10 @@
export type ViewState =
| 'Opportunities'
| 'Kalender'
| 'Interviews'
| 'Kontakte'
| 'Aus Mails'
| 'Mails'
| 'Papierkorb'
| 'Scanner'
| 'Unpassend'
| 'Suchagenten'
@@ -37,6 +38,13 @@ export const SCANNER_VIEWS: ViewState[] = ['Scanner', 'Unpassend', 'Suchagenten'
*/
export const CONTACT_VIEWS: ViewState[] = ['Kontakte', 'Aus Mails'];
/**
* Mailbereich. "Mails" ist das gespiegelte Postfach, "Papierkorb" enthaelt die
* im CRM geloeschten Mails. Geloescht heisst nur: das CRM listet sie nicht mehr -
* im Postfach crm@perinco.de bleiben sie unberuehrt.
*/
export const MAIL_VIEWS: ViewState[] = ['Mails', 'Papierkorb'];
export type Role = 'admin' | 'user';
export interface AppUser {
@@ -45,7 +53,7 @@ export interface AppUser {
email: string | null;
name: string;
role: Role;
/** Persoenliche Kalenderfarbe als Hex; null = automatisch aus der Palette. */
/** Persoenliche Terminfarbe als Hex; null = automatisch aus der Palette. */
color?: string | null;
/** Benutzerfoto als Data-URL oder externe URL; null = Initiale anzeigen. */
avatar?: string | null;
@@ -55,7 +63,7 @@ export interface AppUser {
}
/**
* Farbpalette fuer Benutzer im Kalender. Bewusst kontraststark untereinander
* Farbpalette fuer Benutzer in den Terminen. Bewusst kontraststark untereinander
* und dunkel genug fuer weisse Schrift; das CI-Gruen steht an erster Stelle.
*/
export const USER_COLORS = [
@@ -78,16 +86,15 @@ export function userColor(user?: { id: number; color?: string | null } | null):
return USER_COLORS[Math.abs(user.id) % USER_COLORS.length];
}
export const LEAD_STATUSES = [
'Neu',
'Kontaktiert',
'Qualifiziert',
'Angebot',
'Gewonnen',
'Verloren',
] as const;
/**
* Art eines Kontakts - in der Oberflaeche "Art". Frueher stand hier ein
* Vertriebsstatus (Neu .. Verloren); der sagte nichts darueber, mit wem man es
* zu tun hat. Bestehende Kontakte wurden bei der Umstellung auf 'Recruiter'
* gesetzt (siehe migrateLeadKind() im Backend).
*/
export const LEAD_KINDS = ['Recruiter', 'Partner', 'Endkunde'] as const;
export type LeadStatus = (typeof LEAD_STATUSES)[number];
export type LeadKind = (typeof LEAD_KINDS)[number];
/**
* Herkunft von Kontakten, die aus dem Postfach entstanden sind. Steht in
@@ -115,12 +122,12 @@ export interface Lead {
zip: string | null;
city: string | null;
country: string | null;
status: LeadStatus;
kind: LeadKind;
source: string | null;
/**
* true = Eintrag im Vorbereich "Aus Mails". Solche Kontakte stammen aus dem
* Postfachabgleich und erscheinen nicht in der Kontaktliste, in Opportunities
* oder im Kalender - erst die Uebernahme macht daraus einen Kontakt.
* oder in Terminen - erst die Uebernahme macht daraus einen Kontakt.
*/
mail_pool: boolean;
/**
@@ -262,7 +269,7 @@ export function contactFullName(contact: Pick<OpportunityContact, 'first_name' |
return `${contact.first_name} ${contact.last_name}`.trim();
}
// ---------- Kalender ----------
// ---------- Interviews (Termine) ----------
export interface EventParticipant {
id: number;