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:
co-authored by
Claude Opus 5
parent
23087a12fb
commit
2717462c3a
@@ -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* |
|
||||
|
||||
@@ -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 `<datalist>` |
|
||||
| `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 `<datalist>` |
|
||||
| `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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user