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:
co-authored by
Claude Opus 5.5
parent
5ba1ee1e0b
commit
b9d6544b30
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user