feat(opportunities): Uebernahme zurueck in die Akquise schieben

Im Dialog einer Opportunity aus der Akquise nimmt "Zurueck in die Akquise"
eine versehentliche Uebernahme zurueck: alle Opportunities aus dem Eintrag
wandern in den Papierkorb, der Eintrag steht mit seinem Funddatum wieder in
Eingang bzw. Archiv. Neue Spalte crm_opportunities.acquisition_id, damit bei
mehreren Beratern alle gefunden werden. Wiederherstellen haengt den Eintrag
wieder an die Opportunity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-17 13:12:03 +02:00
co-authored by Claude Opus 5
parent 0ad7b32dbf
commit 33dedaedfb
9 changed files with 270 additions and 19 deletions
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Modal } from '../ui/Modal';
import { ConfirmDialog, Modal } from '../ui/Modal';
import { LeadModal } from '../leads/LeadModal';
import { DictateTextarea, DictateButton, DictationHint } from '../ui/DictationField';
import { Avatar, inputClass, labelClass, labelRowClass, labelTextClass } from '../leads/leadUi';
@@ -96,6 +96,8 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
const [isCreatingLead, setIsCreatingLead] = useState(false);
const [tagInput, setTagInput] = useState('');
const [isSaving, setIsSaving] = useState(false);
// Rueckfrage vor "Zurueck in die Akquise" - der Schritt raeumt die Opportunity weg.
const [confirmBack, setConfirmBack] = useState(false);
/*
* Die Berater kommen aus derselben Auswahlliste wie in der Akquise. Geladen
@@ -223,6 +225,32 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
}
};
/*
* Uebernahme zuruecknehmen. Ungespeicherte Aenderungen im Formular gehen dabei
* verloren - die Opportunity landet ohnehin im Papierkorb, und die Rueckfrage
* sagt, was passiert.
*/
const moveBackToAcquisition = async () => {
if (!opportunity) return;
setConfirmBack(false);
setIsSaving(true);
try {
const result = await opportunitiesApi.backToAcquisition(opportunity.id);
addToast(
result.trashed.length > 1
? `„${opportunity.title}" steht wieder in der Akquise, die ${result.trashed.length} Opportunities liegen im Papierkorb.`
: `„${opportunity.title}" steht wieder in der Akquise, die Opportunity liegt im Papierkorb.`,
'success'
);
onSaved();
onClose();
} catch (err) {
addToast(err instanceof Error ? err.message : 'Zurücknehmen fehlgeschlagen.', 'error');
} finally {
setIsSaving(false);
}
};
return (
<>
<Modal
@@ -239,7 +267,22 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
onClose={onClose}
size="xl"
footer={
<div className="flex gap-3">
<div className="flex flex-wrap gap-3">
{/* Nur bei Opportunities aus der Akquise, deren Eintrag noch da ist. */}
{opportunity?.acquisition_available && (
<button
type="button"
onClick={() => setConfirmBack(true)}
disabled={isSaving}
title="Übernahme zurücknehmen: der Eintrag steht wieder in der Akquise"
className="flex items-center justify-center gap-2 rounded-xl border border-ink-200 px-4 py-3 text-sm font-bold text-ink-600 transition hover:border-brand-300 hover:text-brand-700 disabled:opacity-60"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
Zurück in die Akquise
</button>
)}
<button
type="button"
onClick={onClose}
@@ -751,6 +794,16 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
onSaved={onSaved}
/>
)}
{confirmBack && opportunity && (
<ConfirmDialog
title="Zurück in die Akquise?"
message={`„${opportunity.title}" steht danach wieder in der Akquise, mit seinem ursprünglichen Funddatum. Die Opportunity wandert in den Papierkorb, bei mehreren Beratern alle aus diesem Eintrag. Ungespeicherte Änderungen gehen verloren.`}
confirmLabel="Zurückschieben"
tone="neutral"
onConfirm={moveBackToAcquisition}
onCancel={() => setConfirmBack(false)}
/>
)}
</>
);
};
+9
View File
@@ -223,6 +223,15 @@ export const opportunitiesApi = {
remove: (id: number) => request<{ ok: true }>(`/opportunities/${id}`, { method: 'DELETE' }),
restore: (id: number) =>
request<Opportunity>(`/opportunities/${id}/restore`, { method: 'POST' }),
/**
* Nimmt die Uebernahme aus der Akquise zurueck: alle Opportunities aus dem
* Eintrag wandern in den Papierkorb, der Eintrag steht wieder in der Akquise.
*/
backToAcquisition: (id: number) =>
request<{ ok: true; acquisition_id: number; trashed: number[] }>(
`/opportunities/${id}/acquisition`,
{ method: 'POST' }
),
emptyTrash: () =>
request<{ ok: true; purged: number }>('/opportunities/trash/empty', { method: 'POST' }),
};
+7
View File
@@ -296,6 +296,13 @@ export interface Opportunity {
* entstehen bei der Uebernahme drei Opportunities.
*/
consultant: string | null;
/**
* Eintrag der Akquise, aus dem die Opportunity uebernommen wurde - null bei
* von Hand angelegten. Bei drei Beratern zeigen alle drei auf denselben.
*/
acquisition_id: number | null;
/** Der Eintrag existiert noch und liegt nicht im Papierkorb: der Rueckweg ist offen. */
acquisition_available?: boolean;
description: string | null;
stage: OpportunityStage;
/** 0-100 */