diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md index 29e1d30..d519713 100644 --- a/docs/07-anleitung-benutzer.md +++ b/docs/07-anleitung-benutzer.md @@ -88,7 +88,7 @@ weg. | Feld | wofür | | --- | --- | | **Datum** | wann gefunden. Vorbelegt mit heute; entscheidet später über Archiv und Löschung | -| **Von** | Auswahl: wer es geschickt hat | +| **Von** | Auswahl: wer den Fund hereingebracht hat. Vorbelegt mit dem angemeldeten Benutzer | | **Anbieter** | Freitext: Vermittler oder Portal, z. B. Hays. Vorschläge aus den bisherigen Einträgen | | **Kanal** | Auswahl: auf welchem Weg es kam | | **Projekttitel** | das einzige Pflichtfeld | @@ -99,6 +99,12 @@ weg. *Einstellungen → Akquise* und lassen sich dort von jedem Benutzer pflegen. Ist eine Liste noch leer, steht unter dem Feld ein Verweis dorthin. +**Von** ist mit dem **angemeldeten Benutzer** vorbelegt - in der Regel trägt +jeder seine eigenen Funde ein. Trägt Markus etwas für Artur ein, wählt er Artur +aus der Liste. Der eigene Name steht dabei immer zur Auswahl, auch wenn ihn +niemand in der Liste gepflegt hat; beim Bearbeiten eines Eintrags bleibt der +gespeicherte Name stehen. + **Anbieter** ist bewusst keine Liste, sondern ein Freitextfeld: neue Vermittler tauchen zu oft auf, als dass das Eintragen daran hängen sollte. Das Feld schlägt vor, was schon einmal benutzt wurde - der zuletzt verwendete Anbieter zuerst. @@ -119,7 +125,8 @@ wird geleert - für das nächste Projekt aus derselben Quelle. | Spalte | Bedeutung | | --- | --- | | **Datum** | Funddatum; der Mauszeiger verrät das Alter in Tagen | -| **Von**, **Anbieter**, **Kanal** | woher es kam | +| **Von** | wer den Fund hereingebracht hat | +| **Anbieter**, **Kanal** | woher es kam | | **Projekttitel** | mit Link anklickbar (neuer Tab); darunter klein Endkunde, Tagessatz bzw. Festpreis und Personentage | | **Berater** | für wen | | **Opportunity** | ein Klick macht daraus eine Opportunity in der Phase *Neu* | diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md index 4f5800b..aef577e 100644 --- a/docs/09-quellcode.md +++ b/docs/09-quellcode.md @@ -268,7 +268,7 @@ Warum genau diese Konstanten festgelegt sind, steht in | `components/acquisition/Acquisitions.tsx` | Akquise und Archiv (`view`); Suche, Sortierung, Übernahme, Löschen | | `components/acquisition/AcquisitionForm.tsx` | Eingabe über der Liste; behält Datum, Anbieter, Kanal und Berater für den nächsten Eintrag | | `components/acquisition/AcquisitionTable.tsx` | die Liste; `safeLink()` lässt nur http(s) zum Link werden | -| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen. *Anbieter* nutzt es nicht: Freitext mit `` | +| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen; `alwaysOffer` hält den eigenen Namen für *Von* immer wählbar. *Anbieter* nutzt es nicht: Freitext mit `` | | `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen | | `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `KindBadge`, `Avatar`, Farben der Kontaktart | | `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs | diff --git a/src/components/acquisition/AcquisitionForm.tsx b/src/components/acquisition/AcquisitionForm.tsx index b19131a..f670749 100644 --- a/src/components/acquisition/AcquisitionForm.tsx +++ b/src/components/acquisition/AcquisitionForm.tsx @@ -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 = ({ onCancelEdit, }) => { const { addToast } = useToast(); + const { user } = useAuth(); const { goTo } = useNavigation(); const openSettings = () => goTo('Einstellungen', { tab: 'akquise' }); - const [form, setForm] = useState(() => 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(() => toFormState(editing, ownName)); const [showMore, setShowMore] = useState(false); const [isSaving, setIsSaving] = useState(false); const titleRef = useRef(null); @@ -104,7 +127,7 @@ export const AcquisitionForm: React.FC = ({ * 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 = ({ 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 = ({ className={fieldClass} /> + {/* Vorbelegt mit dem angemeldeten Benutzer; fuer jemand anderen wird er + in der Liste gewechselt. */} = ({ 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 diff --git a/src/components/acquisition/acquisitionUi.tsx b/src/components/acquisition/acquisitionUi.tsx index 85d5a32..eceb2a0 100644 --- a/src/components/acquisition/acquisitionUi.tsx +++ b/src/components/acquisition/acquisitionUi.tsx @@ -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 = ({ 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 (
@@ -53,6 +65,7 @@ export const OptionSelect: React.FC = ({ className={fieldClass} > + {ownEntry && } {options.map((option) => (