feat(opportunities): letzten Phasenwechsel mit Benutzer und Zeit anzeigen

Neue Spalten stage_changed_at/stage_changed_by, gesetzt nur bei echtem
Phasenwechsel per PATCH. Der Dialog zeigt unter "Angelegt von" die Zeile
"Verschoben von ... am ... um ... in ...".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-10-01 11:29:00 +02:00
co-authored by Claude Opus 5.5
parent 5ba1ee1e0b
commit b9d6544b30
7 changed files with 62 additions and 9 deletions
@@ -281,18 +281,35 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
}
};
// MariaDB-DATETIME steht in UTC; ohne das angehaengte Z liest der Browser Ortszeit.
const movedAt = opportunity?.stage_changed_at
? new Date(`${opportunity.stage_changed_at.replace(' ', 'T')}Z`)
: null;
return (
<>
<Modal
title={opportunity ? opportunity.title : 'Neue Opportunity'}
subtitle={
opportunity
? `Angelegt von ${opportunity.created_by_name ?? 'unbekannt'}${
opportunity.created_at
? ` am ${new Date(opportunity.created_at).toLocaleDateString('de-DE')}`
: ''
}`
: 'Vertriebschance mit Projektdaten und Ansprechpartnern'
opportunity ? (
<>
<p className="truncate">
Angelegt von {opportunity.created_by_name ?? 'unbekannt'}
{opportunity.created_at &&
` am ${new Date(opportunity.created_at).toLocaleDateString('de-DE')}`}
</p>
{movedAt && (
<p className="truncate">
Verschoben von {opportunity.stage_changed_by_name ?? 'unbekannt'} am{' '}
{movedAt.toLocaleDateString('de-DE')} um{' '}
{movedAt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} in „
{STAGE_LABELS[opportunity.stage]}“
</p>
)}
</>
) : (
'Vertriebschance mit Projektdaten und Ansprechpartnern'
)
}
onClose={onClose}
size="xl"
+3 -2
View File
@@ -2,7 +2,8 @@ import React, { useEffect, useRef } from 'react';
interface ModalProps {
title: string;
subtitle?: string;
/** Text oder mehrere Zeilen; jede Zeile wird einzeln abgeschnitten. */
subtitle?: React.ReactNode;
onClose: () => void;
children: React.ReactNode;
footer?: React.ReactNode;
@@ -90,7 +91,7 @@ export const Modal: React.FC<ModalProps> = ({
<div className="flex shrink-0 items-start justify-between gap-4 border-b border-ink-100 px-6 py-5">
<div className="min-w-0">
<h2 className="truncate text-xl text-ink-900">{title}</h2>
{subtitle && <p className="mt-0.5 truncate text-xs font-semibold text-ink-400">{subtitle}</p>}
{subtitle && <div className="mt-0.5 truncate text-xs font-semibold text-ink-400">{subtitle}</div>}
</div>
<button
onClick={onClose}
+4
View File
@@ -346,6 +346,9 @@ export interface Opportunity {
created_by: number | null;
/** Zeitpunkt des Wechsels nach "Beauftragt" - Grundlage der Gewinn-Auswertung. */
won_at: string | null;
/** Letzter Phasenwechsel (UTC) und wer ihn gemacht hat; leer, solange nie verschoben. */
stage_changed_at?: string | null;
stage_changed_by?: number | null;
/** Gesetzt = liegt im Papierkorb; nach 90 Tagen wird sie dort geloescht. */
deleted_at: string | null;
deleted_by: number | null;
@@ -357,6 +360,7 @@ export interface Opportunity {
owner_color?: string | null;
owner_avatar?: string | null;
deleted_by_name?: string | null;
stage_changed_by_name?: string | null;
contacts: OpportunityContact[];
}