fix(akquise): Anbieter wieder als Freitext mit Vorschlaegen
Das Dropdown fiel dem Feld zur Last: neue Vermittler tauchen zu oft auf, als dass das Eintragen an einer gepflegten Liste haengen sollte. Anbieter ist wieder ein Textfeld, jetzt mit Vorschlagsliste aus den bisherigen Eintraegen - der zuletzt benutzte zuerst. In den Einstellungen bleiben die drei Listen "Von", "Kanal" und "Berater". dropProviderOptions() nimmt 'provider' aus dem ENUM der Optionstabelle und loescht die dort gepflegten Werte; die Anbieter der Akquise-Eintraege bleiben unberuehrt, sie stehen als Text in crm_acquisitions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
05cc261900
commit
23087a12fb
+30
-1
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user