feat(opportunities): Festpreis, Klick daneben, Kontakt von hier
Drei Dinge, die im Alltag gefehlt haben: - Projektdaten kennen jetzt einen Projektpreis. Ist er gesetzt, gilt er als Volumen statt Tagessatz x Laufzeit - Festpreisprojekte mussten bisher ausgerechnet und in den Tagessatz gefaelscht werden. - Dialoge schliessen auch per Klick in den Hintergrund, nicht nur ueber das X. Escape trifft dabei nur den obersten Dialog; eine Textmarkierung, die im Dialog beginnt und daneben endet, schliesst nichts. - Fehlt ein Ansprechpartner noch im CRM, oeffnet "Neuer Kontakt..." in der Opportunity denselben Dialog wie unter Kontakte. Der frische Kontakt haengt danach sofort als Ansprechpartner dran, ohne die Opportunity zu verlassen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
30164e0ae7
commit
025390e18a
@@ -16,7 +16,7 @@ const STAGES = new Set([
|
||||
|
||||
const TEXT_FIELDS = ['title', 'company', 'description', 'last_interaction_note', 'next_step', 'notes'];
|
||||
const DATE_FIELDS = ['start_date', 'end_date', 'last_interaction_at'];
|
||||
const NUMBER_FIELDS = ['daily_rate', 'duration_days'];
|
||||
const NUMBER_FIELDS = ['daily_rate', 'duration_days', 'fixed_price'];
|
||||
|
||||
function emptyToNull(value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
@@ -85,6 +85,7 @@ function mapOpportunity(row, contacts) {
|
||||
...row,
|
||||
daily_rate: row.daily_rate === null ? null : Number(row.daily_rate),
|
||||
duration_days: row.duration_days === null ? null : Number(row.duration_days),
|
||||
fixed_price: row.fixed_price === null ? null : Number(row.fixed_price),
|
||||
probability: Number(row.probability),
|
||||
tags: parseTags(row.tags),
|
||||
contacts: contacts ?? [],
|
||||
@@ -220,6 +221,7 @@ router.post('/', async (req, res, next) => {
|
||||
for (const [field, label, max] of [
|
||||
['daily_rate', 'Der Tagessatz', 1_000_000],
|
||||
['duration_days', 'Die Laufzeit', 100_000],
|
||||
['fixed_price', 'Der Projektpreis', 1_000_000_000],
|
||||
]) {
|
||||
const result = normalizeNumber(req.body?.[field], label, max);
|
||||
if (result.error) return res.status(400).json({ error: result.error });
|
||||
@@ -253,9 +255,9 @@ router.post('/', async (req, res, next) => {
|
||||
const [result] = await conn.query(
|
||||
`INSERT INTO crm_opportunities
|
||||
(title, company, description, stage, probability, daily_rate, duration_days,
|
||||
start_date, end_date, last_interaction_at, last_interaction_note, next_step,
|
||||
tags, notes, owner_id, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
fixed_price, start_date, end_date, last_interaction_at, last_interaction_note,
|
||||
next_step, tags, notes, owner_id, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
title,
|
||||
emptyToNull(req.body?.company),
|
||||
@@ -264,6 +266,7 @@ router.post('/', async (req, res, next) => {
|
||||
Math.round(probability.value ?? 10),
|
||||
numbers.daily_rate,
|
||||
numbers.duration_days,
|
||||
numbers.fixed_price,
|
||||
dates.start_date,
|
||||
dates.end_date,
|
||||
dates.last_interaction_at,
|
||||
@@ -334,6 +337,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
for (const [field, label, max] of [
|
||||
['daily_rate', 'Der Tagessatz', 1_000_000],
|
||||
['duration_days', 'Die Laufzeit', 100_000],
|
||||
['fixed_price', 'Der Projektpreis', 1_000_000_000],
|
||||
]) {
|
||||
if (req.body?.[field] === undefined) continue;
|
||||
const result = normalizeNumber(req.body[field], label, max);
|
||||
|
||||
Reference in New Issue
Block a user