feat(akquise): "Von" mit dem angemeldeten Benutzer vorbelegen

In der Regel traegt jeder seine eigenen Funde ein - das Feld stand trotzdem bei
jedem Projekt auf leer. Vorbelegt ist jetzt der angemeldete Benutzer; wer fuer
jemand anderen eintraegt, waehlt ihn aus der Liste.

Der eigene Name steht dabei immer zur Auswahl (OptionSelect.alwaysOffer), auch
wenn ihn niemand in den Einstellungen gepflegt hat: sonst muesste sich jeder
erst selbst in die Liste eintragen, bevor er ein Projekt erfassen kann. Steht er
schon in der Liste, gilt deren Schreibweise - sonst stuenden "Markus" und
"Markus Meier" als zwei Namen in derselben Spalte.

Beim Bearbeiten bleibt der gespeicherte Name stehen, sonst wuerde ein Eintrag
beim Oeffnen den Finder wechseln.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-18 13:53:53 +02:00
co-authored by Claude Opus 5
parent 23087a12fb
commit 2717462c3a
5 changed files with 61 additions and 12 deletions
+34 -5
View File
@@ -3,6 +3,7 @@ import { DictateTextarea } from '../ui/DictationField';
import { OptionSelect, fieldClass } from './acquisitionUi';
import { labelClass } from '../leads/leadUi';
import { useToast } from '../Toast';
import { useAuth } from '../../lib/auth';
import { useNavigation } from '../../lib/navigation';
import { acquisitionsApi, type AcquisitionInput } from '../../lib/api';
import {
@@ -58,10 +59,16 @@ const today = () => new Date().toISOString().slice(0, 10);
const numberToInput = (value: number | null) =>
value === null ? '' : String(value).replace('.', ',');
function toFormState(entry: Acquisition | null): FormState {
/**
* `ownName` ist der Name des angemeldeten Benutzers und belegt "Von" vor: in der
* Regel traegt jeder seine eigenen Funde ein. Wer fuer jemand anderen eintraegt,
* waehlt ihn in der Liste aus. Beim Bearbeiten gilt der gespeicherte Wert - der
* Eintrag wuerde sonst beim Oeffnen den Finder wechseln.
*/
function toFormState(entry: Acquisition | null, ownName: string): FormState {
return {
found_at: entry?.found_at?.slice(0, 10) ?? today(),
sender: entry?.sender ?? '',
sender: entry?.sender ?? ownName,
provider: entry?.provider ?? '',
channel: entry?.channel ?? '',
title: entry?.title ?? '',
@@ -87,9 +94,25 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
onCancelEdit,
}) => {
const { addToast } = useToast();
const { user } = useAuth();
const { goTo } = useNavigation();
const openSettings = () => goTo('Einstellungen', { tab: 'akquise' });
const [form, setForm] = useState<FormState>(() => toFormState(editing));
/*
* Steht der eigene Name schon in der Liste, gilt deren Schreibweise - sonst
* haette man "Markus" aus der Liste und "Markus Meier" vom Konto als zwei
* Eintraege in derselben Spalte.
*/
const ownName = useMemo(() => {
const name = user?.name?.trim() ?? '';
if (!name) return '';
const listed = options.sender.find(
(option) => option.value.toLowerCase() === name.toLowerCase()
);
return listed?.value ?? name;
}, [user?.name, options.sender]);
const [form, setForm] = useState<FormState>(() => toFormState(editing, ownName));
const [showMore, setShowMore] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const titleRef = useRef<HTMLInputElement>(null);
@@ -104,7 +127,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
* Liste, das Formular oben.
*/
useEffect(() => {
setForm(toFormState(editing));
setForm(toFormState(editing, ownName));
if (editing) {
const hasExtras = Boolean(
editing.company ||
@@ -121,7 +144,10 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
rootRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
titleRef.current?.focus();
}
}, [editing]);
// `ownName` steht erst, wenn die Auswahllisten geladen sind - deshalb haengt
// der Effekt daran. Ein angefangenes Formular wird dabei zurueckgesetzt; das
// passiert nur direkt beim Oeffnen der Ansicht.
}, [editing, ownName]);
// Volumen mitrechnen wie in der Opportunity: Festpreis schlaegt Tagessatz.
const volume = useMemo(() => {
@@ -247,6 +273,8 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
className={fieldClass}
/>
</div>
{/* Vorbelegt mit dem angemeldeten Benutzer; fuer jemand anderen wird er
in der Liste gewechselt. */}
<OptionSelect
id="acq-sender"
label={ACQUISITION_FIELD_LABELS.sender}
@@ -254,6 +282,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
options={options.sender}
onChange={(value) => set('sender', value)}
onOpenSettings={openSettings}
alwaysOffer={ownName}
/>
{/* Anbieter bleibt Freitext: neue Vermittler tauchen zu oft auf, als dass
das Eintragen an einer gepflegten Liste haengen sollte. Die
+16 -3
View File
@@ -22,10 +22,16 @@ interface OptionSelectProps {
onChange: (value: string) => void;
/** Fuehrt zu Einstellungen -> Akquise, wo die Liste gepflegt wird. */
onOpenSettings: () => void;
/**
* Wert, der immer zur Auswahl steht, auch wenn er nicht in der Liste gepflegt
* ist - gedacht fuer den eigenen Namen im Feld "Von". Ohne das muesste sich
* jeder erst selbst in die Liste eintragen, bevor er ein Projekt erfassen kann.
*/
alwaysOffer?: string;
}
/**
* Auswahlfeld fuer "Von", "Anbieter", "Kanal" und "Berater".
* Auswahlfeld fuer "Von", "Kanal" und "Berater" ("Anbieter" ist Freitext).
*
* Steht im Eintrag ein Wert, den die Liste nicht (mehr) kennt, wird er zusaetzlich
* angeboten und als solcher gekennzeichnet. Ohne das wuerde das Bearbeiten eines
@@ -38,8 +44,14 @@ export const OptionSelect: React.FC<OptionSelectProps> = ({
options,
onChange,
onOpenSettings,
alwaysOffer,
}) => {
const isUnlisted = value !== '' && !options.some((option) => option.value === value);
const inList = (candidate: string) => options.some((option) => option.value === candidate);
// Nur anhaengen, wenn der Wert nicht schon gepflegt ist - sonst stuende er
// zweimal im Auswahlfeld.
const ownEntry = alwaysOffer && !inList(alwaysOffer) ? alwaysOffer : null;
const isUnlisted = value !== '' && !inList(value) && value !== ownEntry;
return (
<div>
@@ -53,6 +65,7 @@ export const OptionSelect: React.FC<OptionSelectProps> = ({
className={fieldClass}
>
<option value="">—</option>
{ownEntry && <option value={ownEntry}>{ownEntry}</option>}
{options.map((option) => (
<option key={option.id} value={option.value}>
{option.value}
@@ -61,7 +74,7 @@ export const OptionSelect: React.FC<OptionSelectProps> = ({
{isUnlisted && <option value={value}>{value} (nicht in der Liste)</option>}
</select>
{options.length === 0 && (
{options.length === 0 && !ownEntry && (
<p className="mt-1.5 text-[11px] text-ink-400">
Liste ist leer -{' '}
<button type="button" onClick={onOpenSettings} className="font-bold underline">
+1 -1
View File
@@ -271,7 +271,7 @@ export const ACQUISITION_FIELD_LABELS: Record<AcquisitionOptionField, string> =
/** Was in der jeweiligen Liste steht - als Hilfe in den Einstellungen. */
export const ACQUISITION_FIELD_HINTS: Record<AcquisitionOptionField, string> = {
sender: 'Personen, von denen Projekte kommen',
sender: 'Wer den Fund hereingebracht hat; vorbelegt ist der angemeldete Benutzer',
channel: 'Auf welchem Weg das Projekt kam',
consultant: 'Eigene Berater, für die Projekte gesucht werden',
};