feat(akquise): E-Mail-Adresse je Berater in den Einstellungen
Neue Spalte crm_acquisition_options.email, pflegbar unter Einstellungen -> Akquise direkt in der Berater-Zeile. Beim ersten Start werden vorhandene Berater einmalig mit vorname.nachname@perinco.de vorbelegt (Merker consultant_email_seeded); Namen ohne Nachnamen bleiben leer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b9d6544b30
commit
907b010f3c
@@ -103,6 +103,7 @@ erDiagram
|
|||||||
int id PK
|
int id PK
|
||||||
enum field "sender | channel | consultant"
|
enum field "sender | channel | consultant"
|
||||||
varchar value "Inhalt des Dropdowns"
|
varchar value "Inhalt des Dropdowns"
|
||||||
|
varchar email "nur bei consultant"
|
||||||
}
|
}
|
||||||
crm_acquisition_history {
|
crm_acquisition_history {
|
||||||
date found_on PK
|
date found_on PK
|
||||||
|
|||||||
@@ -976,6 +976,13 @@ Zwei Dinge sind dabei unterschiedlich:
|
|||||||
behalten ihn und zeigen ihn im Formular als „nicht in der Liste" - so ändert
|
behalten ihn und zeigen ihn im Formular als „nicht in der Liste" - so ändert
|
||||||
das Aufräumen einer Liste keine Daten.
|
das Aufräumen einer Liste keine Daten.
|
||||||
|
|
||||||
|
Bei den **Beratern** steht unter jedem Namen zusätzlich ein Feld für die
|
||||||
|
**E-Mail-Adresse**. Gespeichert wird beim Verlassen des Feldes oder mit Enter;
|
||||||
|
ein leeres Feld entfernt die Adresse. Beim Einführen des Feldes (21.10.2026)
|
||||||
|
wurden die vorhandenen Berater einmalig mit `vorname.nachname@perinco.de`
|
||||||
|
vorbelegt - Umlaute ausgeschrieben, bei mehrteiligen Namen erster und letzter
|
||||||
|
Teil. Berater mit nur einem Namen blieben leer und brauchen eine Adresse von Hand.
|
||||||
|
|
||||||
Im Reiter **Scanner** steht das **Suchintervall (Minuten)** - also wie oft der
|
Im Reiter **Scanner** steht das **Suchintervall (Minuten)** - also wie oft der
|
||||||
Hintergrund-Scanner die Börsen durchsucht.
|
Hintergrund-Scanner die Börsen durchsucht.
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ Mal etwas:
|
|||||||
das Feld ist Freitext mit Vorschlägen. Erkannt am Spaltentyp: steht `provider`
|
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,
|
noch im ENUM, ist die Umstellung offen. Die Zeilen müssen vor dem Verengen weg,
|
||||||
sonst weist MariaDB den `ALTER` ab.
|
sonst weist MariaDB den `ALTER` ab.
|
||||||
|
* `addConsultantEmail()` legt `crm_acquisition_options.email` an und belegt die
|
||||||
|
vorhandenen Berater einmalig mit `vorname.nachname@perinco.de` vor
|
||||||
|
(`guessPerincoEmail()`, Merker `consultant_email_seeded`). Namen ohne
|
||||||
|
Nachnamen bleiben leer.
|
||||||
|
|
||||||
`ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne
|
`ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne
|
||||||
`ADMIN_PASSWORD` erzeugt es ein Zufallspasswort und schreibt es einmalig ins Log -
|
`ADMIN_PASSWORD` erzeugt es ein Zufallspasswort und schreibt es einmalig ins Log -
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export async function initDatabase() {
|
|||||||
await migrateOpportunityConsultant();
|
await migrateOpportunityConsultant();
|
||||||
await seedAcquisitionOptions();
|
await seedAcquisitionOptions();
|
||||||
await dropProviderOptions();
|
await dropProviderOptions();
|
||||||
|
await addConsultantEmail();
|
||||||
await migrateIconFlag();
|
await migrateIconFlag();
|
||||||
await ensureAdminUser();
|
await ensureAdminUser();
|
||||||
}
|
}
|
||||||
@@ -274,6 +275,71 @@ async function dropProviderOptions() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* E-Mail-Adresse je Berater in der Auswahlliste.
|
||||||
|
*
|
||||||
|
* Die Spalte gilt fuer alle Listen, gepflegt wird sie nur bei "Berater" - eine
|
||||||
|
* eigene Tabelle fuer eine Spalte waere mehr Aufwand als Nutzen.
|
||||||
|
*
|
||||||
|
* Beim ersten Anlegen werden die vorhandenen Berater einmalig mit
|
||||||
|
* vorname.nachname@perinco.de vorbelegt (Umlaute ausgeschrieben). Ein Merker in
|
||||||
|
* crm_settings haelt das fest - eine bewusst geleerte Adresse soll nach einem
|
||||||
|
* Neustart nicht wiederkommen. Namen ohne Nachnamen bleiben leer, da laesst
|
||||||
|
* sich keine Adresse raten.
|
||||||
|
*/
|
||||||
|
async function addConsultantEmail() {
|
||||||
|
const [columns] = await pool.query(
|
||||||
|
`SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'crm_acquisition_options'
|
||||||
|
AND COLUMN_NAME = 'email' LIMIT 1`
|
||||||
|
);
|
||||||
|
if (!columns[0]) {
|
||||||
|
await pool.query(
|
||||||
|
'ALTER TABLE crm_acquisition_options ADD COLUMN email VARCHAR(190) NULL AFTER value'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flag = 'consultant_email_seeded';
|
||||||
|
const [rows] = await pool.query('SELECT value FROM crm_settings WHERE key_name = ? LIMIT 1', [flag]);
|
||||||
|
if (rows[0]?.value === 'true') return;
|
||||||
|
|
||||||
|
const [consultants] = await pool.query(
|
||||||
|
"SELECT id, value FROM crm_acquisition_options WHERE field = 'consultant' AND email IS NULL"
|
||||||
|
);
|
||||||
|
let filled = 0;
|
||||||
|
for (const row of consultants) {
|
||||||
|
const email = guessPerincoEmail(row.value);
|
||||||
|
if (!email) continue;
|
||||||
|
await pool.query('UPDATE crm_acquisition_options SET email = ? WHERE id = ?', [email, row.id]);
|
||||||
|
filled += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO crm_settings (key_name, value) VALUES (?, 'true')
|
||||||
|
ON DUPLICATE KEY UPDATE value = 'true'`,
|
||||||
|
[flag]
|
||||||
|
);
|
||||||
|
console.log(`[Akquise] Berater-Adressen vorbelegt - ${filled} von ${consultants.length} Berater(n).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Jörg Müller-Lüdenscheidt" -> "joerg.mueller-luedenscheidt@perinco.de"; ohne Nachname null. */
|
||||||
|
function guessPerincoEmail(name) {
|
||||||
|
const parts = String(name)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ä/g, 'ae')
|
||||||
|
.replace(/ö/g, 'oe')
|
||||||
|
.replace(/ü/g, 'ue')
|
||||||
|
.replace(/ß/g, 'ss')
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[̀-ͯ]/g, '')
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((part) => part.replace(/[^a-z0-9-]/g, ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
return `${parts[0]}.${parts[parts.length - 1]}@perinco.de`;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Das Feld "Berater" nimmt mehrere Namen auf, kommagetrennt (siehe
|
* Das Feld "Berater" nimmt mehrere Namen auf, kommagetrennt (siehe
|
||||||
* acquisitionRules.js). VARCHAR(190) reichte fuer einen Namen; drei bis vier
|
* acquisitionRules.js). VARCHAR(190) reichte fuer einen Namen; drei bis vier
|
||||||
|
|||||||
@@ -295,13 +295,29 @@ async function purgeOldEntries() {
|
|||||||
/** Alle Auswahllisten als { sender: [...], channel: [...], consultant: [...] }. */
|
/** Alle Auswahllisten als { sender: [...], channel: [...], consultant: [...] }. */
|
||||||
async function loadOptions() {
|
async function loadOptions() {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
'SELECT id, field, value FROM crm_acquisition_options ORDER BY field, value'
|
'SELECT id, field, value, email FROM crm_acquisition_options ORDER BY field, value'
|
||||||
);
|
);
|
||||||
const options = { sender: [], channel: [], consultant: [] };
|
const options = { sender: [], channel: [], consultant: [] };
|
||||||
for (const row of rows) options[row.field].push({ id: row.id, value: row.value });
|
for (const row of rows) {
|
||||||
|
options[row.field].push({ id: row.id, value: row.value, email: row.email ?? null });
|
||||||
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E-Mail-Adresse eines Beraters pruefen. Leer heisst "keine Adresse" (null);
|
||||||
|
* geprueft wird nur grob die Form - ob das Postfach existiert, weiss hier keiner.
|
||||||
|
*/
|
||||||
|
function normalizeOptionEmail(raw) {
|
||||||
|
const email = emptyToNull(raw);
|
||||||
|
if (email === null) return { value: null };
|
||||||
|
if (email.length > 190) return { error: 'Die E-Mail-Adresse ist zu lang (max. 190 Zeichen).' };
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
return { error: 'Das ist keine gültige E-Mail-Adresse.' };
|
||||||
|
}
|
||||||
|
return { value: email.toLowerCase() };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bisher benutzte Anbieter, der zuletzt verwendete zuerst. Das Feld ist Freitext -
|
* Bisher benutzte Anbieter, der zuletzt verwendete zuerst. Das Feld ist Freitext -
|
||||||
* die Vorschlaege ersparen das Ausschreiben, ohne einen neuen Vermittler zu
|
* die Vorschlaege ersparen das Ausschreiben, ohne einen neuen Vermittler zu
|
||||||
@@ -452,13 +468,16 @@ router.post('/options', async (req, res, next) => {
|
|||||||
if (value.length > limit) {
|
if (value.length > limit) {
|
||||||
return res.status(400).json({ error: `Der Wert ist zu lang (max. ${limit} Zeichen).` });
|
return res.status(400).json({ error: `Der Wert ist zu lang (max. ${limit} Zeichen).` });
|
||||||
}
|
}
|
||||||
|
// Eine Adresse gibt es nur bei Beratern.
|
||||||
|
const email = field === 'consultant' ? normalizeOptionEmail(req.body?.email) : { value: null };
|
||||||
|
if (email.error) return res.status(400).json({ error: email.error });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
'INSERT INTO crm_acquisition_options (field, value, created_by) VALUES (?, ?, ?)',
|
'INSERT INTO crm_acquisition_options (field, value, email, created_by) VALUES (?, ?, ?, ?)',
|
||||||
[field, value, req.user.id]
|
[field, value, email.value, req.user.id]
|
||||||
);
|
);
|
||||||
res.status(201).json({ id: result.insertId, field, value });
|
res.status(201).json({ id: result.insertId, field, value, email: email.value });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err?.code === 'ER_DUP_ENTRY') {
|
if (err?.code === 'ER_DUP_ENTRY') {
|
||||||
return res.status(409).json({ error: 'Dieser Wert steht schon in der Liste.' });
|
return res.status(409).json({ error: 'Dieser Wert steht schon in der Liste.' });
|
||||||
@@ -478,26 +497,48 @@ router.patch('/options/:id', async (req, res, next) => {
|
|||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Options-ID.' });
|
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Options-ID.' });
|
||||||
|
|
||||||
const value = emptyToNull(req.body?.value);
|
/*
|
||||||
if (!value) return res.status(400).json({ error: 'Ein Wert ist erforderlich.' });
|
* `value` und `email` sind beide optional: die Einstellungen aendern mal den
|
||||||
|
* Namen, mal nur die Adresse. Fehlt `value`, bleibt der Name, wie er ist.
|
||||||
|
*/
|
||||||
|
const emailGiven = req.body?.email !== undefined;
|
||||||
|
const email = emailGiven ? normalizeOptionEmail(req.body.email) : null;
|
||||||
|
if (email?.error) return res.status(400).json({ error: email.error });
|
||||||
|
|
||||||
|
const valueGiven = req.body?.value !== undefined;
|
||||||
|
const requested = valueGiven ? emptyToNull(req.body.value) : null;
|
||||||
|
if (valueGiven && !requested) return res.status(400).json({ error: 'Ein Wert ist erforderlich.' });
|
||||||
|
if (!valueGiven && !emailGiven) return res.status(400).json({ error: 'Nichts zu ändern.' });
|
||||||
|
|
||||||
const conn = await pool.getConnection();
|
const conn = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
const [rows] = await conn.query(
|
const [rows] = await conn.query(
|
||||||
'SELECT field, value FROM crm_acquisition_options WHERE id = ? LIMIT 1',
|
'SELECT field, value, email FROM crm_acquisition_options WHERE id = ? LIMIT 1',
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
const existing = rows[0];
|
const existing = rows[0];
|
||||||
if (!existing) return res.status(404).json({ error: 'Eintrag der Liste nicht gefunden.' });
|
if (!existing) return res.status(404).json({ error: 'Eintrag der Liste nicht gefunden.' });
|
||||||
|
if (emailGiven && existing.field !== 'consultant') {
|
||||||
|
return res.status(400).json({ error: 'Eine E-Mail-Adresse gibt es nur bei Beratern.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = requested ?? existing.value;
|
||||||
|
const newEmail = emailGiven ? email.value : existing.email ?? null;
|
||||||
const limit = existing.field === 'channel' ? 60 : 190;
|
const limit = existing.field === 'channel' ? 60 : 190;
|
||||||
if (value.length > limit) {
|
if (value.length > limit) {
|
||||||
return res.status(400).json({ error: `Der Wert ist zu lang (max. ${limit} Zeichen).` });
|
return res.status(400).json({ error: `Der Wert ist zu lang (max. ${limit} Zeichen).` });
|
||||||
}
|
}
|
||||||
if (value === existing.value) return res.json({ id, field: existing.field, value, updated: 0 });
|
if (value === existing.value) {
|
||||||
|
await conn.query('UPDATE crm_acquisition_options SET email = ? WHERE id = ?', [newEmail, id]);
|
||||||
|
return res.json({ id, field: existing.field, value, email: newEmail, updated: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
await conn.beginTransaction();
|
await conn.beginTransaction();
|
||||||
await conn.query('UPDATE crm_acquisition_options SET value = ? WHERE id = ?', [value, id]);
|
await conn.query('UPDATE crm_acquisition_options SET value = ?, email = ? WHERE id = ?', [
|
||||||
|
value,
|
||||||
|
newEmail,
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
|
||||||
let touched;
|
let touched;
|
||||||
if (existing.field === 'consultant') {
|
if (existing.field === 'consultant') {
|
||||||
@@ -528,7 +569,7 @@ router.patch('/options/:id', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
|
|
||||||
res.json({ id, field: existing.field, value, updated: touched });
|
res.json({ id, field: existing.field, value, email: newEmail, updated: touched });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await conn.rollback().catch(() => {});
|
await conn.rollback().catch(() => {});
|
||||||
if (err?.code === 'ER_DUP_ENTRY') {
|
if (err?.code === 'ER_DUP_ENTRY') {
|
||||||
|
|||||||
@@ -575,6 +575,7 @@ CREATE TABLE IF NOT EXISTS crm_acquisition_options (
|
|||||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
field ENUM('sender','channel','consultant') NOT NULL,
|
field ENUM('sender','channel','consultant') NOT NULL,
|
||||||
value VARCHAR(190) NOT NULL,
|
value VARCHAR(190) NOT NULL,
|
||||||
|
email VARCHAR(190) NULL,
|
||||||
created_by INT UNSIGNED NULL,
|
created_by INT UNSIGNED NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
|
|||||||
@@ -34,11 +34,63 @@ const PlusIcon = (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* E-Mail-Adresse eines Beraters, direkt in der Zeile bearbeitbar. Gespeichert
|
||||||
|
* wird beim Verlassen des Feldes oder mit Enter - wie beim Umbenennen, nur ohne
|
||||||
|
* extra Knopf, weil die Adresse oefter nachgetragen als geaendert wird.
|
||||||
|
*/
|
||||||
|
const EmailField: React.FC<{
|
||||||
|
option: AcquisitionOption;
|
||||||
|
onSave: (email: string) => Promise<void>;
|
||||||
|
}> = ({ option, onSave }) => {
|
||||||
|
const [draft, setDraft] = useState(option.email ?? '');
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraft(option.email ?? '');
|
||||||
|
}, [option.email]);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const email = draft.trim();
|
||||||
|
if (email === (option.email ?? '')) return;
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
await onSave(email);
|
||||||
|
} catch {
|
||||||
|
setDraft(option.email ?? '');
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={draft}
|
||||||
|
disabled={isSaving}
|
||||||
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.blur();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') setDraft(option.email ?? '');
|
||||||
|
}}
|
||||||
|
onBlur={save}
|
||||||
|
placeholder="E-Mail-Adresse"
|
||||||
|
aria-label={`E-Mail-Adresse von ${option.value}`}
|
||||||
|
className={`${fieldClass} py-1.5 text-xs`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const OptionRow: React.FC<{
|
const OptionRow: React.FC<{
|
||||||
option: AcquisitionOption;
|
option: AcquisitionOption;
|
||||||
onRenamed: (value: string) => Promise<void>;
|
onRenamed: (value: string) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}> = ({ option, onRenamed, onDelete }) => {
|
/** Nur bei "Berater" gesetzt - dann zeigt die Zeile das Adressfeld. */
|
||||||
|
onEmailSaved?: (email: string) => Promise<void>;
|
||||||
|
}> = ({ option, onRenamed, onDelete, onEmailSaved }) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(option.value);
|
const [draft, setDraft] = useState(option.value);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -66,8 +118,18 @@ const OptionRow: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center gap-2 rounded-xl border border-ink-200 bg-surface px-3 py-2">
|
<li className="rounded-xl border border-ink-200 bg-surface px-3 py-2">
|
||||||
{isEditing ? (
|
<div className="flex items-center gap-2">{renderName()}</div>
|
||||||
|
{onEmailSaved && (
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<EmailField option={option} onSave={onEmailSaved} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderName() {
|
||||||
|
return isEditing ? (
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
value={draft}
|
value={draft}
|
||||||
@@ -125,9 +187,8 @@ const OptionRow: React.FC<{
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AcquisitionSettings: React.FC = () => {
|
export const AcquisitionSettings: React.FC = () => {
|
||||||
@@ -190,6 +251,17 @@ export const AcquisitionSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveEmail = async (option: AcquisitionOption, email: string) => {
|
||||||
|
try {
|
||||||
|
await acquisitionOptionsApi.updateEmail(option.id, email);
|
||||||
|
addToast(email ? `Adresse für ${option.value} gespeichert.` : 'Adresse entfernt.', 'success');
|
||||||
|
await load(true);
|
||||||
|
} catch (err) {
|
||||||
|
addToast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!toDelete) return;
|
if (!toDelete) return;
|
||||||
try {
|
try {
|
||||||
@@ -289,6 +361,9 @@ export const AcquisitionSettings: React.FC = () => {
|
|||||||
option={option}
|
option={option}
|
||||||
onRenamed={(value) => rename(option, value)}
|
onRenamed={(value) => rename(option, value)}
|
||||||
onDelete={() => setToDelete({ field, option })}
|
onDelete={() => setToDelete({ field, option })}
|
||||||
|
onEmailSaved={
|
||||||
|
field === 'consultant' ? (email) => saveEmail(option, email) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+9
-2
@@ -490,10 +490,10 @@ export const acquisitionsApi = {
|
|||||||
export const acquisitionOptionsApi = {
|
export const acquisitionOptionsApi = {
|
||||||
list: () => request<AcquisitionOptions>('/acquisitions/options'),
|
list: () => request<AcquisitionOptions>('/acquisitions/options'),
|
||||||
|
|
||||||
create: (field: AcquisitionOptionField, value: string) =>
|
create: (field: AcquisitionOptionField, value: string, email?: string | null) =>
|
||||||
request<AcquisitionOption & { field: AcquisitionOptionField }>('/acquisitions/options', {
|
request<AcquisitionOption & { field: AcquisitionOptionField }>('/acquisitions/options', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ field, value }),
|
body: JSON.stringify({ field, value, email }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/** Umbenennen zieht die vorhandenen Akquise-Eintraege mit; `updated` sagt wie viele. */
|
/** Umbenennen zieht die vorhandenen Akquise-Eintraege mit; `updated` sagt wie viele. */
|
||||||
@@ -503,6 +503,13 @@ export const acquisitionOptionsApi = {
|
|||||||
{ method: 'PATCH', body: JSON.stringify({ value }) }
|
{ method: 'PATCH', body: JSON.stringify({ value }) }
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/** Nur die E-Mail-Adresse eines Beraters; leer entfernt sie. */
|
||||||
|
updateEmail: (id: number, email: string) =>
|
||||||
|
request<AcquisitionOption & { field: AcquisitionOptionField; updated: number }>(
|
||||||
|
`/acquisitions/options/${id}`,
|
||||||
|
{ method: 'PATCH', body: JSON.stringify({ email }) }
|
||||||
|
),
|
||||||
|
|
||||||
/** Entfernt nur die Auswahl; `still_in_use` zaehlt die Eintraege, die den Wert behalten. */
|
/** Entfernt nur die Auswahl; `still_in_use` zaehlt die Eintraege, die den Wert behalten. */
|
||||||
remove: (id: number) =>
|
remove: (id: number) =>
|
||||||
request<{ ok: true; still_in_use: number }>(`/acquisitions/options/${id}`, { method: 'DELETE' }),
|
request<{ ok: true; still_in_use: number }>(`/acquisitions/options/${id}`, { method: 'DELETE' }),
|
||||||
|
|||||||
@@ -397,6 +397,8 @@ export const ACQUISITION_FIELD_HINTS: Record<AcquisitionOptionField, string> = {
|
|||||||
export interface AcquisitionOption {
|
export interface AcquisitionOption {
|
||||||
id: number;
|
id: number;
|
||||||
value: string;
|
value: string;
|
||||||
|
/** Nur bei "Berater" gepflegt, sonst immer null. */
|
||||||
|
email: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user