diff --git a/docs/02-datenmodell.md b/docs/02-datenmodell.md index 9b7a900..024dc54 100644 --- a/docs/02-datenmodell.md +++ b/docs/02-datenmodell.md @@ -25,7 +25,7 @@ erDiagram crm_opportunities ||--o{ crm_opportunity_contacts : hat crm_users ||--o{ crm_acquisitions : created_by crm_acquisitions |o--o| crm_opportunities : "wird zu" - crm_acquisition_options ||--o{ crm_acquisitions : "fuellt Von, Anbieter, Kanal, Berater" + crm_acquisition_options ||--o{ crm_acquisitions : "fuellt Von, Kanal, Berater" crm_users { int id PK @@ -91,7 +91,7 @@ erDiagram } crm_acquisition_options { int id PK - enum field "sender | provider | channel | consultant" + enum field "sender | channel | consultant" varchar value "Inhalt des Dropdowns" } crm_events { @@ -126,14 +126,16 @@ Sechs Eigenheiten, die man kennen muss: - **`avatar` ist eine Data-URL in der Datenbank.** Der Browser verkleinert jedes Bild vor dem Upload auf 256 px (`src/lib/imageCompress.ts`), aus 2,9 MB werden wenige Kilobyte. -- **Die Dropdowns der Akquise stehen in einer Tabelle, nicht in vier.** +- **Die Dropdowns der Akquise stehen in einer Tabelle, nicht in drei.** `crm_acquisition_options` unterscheidet die Listen über die Spalte `field`. Die - vier Felder verhalten sich gleich, also ist jede weitere Liste ein Wert im - ENUM statt einer neuen Tabelle samt Route und Oberfläche. Der Unique-Schlüssel - auf `(field, value)` greift über die Kollation `utf8mb4_unicode_ci` auch bei - abweichender Groß-/Kleinschreibung - „hays" ist neben „Hays" kein neuer Wert. + Felder verhalten sich gleich, also ist jede weitere Liste ein Wert im ENUM + statt einer neuen Tabelle samt Route und Oberfläche. Der Unique-Schlüssel auf + `(field, value)` greift über die Kollation `utf8mb4_unicode_ci` auch bei + abweichender Groß-/Kleinschreibung - „mail" ist neben „Mail" kein neuer Wert. Die Akquise-Einträge halten den **Text**, keinen Fremdschlüssel: wird ein Wert aus der Liste entfernt, bleiben die Einträge lesbar und änderbar. + `crm_acquisitions.provider` (Anbieter) gehört nicht dazu - das Feld ist + Freitext, seine Vorschläge kommen aus den vorhandenen Einträgen. - **`crm_acquisitions` hat keine Zustandsspalte.** Eingang, Archiv und Löschung ergeben sich aus `found_at` (14 bzw. 90 Tage) und `converted_at`. Eine Spalte, die ein Job umsetzen müsste, kann von der Wirklichkeit abweichen - eine diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md index 44194ed..29e1d30 100644 --- a/docs/07-anleitung-benutzer.md +++ b/docs/07-anleitung-benutzer.md @@ -89,16 +89,19 @@ weg. | --- | --- | | **Datum** | wann gefunden. Vorbelegt mit heute; entscheidet später über Archiv und Löschung | | **Von** | Auswahl: wer es geschickt hat | -| **Anbieter** | Auswahl: Vermittler oder Portal, z. B. Hays | +| **Anbieter** | Freitext: Vermittler oder Portal, z. B. Hays. Vorschläge aus den bisherigen Einträgen | | **Kanal** | Auswahl: auf welchem Weg es kam | | **Projekttitel** | das einzige Pflichtfeld | | **Berater** | Auswahl: für wen das Projekt passt | | **Link** | Adresse der Ausschreibung. Ist einer hinterlegt, wird der Titel in der Liste anklickbar und öffnet ihn in einem neuen Tab | -*Von*, *Anbieter*, *Kanal* und *Berater* sind **Auswahllisten**. Ihre Inhalte -stehen unter *Einstellungen → Akquise* und lassen sich dort von jedem Benutzer -pflegen - Freitext hätte aus „Hays", „hays" und „Hays AG" drei Anbieter gemacht. -Ist eine Liste noch leer, steht unter dem Feld ein Verweis dorthin. +*Von*, *Kanal* und *Berater* sind **Auswahllisten**. Ihre Inhalte stehen unter +*Einstellungen → Akquise* und lassen sich dort von jedem Benutzer pflegen. Ist +eine Liste noch leer, steht unter dem Feld ein Verweis dorthin. + +**Anbieter** ist bewusst keine Liste, sondern ein Freitextfeld: neue Vermittler +tauchen zu oft auf, als dass das Eintragen daran hängen sollte. Das Feld schlägt +vor, was schon einmal benutzt wurde - der zuletzt verwendete Anbieter zuerst. Beim Link genügt `hays.de/projekt/1`, das `https://` wird ergänzt. @@ -564,13 +567,14 @@ Datenbank. | --- | --- | --- | | **Nutzerverwaltung** | Admin | Benutzer anlegen, bearbeiten, deaktivieren, löschen | | **Mein Konto** | alle | Foto, Terminfarbe, Passwort ändern | -| **Akquise** | alle | Inhalte der Auswahllisten *Von*, *Anbieter*, *Kanal*, *Berater* | +| **Akquise** | alle | Inhalte der Auswahllisten *Von*, *Kanal*, *Berater* | | **Postfach** | Admin | Verbindung zu Microsoft 365, Abgleich, Kontaktübernahme | | **Scanner** | Admin | Suchintervall, Schwellenwerte, KI-Schlüssel, Ton, E-Mail | | **Whisper** | alle | Spracherkennung für das Diktat einrichten | -Im Reiter **Akquise** stehen die vier Auswahllisten des Akquise-Formulars, jede +Im Reiter **Akquise** stehen die drei Auswahllisten des Akquise-Formulars, jede mit einem Feld zum Hinzufügen und je Eintrag einem Stift und einem Papierkorb. +*Anbieter* fehlt hier: das Feld ist Freitext mit Vorschlägen. Zwei Dinge sind dabei unterschiedlich: * **Umbenennen** ändert die vorhandenen Akquise-Einträge mit. Aus „Hays" wird in diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md index 027804d..4f5800b 100644 --- a/docs/09-quellcode.md +++ b/docs/09-quellcode.md @@ -28,7 +28,7 @@ Beim Start: `initDatabase()` (CRM-Schema, Admin-Seed) → `initScannerDatabase() | Export | Zweck | | --- | --- | | `pool` | mysql2-Pool, `connectionLimit: 8`, `dateStrings: true` | -| `initDatabase()` | führt `schema.sql` Statement für Statement aus, dann `migrateLeadKind()`, `migrateMailPool()`, `ensureAdminUser()` | +| `initDatabase()` | führt `schema.sql` Statement für Statement aus, dann `migrateLeadKind()`, `migrateMailPool()`, `seedAcquisitionOptions()`, `dropProviderOptions()`, `ensureAdminUser()` | `dateStrings: true` ist eine bewusste Entscheidung: Termine kommen als `YYYY-MM-DD HH:MM:SS` **ohne** Zeitzonenumrechnung an. Die Terminansicht rechnet mit @@ -52,6 +52,10 @@ Mal etwas: Einträgen als Freitext steht. Auch hier ein Schalter in `crm_settings` - ohne ihn wäre jeder Neustart ein Wiederherstellen von Werten, die jemand bewusst entfernt hat. +* `dropProviderOptions()` nimmt *Anbieter* wieder aus den Auswahllisten heraus - + das Feld ist Freitext mit Vorschlägen. Erkannt am Spaltentyp: steht `provider` + noch im ENUM, ist die Umstellung offen. Die Zeilen müssen vor dem Verengen weg, + sonst weist MariaDB den `ALTER` ab. `ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne `ADMIN_PASSWORD` erzeugt es ein Zufallspasswort und schreibt es einmalig ins Log - @@ -264,7 +268,7 @@ Warum genau diese Konstanten festgelegt sind, steht in | `components/acquisition/Acquisitions.tsx` | Akquise und Archiv (`view`); Suche, Sortierung, Übernahme, Löschen | | `components/acquisition/AcquisitionForm.tsx` | Eingabe über der Liste; behält Datum, Anbieter, Kanal und Berater für den nächsten Eintrag | | `components/acquisition/AcquisitionTable.tsx` | die Liste; `safeLink()` lässt nur http(s) zum Link werden | -| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen | +| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen. *Anbieter* nutzt es nicht: Freitext mit `` | | `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen | | `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `KindBadge`, `Avatar`, Farben der Kontaktart | | `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs | diff --git a/docs/10-glossar.md b/docs/10-glossar.md index d811f60..5a1de22 100644 --- a/docs/10-glossar.md +++ b/docs/10-glossar.md @@ -22,7 +22,7 @@ Quelltext. | **Match** / Treffer | Die Bewertung **eines** Projekts für **einen** Agenten: Prozentwert, Pro-Liste, Contra-Liste, Status. Tabelle `matches`. Ein Projekt erzeugt so viele Treffer, wie es aktive Agenten gibt. | | **Opportunity** | Geschäft in Arbeit: Phase, Tagessatz und Laufzeit oder Projektpreis, Wahrscheinlichkeit, Ansprechpartner. Tabelle `crm_opportunities`. | | **Akquise** | Eingang für gefundene Projekte, bevor daraus eine Opportunity wird. Tabelle `crm_acquisitions`. Nach 14 Tagen im **Archiv**, nach 90 Tagen gelöscht. | -| **Auswahllisten** (Akquise) | Inhalte der Dropdowns *Von*, *Anbieter*, *Kanal*, *Berater*. Tabelle `crm_acquisition_options`, gepflegt unter *Einstellungen → Akquise*. | +| **Auswahllisten** (Akquise) | Inhalte der Dropdowns *Von*, *Kanal*, *Berater*. Tabelle `crm_acquisition_options`, gepflegt unter *Einstellungen → Akquise*. *Anbieter* ist Freitext mit Vorschlägen. | | **Art** (eines Kontakts) | Recruiter, Partner oder Endkunde. Spalte `crm_leads.kind`; hieß bis 18.08.2026 „Status" mit den Werten Neu .. Verloren. | | **Papierkorb** | Im CRM gelöschte Mails. Umkehrbar, bis er geleert wird. Im Postfach crm@perinco.de bleiben die Mails in jedem Fall; der Abgleich holt gelöschte nicht wieder herein. | | **Phase** / Stage | Fortschritt einer Opportunity: Neu, Qualifizierung, Angebot, Verhandlung, Gewonnen, Verloren. | diff --git a/server/src/db.js b/server/src/db.js index ded8c88..1ab5146 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -51,6 +51,7 @@ export async function initDatabase() { await migrateLeadKind(); await migrateMailPool(); await seedAcquisitionOptions(); + await dropProviderOptions(); await ensureAdminUser(); } @@ -151,7 +152,7 @@ async function seedAcquisitionOptions() { // Vorhandene Freitexte uebernehmen, damit kein Eintrag einen Wert traegt, den // sein Dropdown nicht kennt. - for (const field of ['sender', 'provider', 'consultant', 'channel']) { + for (const field of ['sender', 'consultant', 'channel']) { await pool.query( `INSERT IGNORE INTO crm_acquisition_options (field, value) SELECT ?, TRIM(${field}) FROM crm_acquisitions @@ -171,6 +172,34 @@ async function seedAcquisitionOptions() { console.log(`[Akquise] Auswahllisten angelegt - ${count.count} Eintrag/Eintraege.`); } +/* + * Einmalige Umstellung: "Anbieter" ist keine Auswahlliste mehr, sondern wieder + * ein Freitextfeld mit Vorschlaegen. Die gepflegten Werte werden nicht mehr + * gebraucht - die Vorschlaege kommen aus den Eintraegen selbst. + * + * Erkannt am Spaltentyp: enthaelt das ENUM noch 'provider', ist die Umstellung + * offen. Die Zeilen muessen vor dem Verengen weg, sonst weist MariaDB den ALTER + * ab. Die Akquise-Eintraege selbst bleiben unberuehrt - ihr Anbieter steht als + * Text in crm_acquisitions, nicht als Verweis. + */ +async function dropProviderOptions() { + const [rows] = await pool.query( + `SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'crm_acquisition_options' + AND COLUMN_NAME = 'field' LIMIT 1` + ); + if (!rows[0] || !String(rows[0].COLUMN_TYPE).includes("'provider'")) return; + + const [result] = await pool.query("DELETE FROM crm_acquisition_options WHERE field = 'provider'"); + await pool.query( + `ALTER TABLE crm_acquisition_options + MODIFY field ENUM('sender','channel','consultant') NOT NULL` + ); + console.log( + `[Akquise] Anbieter ist wieder ein Freitextfeld - ${result.affectedRows} Auswahlwert(e) entfernt.` + ); +} + async function ensureAdminUser() { const [rows] = await pool.query('SELECT COUNT(*) AS count FROM crm_users'); if (rows[0].count > 0) return; diff --git a/server/src/routes/acquisitions.js b/server/src/routes/acquisitions.js index 27c885c..dfb3177 100644 --- a/server/src/routes/acquisitions.js +++ b/server/src/routes/acquisitions.js @@ -30,8 +30,11 @@ const DELETE_AFTER_DAYS = 90; * prueft das Backend beim Speichern eines Eintrags nur die Laenge und nicht die * Zugehoerigkeit zur Liste: wer einen Wert aus der Liste entfernt, soll damit * nicht jeden alten Eintrag unaenderbar machen. + * + * "provider" (Anbieter) fehlt hier absichtlich: das Feld ist Freitext mit + * Vorschlaegen aus den bisherigen Eintraegen. */ -const OPTION_FIELDS = ['sender', 'provider', 'channel', 'consultant']; +const OPTION_FIELDS = ['sender', 'channel', 'consultant']; // `link` fehlt hier absichtlich: der Wert wird eigens geprueft (normalizeLink). const TEXT_FIELDS = [ @@ -185,16 +188,32 @@ async function purgeOldEntries() { } } -/** Alle Auswahllisten als { sender: [...], provider: [...], ... }. */ +/** Alle Auswahllisten als { sender: [...], channel: [...], consultant: [...] }. */ async function loadOptions() { const [rows] = await pool.query( 'SELECT id, field, value FROM crm_acquisition_options ORDER BY field, value' ); - const options = { sender: [], provider: [], channel: [], consultant: [] }; + const options = { sender: [], channel: [], consultant: [] }; for (const row of rows) options[row.field].push({ id: row.id, value: row.value }); return options; } +/** + * Bisher benutzte Anbieter, der zuletzt verwendete zuerst. Das Feld ist Freitext - + * die Vorschlaege ersparen das Ausschreiben, ohne einen neuen Vermittler zu + * verbieten. Bewusst aus dem gesamten Bestand, nicht nur aus der gezeigten Liste. + */ +async function loadProviderSuggestions() { + const [rows] = await pool.query( + `SELECT provider FROM crm_acquisitions + WHERE provider IS NOT NULL AND provider <> '' + GROUP BY provider + ORDER BY MAX(found_at) DESC, provider ASC + LIMIT 100` + ); + return rows.map((row) => row.provider); +} + /** Aendern und loeschen darf der Ersteller und jeder Admin. */ function mayModify(user, row) { return user.role === 'admin' || row.created_by === user.id; @@ -421,12 +440,14 @@ router.get('/', async (req, res, next) => { [ARCHIVE_AFTER_DAYS, ARCHIVE_AFTER_DAYS, DELETE_AFTER_DAYS] ); - // Die Auswahllisten kommen mit: die Eingabe braucht sie sofort, und ein - // zweiter Aufruf nur fuer vier kurze Listen waere verschenkt. + // Auswahllisten und Anbieter-Vorschlaege kommen mit: die Eingabe braucht sie + // sofort, und ein zweiter Aufruf nur fuer ein paar kurze Listen waere + // verschenkt. res.json({ archive_after_days: ARCHIVE_AFTER_DAYS, delete_after_days: DELETE_AFTER_DAYS, options: await loadOptions(), + provider_suggestions: await loadProviderSuggestions(), counts: { eingang: Number(counts.eingang ?? 0), archiv: Number(counts.archiv ?? 0), diff --git a/server/src/schema.sql b/server/src/schema.sql index f276c30..b1eafb5 100644 --- a/server/src/schema.sql +++ b/server/src/schema.sql @@ -399,20 +399,23 @@ CREATE TABLE IF NOT EXISTS crm_acquisitions ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* - * Auswahllisten der Akquise. "Von", "Anbieter", "Kanal" und "Berater" sind - * Dropdowns, deren Inhalte in den Einstellungen gepflegt werden - Freitext hat - * aus "Hays", "hays" und "Hays AG" drei Anbieter gemacht. + * Auswahllisten der Akquise. "Von", "Kanal" und "Berater" sind Dropdowns, deren + * Inhalte in den Einstellungen gepflegt werden. * - * Bewusst eine Tabelle mit Feldkennzeichen statt vier Tabellen: die Felder + * "Anbieter" gehoert ausdruecklich nicht dazu: dort wird frei getippt, mit + * Vorschlaegen aus den bisherigen Eintraegen. Neue Vermittler tauchen zu oft auf, + * als dass sich das Eintragen an einer gepflegten Liste aufhalten sollte. + * + * Bewusst eine Tabelle mit Feldkennzeichen statt drei Tabellen: die Felder * verhalten sich gleich, und jede weitere Liste ist damit ein Wert im ENUM statt * einer neuen Tabelle samt Route. * * Der Unique-Schluessel greift ueber die Kollation utf8mb4_unicode_ci auch bei - * abweichender Gross-/Kleinschreibung: "hays" ist neben "Hays" kein neuer Wert. + * abweichender Gross-/Kleinschreibung: "mail" ist neben "Mail" kein neuer Wert. */ CREATE TABLE IF NOT EXISTS crm_acquisition_options ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, - field ENUM('sender','provider','channel','consultant') NOT NULL, + field ENUM('sender','channel','consultant') NOT NULL, value VARCHAR(190) NOT NULL, created_by INT UNSIGNED NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/src/components/acquisition/AcquisitionForm.tsx b/src/components/acquisition/AcquisitionForm.tsx index 6cb82f3..b19131a 100644 --- a/src/components/acquisition/AcquisitionForm.tsx +++ b/src/components/acquisition/AcquisitionForm.tsx @@ -27,6 +27,8 @@ interface AcquisitionFormProps { editing: Acquisition | null; /** Inhalte der Dropdowns, gepflegt unter Einstellungen -> Akquise. */ options: AcquisitionOptions; + /** Schon benutzte Anbieter - das Feld bleibt Freitext. */ + providerSuggestions: string[]; onSaved: () => void; onCancelEdit: () => void; } @@ -80,6 +82,7 @@ function toFormState(entry: Acquisition | null): FormState { export const AcquisitionForm: React.FC = ({ editing, options, + providerSuggestions, onSaved, onCancelEdit, }) => { @@ -252,14 +255,27 @@ export const AcquisitionForm: React.FC = ({ onChange={(value) => set('sender', value)} onOpenSettings={openSettings} /> - set('provider', value)} - onOpenSettings={openSettings} - /> + {/* Anbieter bleibt Freitext: neue Vermittler tauchen zu oft auf, als dass + das Eintragen an einer gepflegten Liste haengen sollte. Die + Vorschlagsliste erspart das Ausschreiben bekannter Namen. */} +
+ + set('provider', event.target.value)} + placeholder="z. B. Hays" + className={fieldClass} + /> + + {providerSuggestions.map((option) => ( + +
= ({ view = 'eingang', re load(true)} onCancelEdit={() => setEditing(null)} /> diff --git a/src/components/settings/AcquisitionSettings.tsx b/src/components/settings/AcquisitionSettings.tsx index 6366e92..88b8e67 100644 --- a/src/components/settings/AcquisitionSettings.tsx +++ b/src/components/settings/AcquisitionSettings.tsx @@ -15,10 +15,13 @@ import { } from '../../types'; /* - * Inhalte der Akquise-Dropdowns pflegen: "Von", "Anbieter", "Kanal", "Berater". + * Inhalte der Akquise-Dropdowns pflegen: "Von", "Kanal", "Berater". * - * Vier gleich aufgebaute Listen statt vier Sonderfaelle - die Felder verhalten - * sich gleich, also sieht ihre Pflege auch gleich aus. + * "Anbieter" steht bewusst nicht dabei - das Feld ist Freitext mit Vorschlaegen + * aus den bisherigen Eintraegen. + * + * Gleich aufgebaute Listen statt Sonderfaelle - die Felder verhalten sich + * gleich, also sieht ihre Pflege auch gleich aus. * * Umbenennen zieht die vorhandenen Akquise-Eintraege mit, Loeschen nicht: eine * Liste aufzuraeumen darf keine Daten aendern. Beide Antworten sagen, wie viele @@ -133,7 +136,6 @@ export const AcquisitionSettings: React.FC = () => { const [isLoading, setIsLoading] = useState(true); const [drafts, setDrafts] = useState>({ sender: '', - provider: '', channel: '', consultant: '', }); @@ -219,10 +221,11 @@ export const AcquisitionSettings: React.FC = () => {

Auswahllisten der Akquise

- Diese vier Listen füllen die Dropdowns im Formular unter Akquise. Feste Auswahl - statt Freitext, damit aus „Hays", „hays" und „Hays AG" nicht drei Anbieter werden. - Pflegen darf jeder angemeldete Benutzer - wer beim Eintragen auf einen neuen Anbieter - stößt, soll weiterarbeiten können. + Diese drei Listen füllen die Dropdowns Von, Kanal und Berater im + Formular unter Akquise. Pflegen darf jeder angemeldete Benutzer - wer beim + Eintragen einen fehlenden Wert braucht, soll weiterarbeiten können. Das Feld + Anbieter steht bewusst nicht hier: dort wird frei getippt, mit Vorschlägen aus + den bisherigen Einträgen.

Umbenennen ändert die vorhandenen Akquise-Einträge mit. diff --git a/src/types.ts b/src/types.ts index 81d1e4d..fa5f764 100644 --- a/src/types.ts +++ b/src/types.ts @@ -253,16 +253,18 @@ export type OpportunityViewMode = 'grid' | 'list' | 'pipeline'; /** * Felder der Akquise, die aus einer Auswahlliste kommen. Die Werte stehen in der * Datenbank (crm_acquisition_options) und werden unter Einstellungen -> Akquise - * gepflegt - als Freitext waeren "Hays", "hays" und "Hays AG" drei Anbieter. + * gepflegt. + * + * "Anbieter" ist ausdruecklich nicht dabei: dort wird frei getippt, mit + * Vorschlaegen aus den bisherigen Eintraegen (`AcquisitionList.provider_suggestions`). */ -export const ACQUISITION_OPTION_FIELDS = ['sender', 'provider', 'channel', 'consultant'] as const; +export const ACQUISITION_OPTION_FIELDS = ['sender', 'channel', 'consultant'] as const; export type AcquisitionOptionField = (typeof ACQUISITION_OPTION_FIELDS)[number]; /** Beschriftung der Felder - dieselbe in der Liste, im Formular und in den Einstellungen. */ export const ACQUISITION_FIELD_LABELS: Record = { sender: 'Von', - provider: 'Anbieter', channel: 'Kanal', consultant: 'Berater', }; @@ -270,7 +272,6 @@ export const ACQUISITION_FIELD_LABELS: Record = /** Was in der jeweiligen Liste steht - als Hilfe in den Einstellungen. */ export const ACQUISITION_FIELD_HINTS: Record = { sender: 'Personen, von denen Projekte kommen', - provider: 'Vermittler und Portale, z. B. Hays', channel: 'Auf welchem Weg das Projekt kam', consultant: 'Eigene Berater, für die Projekte gesucht werden', }; @@ -284,7 +285,6 @@ export type AcquisitionOptions = Record