fix(opportunities): Berater als eigenes Feld statt nur in den Notizen
Bei der Uebernahme aus der Akquise verschwand die wichtigste Angabe nach dem Titel: fuer wen das Projekt laeuft. Die Opportunity hatte kein Feld dafuer, also schrieb die Uebernahme den Namen in den Herkunftsvermerk der Notizen - in Pipeline, Liste und Kacheln war er damit nicht zu sehen, und suchen liess sich nur im Notiztext. crm_opportunities.consultant traegt ihn jetzt selbst. Genau ein Name je Opportunity: passt ein Fund zu drei Beratern, entstehen bei der Uebernahme drei Opportunities. Sichtbar in Pipeline-Karte, Kachel, Papierkorb und als sortierbare Spalte direkt hinter dem Titel; die Suche fasst ihn mit ein, und im Dialog kommt die Auswahl aus derselben Liste wie in der Akquise (Einstellungen -> Akquise), damit nicht "Nico" und "Nico N." nebeneinander entstehen. Bestehende Opportunities holt migrateOpportunityConsultant() beim Start aus ihren Notizen nach - nur dort, wo die Spalte leer ist. Damit ist der Lauf wiederholbar und ueberschreibt nichts, was seither von Hand gesetzt wurde; ein Merker in crm_settings ist dafuer nicht noetig. Der Text in den Notizen bleibt stehen: er ist der Herkunftsnachweis samt Anbieter, Kanal und Link. Der Namenszusatz im Titel entfaellt wieder. Er war gestern dazugekommen, damit sich mehrere Opportunities aus einem Fund unterscheiden liessen - mit einer eigenen Spalte ist er nur noch Rauschen. Nebenbei: der Block, der won_at nachzieht, stand zweimal in schema.sql. Idempotent und damit harmlos, aber eine Kopie - jetzt einmal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8403f1c39d
commit
93fe8cfd73
@@ -18,7 +18,21 @@ const STAGES = new Set([
|
||||
'Absage',
|
||||
]);
|
||||
|
||||
const TEXT_FIELDS = ['title', 'company', 'description', 'last_interaction_note', 'next_step', 'notes'];
|
||||
const TEXT_FIELDS = [
|
||||
'title',
|
||||
'company',
|
||||
// Fuer welchen Berater das Projekt laeuft. Gepflegt wird die Auswahl unter
|
||||
// Einstellungen -> Akquise; geprueft wird hier nur die Laenge, damit ein aus
|
||||
// der Liste entfernter Name eine alte Opportunity nicht unaenderbar macht.
|
||||
'consultant',
|
||||
'description',
|
||||
'last_interaction_note',
|
||||
'next_step',
|
||||
'notes',
|
||||
];
|
||||
|
||||
/** Laengengrenze der Spalte - abgewiesen statt stillschweigend abgeschnitten. */
|
||||
const CONSULTANT_MAX_LENGTH = 190;
|
||||
const DATE_FIELDS = ['start_date', 'end_date', 'last_interaction_at'];
|
||||
const NUMBER_FIELDS = ['daily_rate', 'duration_days', 'fixed_price'];
|
||||
|
||||
@@ -303,6 +317,13 @@ router.post('/', async (req, res, next) => {
|
||||
return res.status(400).json({ error: 'Zuständiger Benutzer existiert nicht.' });
|
||||
}
|
||||
|
||||
const consultant = emptyToNull(req.body?.consultant);
|
||||
if (consultant !== null && consultant.length > CONSULTANT_MAX_LENGTH) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: `Der Berater ist zu lang (max. ${CONSULTANT_MAX_LENGTH} Zeichen).` });
|
||||
}
|
||||
|
||||
const contacts = await normalizeContacts(req.body?.contacts ?? []);
|
||||
if (contacts.error) return res.status(400).json({ error: contacts.error });
|
||||
|
||||
@@ -313,13 +334,14 @@ router.post('/', async (req, res, next) => {
|
||||
// Wer direkt in 'Beauftragt' anlegt, hat gerade gewonnen - dann steht das
|
||||
// Datum sofort.
|
||||
`INSERT INTO crm_opportunities
|
||||
(title, company, description, stage, won_at, probability, daily_rate, duration_days,
|
||||
fixed_price, start_date, end_date, last_interaction_at, last_interaction_note,
|
||||
next_step, tags, notes, owner_id, created_by)
|
||||
VALUES (?, ?, ?, ?, ${stage === 'Beauftragt' ? 'UTC_TIMESTAMP()' : 'NULL'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(title, company, consultant, description, stage, won_at, probability, daily_rate,
|
||||
duration_days, fixed_price, start_date, end_date, last_interaction_at,
|
||||
last_interaction_note, next_step, tags, notes, owner_id, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ${stage === 'Beauftragt' ? 'UTC_TIMESTAMP()' : 'NULL'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
title,
|
||||
emptyToNull(req.body?.company),
|
||||
consultant,
|
||||
emptyToNull(req.body?.description),
|
||||
stage,
|
||||
Math.round(probability.value ?? 10),
|
||||
@@ -381,6 +403,11 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (field === 'title' && !value) {
|
||||
return res.status(400).json({ error: 'Der Titel darf nicht leer sein.' });
|
||||
}
|
||||
if (field === 'consultant' && value !== null && value.length > CONSULTANT_MAX_LENGTH) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: `Der Berater ist zu lang (max. ${CONSULTANT_MAX_LENGTH} Zeichen).` });
|
||||
}
|
||||
fields.push(`${field} = ?`);
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user