feat(opportunities): Link zur Ausschreibung als eigenes Feld

Die Uebernahme aus der Akquise schrieb den Link bisher nur in die Notizen.
Jetzt hat die Opportunity eine Spalte `link`, gefuellt bei Uebernahme und
Kopie; im Dialog steht sie rechts neben der Phase mit Knopf "Oeffnen".
schema.sql traegt den Link fuer aeltere Uebernahmen nach. Die Pruefung liegt
in server/src/link.js und gilt fuer Akquise und Opportunities gleich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-23 08:56:39 +02:00
co-authored by Claude Opus 5.5
parent 3e76a73f2e
commit 8cb5cbc7cc
9 changed files with 144 additions and 40 deletions
+21 -7
View File
@@ -1,6 +1,7 @@
import express from 'express';
import { pool } from '../db.js';
import { requireAuth } from '../middleware/auth.js';
import { normalizeLink } from '../link.js';
const router = express.Router();
router.use(requireAuth);
@@ -328,6 +329,10 @@ router.post('/', async (req, res, next) => {
.json({ error: `Der Berater ist zu lang (max. ${CONSULTANT_MAX_LENGTH} Zeichen).` });
}
// Link zur Ausschreibung - dieselbe Pruefung wie in der Akquise.
const link = normalizeLink(req.body?.link);
if (link.error) return res.status(400).json({ error: link.error });
const contacts = await normalizeContacts(req.body?.contacts ?? []);
if (contacts.error) return res.status(400).json({ error: contacts.error });
@@ -338,14 +343,15 @@ 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, consultant, description, stage, won_at, probability, daily_rate,
(title, company, consultant, link, 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'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?, ${stage === 'Beauftragt' ? 'UTC_TIMESTAMP()' : 'NULL'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
title,
emptyToNull(req.body?.company),
consultant,
link.value,
emptyToNull(req.body?.description),
stage,
Math.round(probability.value ?? 10),
@@ -416,6 +422,13 @@ router.patch('/:id', async (req, res, next) => {
values.push(value);
}
if (req.body?.link !== undefined) {
const link = normalizeLink(req.body.link);
if (link.error) return res.status(400).json({ error: link.error });
fields.push('link = ?');
values.push(link.value);
}
if (req.body?.stage !== undefined) {
if (!STAGES.has(req.body.stage)) return res.status(400).json({ error: 'Ungültige Phase.' });
fields.push('stage = ?');
@@ -708,7 +721,7 @@ router.post('/:id/acquisition', async (req, res, next) => {
* fuer einen Kollegen, entsteht je Berater eine Kopie mit eigener Phase - wie
* bei der Uebernahme aus der Akquise, nur nachtraeglich.
*
* Kopiert wird, was das Projekt beschreibt: Titel, Firma, Beschreibung, Zahlen,
* Kopiert wird, was das Projekt beschreibt: Titel, Firma, Link, Beschreibung, Zahlen,
* Zeitraum, Tags, Notizen, Ansprechpartner, Zustaendiger und die Herkunft aus
* der Akquise (damit "Zurueck in die Akquise" auch die Kopien findet). Nicht
* kopiert wird, was zum Verlauf des Originals gehoert: letzte Interaktion,
@@ -764,15 +777,16 @@ router.post('/:id/copy', async (req, res, next) => {
for (const consultant of consultants) {
const [created] = await conn.query(
`INSERT INTO crm_opportunities
(title, company, consultant, acquisition_id, description, stage, won_at, probability,
daily_rate, duration_days, fixed_price, start_date, end_date, tags, notes,
owner_id, created_by)
VALUES (?, ?, ?, ?, ?, ?, ${stage === 'Beauftragt' ? 'UTC_TIMESTAMP()' : 'NULL'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
(title, company, consultant, acquisition_id, link, description, stage, won_at,
probability, daily_rate, duration_days, fixed_price, start_date, end_date, tags,
notes, owner_id, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ${stage === 'Beauftragt' ? 'UTC_TIMESTAMP()' : 'NULL'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
source.title,
source.company,
consultant,
source.acquisition_id,
source.link,
source.description,
stage,
source.probability,