feat(akquise): Mail an Berater bei neuem Projekt

Wird in der Akquise ein Projekt mit Berater angelegt (oder ein Berater
beim Bearbeiten hinzugefuegt), bekommt er eine Mail ueber Graph sendMail
aus crm@perinco.de; Reply-To ist der eintragende Benutzer. Je Berater
abschaltbar unter Einstellungen -> Akquise (Spalte notify, Vorgabe an).
Versand laeuft nach der Antwort und kippt das Speichern nie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-10-21 09:38:13 +02:00
co-authored by Claude Opus 5.5
parent 907b010f3c
commit eaef454442
15 changed files with 402 additions and 24 deletions
@@ -84,13 +84,53 @@ const EmailField: React.FC<{
);
};
/*
* Schalter "Mail bei neuem Projekt". Ohne Adresse bleibt er bedienbar, sagt
* aber, dass so keine Mail herausgeht - sonst wirkt er an und tut nichts.
*/
const NotifyToggle: React.FC<{
option: AcquisitionOption;
onChange: (notify: boolean) => Promise<void>;
}> = ({ option, onChange }) => {
const [isSaving, setIsSaving] = useState(false);
const toggle = async (notify: boolean) => {
setIsSaving(true);
try {
await onChange(notify);
} catch {
// Meldung kommt aus onChange, der Haken bleibt auf dem gespeicherten Stand.
} finally {
setIsSaving(false);
}
};
return (
<label className="mt-1.5 flex cursor-pointer items-center gap-2 text-xs text-ink-600">
<input
type="checkbox"
checked={option.notify}
disabled={isSaving}
onChange={(event) => toggle(event.target.checked)}
className="h-4 w-4 accent-brand-500"
/>
Mail bei neuem Projekt
{option.notify && !option.email && (
<span className="text-amber-600">- ohne Adresse geht keine Mail heraus</span>
)}
</label>
);
};
const OptionRow: React.FC<{
option: AcquisitionOption;
onRenamed: (value: string) => Promise<void>;
onDelete: () => void;
/** Nur bei "Berater" gesetzt - dann zeigt die Zeile das Adressfeld. */
onEmailSaved?: (email: string) => Promise<void>;
}> = ({ option, onRenamed, onDelete, onEmailSaved }) => {
/** Nur bei "Berater" gesetzt - Schalter fuer die Mail bei neuem Projekt. */
onNotifyChanged?: (notify: boolean) => Promise<void>;
}> = ({ option, onRenamed, onDelete, onEmailSaved, onNotifyChanged }) => {
const [isEditing, setIsEditing] = useState(false);
const [draft, setDraft] = useState(option.value);
const [isSaving, setIsSaving] = useState(false);
@@ -125,6 +165,7 @@ const OptionRow: React.FC<{
<EmailField option={option} onSave={onEmailSaved} />
</div>
)}
{onNotifyChanged && <NotifyToggle option={option} onChange={onNotifyChanged} />}
</li>
);
@@ -262,6 +303,22 @@ export const AcquisitionSettings: React.FC = () => {
}
};
const saveNotify = async (option: AcquisitionOption, notify: boolean) => {
try {
await acquisitionOptionsApi.updateNotify(option.id, notify);
addToast(
notify
? `${option.value} bekommt eine Mail bei neuen Projekten.`
: `Keine Mails mehr an ${option.value}.`,
'success'
);
await load(true);
} catch (err) {
addToast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error');
throw err;
}
};
const confirmDelete = async () => {
if (!toDelete) return;
try {
@@ -364,6 +421,9 @@ export const AcquisitionSettings: React.FC = () => {
onEmailSaved={
field === 'consultant' ? (email) => saveEmail(option, email) : undefined
}
onNotifyChanged={
field === 'consultant' ? (notify) => saveNotify(option, notify) : undefined
}
/>
))}
</ul>
+7
View File
@@ -510,6 +510,13 @@ export const acquisitionOptionsApi = {
{ method: 'PATCH', body: JSON.stringify({ email }) }
),
/** Mail an den Berater bei neuem Projekt an- oder abschalten. */
updateNotify: (id: number, notify: boolean) =>
request<AcquisitionOption & { field: AcquisitionOptionField; updated: number }>(
`/acquisitions/options/${id}`,
{ method: 'PATCH', body: JSON.stringify({ notify }) }
),
/** Entfernt nur die Auswahl; `still_in_use` zaehlt die Eintraege, die den Wert behalten. */
remove: (id: number) =>
request<{ ok: true; still_in_use: number }>(`/acquisitions/options/${id}`, { method: 'DELETE' }),
+2
View File
@@ -399,6 +399,8 @@ export interface AcquisitionOption {
value: string;
/** Nur bei "Berater" gepflegt, sonst immer null. */
email: string | null;
/** Nur bei "Berater": Mail, sobald er in der Akquise einem Projekt zugeordnet wird. */
notify: boolean;
}
/*