feat(akquise): E-Mail-Adresse je Berater in den Einstellungen
Neue Spalte crm_acquisition_options.email, pflegbar unter Einstellungen -> Akquise direkt in der Berater-Zeile. Beim ersten Start werden vorhandene Berater einmalig mit vorname.nachname@perinco.de vorbelegt (Merker consultant_email_seeded); Namen ohne Nachnamen bleiben leer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b9d6544b30
commit
907b010f3c
@@ -34,11 +34,63 @@ const PlusIcon = (
|
||||
</svg>
|
||||
);
|
||||
|
||||
/*
|
||||
* E-Mail-Adresse eines Beraters, direkt in der Zeile bearbeitbar. Gespeichert
|
||||
* wird beim Verlassen des Feldes oder mit Enter - wie beim Umbenennen, nur ohne
|
||||
* extra Knopf, weil die Adresse oefter nachgetragen als geaendert wird.
|
||||
*/
|
||||
const EmailField: React.FC<{
|
||||
option: AcquisitionOption;
|
||||
onSave: (email: string) => Promise<void>;
|
||||
}> = ({ option, onSave }) => {
|
||||
const [draft, setDraft] = useState(option.email ?? '');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(option.email ?? '');
|
||||
}, [option.email]);
|
||||
|
||||
const save = async () => {
|
||||
const email = draft.trim();
|
||||
if (email === (option.email ?? '')) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave(email);
|
||||
} catch {
|
||||
setDraft(option.email ?? '');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
type="email"
|
||||
value={draft}
|
||||
disabled={isSaving}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
if (event.key === 'Escape') setDraft(option.email ?? '');
|
||||
}}
|
||||
onBlur={save}
|
||||
placeholder="E-Mail-Adresse"
|
||||
aria-label={`E-Mail-Adresse von ${option.value}`}
|
||||
className={`${fieldClass} py-1.5 text-xs`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const OptionRow: React.FC<{
|
||||
option: AcquisitionOption;
|
||||
onRenamed: (value: string) => Promise<void>;
|
||||
onDelete: () => void;
|
||||
}> = ({ option, onRenamed, onDelete }) => {
|
||||
/** Nur bei "Berater" gesetzt - dann zeigt die Zeile das Adressfeld. */
|
||||
onEmailSaved?: (email: string) => Promise<void>;
|
||||
}> = ({ option, onRenamed, onDelete, onEmailSaved }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(option.value);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -66,8 +118,18 @@ const OptionRow: React.FC<{
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="flex items-center gap-2 rounded-xl border border-ink-200 bg-surface px-3 py-2">
|
||||
{isEditing ? (
|
||||
<li className="rounded-xl border border-ink-200 bg-surface px-3 py-2">
|
||||
<div className="flex items-center gap-2">{renderName()}</div>
|
||||
{onEmailSaved && (
|
||||
<div className="mt-1.5">
|
||||
<EmailField option={option} onSave={onEmailSaved} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
||||
function renderName() {
|
||||
return isEditing ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={draft}
|
||||
@@ -125,9 +187,8 @@ const OptionRow: React.FC<{
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const AcquisitionSettings: React.FC = () => {
|
||||
@@ -190,6 +251,17 @@ export const AcquisitionSettings: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveEmail = async (option: AcquisitionOption, email: string) => {
|
||||
try {
|
||||
await acquisitionOptionsApi.updateEmail(option.id, email);
|
||||
addToast(email ? `Adresse für ${option.value} gespeichert.` : 'Adresse entfernt.', 'success');
|
||||
await load(true);
|
||||
} catch (err) {
|
||||
addToast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!toDelete) return;
|
||||
try {
|
||||
@@ -289,6 +361,9 @@ export const AcquisitionSettings: React.FC = () => {
|
||||
option={option}
|
||||
onRenamed={(value) => rename(option, value)}
|
||||
onDelete={() => setToDelete({ field, option })}
|
||||
onEmailSaved={
|
||||
field === 'consultant' ? (email) => saveEmail(option, email) : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
+9
-2
@@ -490,10 +490,10 @@ export const acquisitionsApi = {
|
||||
export const acquisitionOptionsApi = {
|
||||
list: () => request<AcquisitionOptions>('/acquisitions/options'),
|
||||
|
||||
create: (field: AcquisitionOptionField, value: string) =>
|
||||
create: (field: AcquisitionOptionField, value: string, email?: string | null) =>
|
||||
request<AcquisitionOption & { field: AcquisitionOptionField }>('/acquisitions/options', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ field, value }),
|
||||
body: JSON.stringify({ field, value, email }),
|
||||
}),
|
||||
|
||||
/** Umbenennen zieht die vorhandenen Akquise-Eintraege mit; `updated` sagt wie viele. */
|
||||
@@ -503,6 +503,13 @@ export const acquisitionOptionsApi = {
|
||||
{ method: 'PATCH', body: JSON.stringify({ value }) }
|
||||
),
|
||||
|
||||
/** Nur die E-Mail-Adresse eines Beraters; leer entfernt sie. */
|
||||
updateEmail: (id: number, email: string) =>
|
||||
request<AcquisitionOption & { field: AcquisitionOptionField; updated: number }>(
|
||||
`/acquisitions/options/${id}`,
|
||||
{ method: 'PATCH', body: JSON.stringify({ email }) }
|
||||
),
|
||||
|
||||
/** 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' }),
|
||||
|
||||
@@ -397,6 +397,8 @@ export const ACQUISITION_FIELD_HINTS: Record<AcquisitionOptionField, string> = {
|
||||
export interface AcquisitionOption {
|
||||
id: number;
|
||||
value: string;
|
||||
/** Nur bei "Berater" gepflegt, sonst immer null. */
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user