feat(akquise): Auswahllisten statt Freitext, Felder abgesetzt
"Von", "Anbieter", "Kanal" und "Berater" sind Dropdowns. Ihre Inhalte stehen in crm_acquisition_options und werden im neuen Reiter Einstellungen -> Akquise gepflegt. Freitext hat aus "Hays", "hays" und "Hays AG" drei Anbieter gemacht; der Unique-Schluessel greift ueber die Kollation auch bei abweichender Gross-/Kleinschreibung. Eine Tabelle mit Feldkennzeichen statt vier Tabellen: die Felder verhalten sich gleich, also ist jede weitere Liste ein Wert im ENUM statt einer neuen Route und einer neuen Oberflaeche. Zwei Entscheidungen dabei: - Pflegen darf jeder Angemeldete, nicht nur ein Administrator. Eine Liste, die nur ein Admin ergaenzen kann, blockiert die Eingabe genau dann, wenn ein neuer Anbieter auftaucht. - Die Eintraege halten den Text, keinen Fremdschluessel. Umbenennen zieht sie darum ausdruecklich mit (in einer Transaktion), Loeschen nicht: eine Liste aufzuraeumen darf keine Daten aendern. Ein Wert, der nicht mehr in der Liste steht, wird im Dropdown weiter angeboten und als solcher gekennzeichnet - sonst wuerde das Bearbeiten eines alten Eintrags ihn stillschweigend ueberschreiben. Das Backend prueft beim Speichern eines Eintrags nur noch die Laenge und nicht mehr die Zugehoerigkeit zur Liste - aus demselben Grund. Dazu: die Felder der Akquise-Eingabe sind abgesetzt (getoente Flaeche, deutlicherer Rahmen, beim Hineinklicken weiss). Auf der weissen Karte gingen sie unter, und im Formular stehen bis zu fuenfzehn davon nebeneinander. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b8a78f609c
commit
05cc261900
@@ -1,13 +1,15 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DictateTextarea } from '../ui/DictationField';
|
||||
import { inputClass, labelClass } from '../leads/leadUi';
|
||||
import { OptionSelect, fieldClass } from './acquisitionUi';
|
||||
import { labelClass } from '../leads/leadUi';
|
||||
import { useToast } from '../Toast';
|
||||
import { useNavigation } from '../../lib/navigation';
|
||||
import { acquisitionsApi, type AcquisitionInput } from '../../lib/api';
|
||||
import {
|
||||
ACQUISITION_CHANNELS,
|
||||
ACQUISITION_FIELD_LABELS,
|
||||
formatEuro,
|
||||
type Acquisition,
|
||||
type AcquisitionChannel,
|
||||
type AcquisitionOptions,
|
||||
} from '../../types';
|
||||
|
||||
/*
|
||||
@@ -23,7 +25,8 @@ import {
|
||||
interface AcquisitionFormProps {
|
||||
/** Gesetzt = Bearbeiten, sonst Neuanlage. */
|
||||
editing: Acquisition | null;
|
||||
suggestions: { providers: string[]; consultants: string[]; senders: string[] };
|
||||
/** Inhalte der Dropdowns, gepflegt unter Einstellungen -> Akquise. */
|
||||
options: AcquisitionOptions;
|
||||
onSaved: () => void;
|
||||
onCancelEdit: () => void;
|
||||
}
|
||||
@@ -74,42 +77,15 @@ function toFormState(entry: Acquisition | null): FormState {
|
||||
};
|
||||
}
|
||||
|
||||
/** Freitextfeld mit Vorschlagsliste aus den bisherigen Eintraegen. */
|
||||
const SuggestInput: React.FC<{
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
options: string[];
|
||||
placeholder?: string;
|
||||
onChange: (value: string) => void;
|
||||
}> = ({ id, label, value, options, placeholder, onChange }) => (
|
||||
<div>
|
||||
<label className={labelClass} htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
list={`${id}-vorschlaege`}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={inputClass}
|
||||
/>
|
||||
<datalist id={`${id}-vorschlaege`}>
|
||||
{options.map((option) => (
|
||||
<option key={option} value={option} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
editing,
|
||||
suggestions,
|
||||
options,
|
||||
onSaved,
|
||||
onCancelEdit,
|
||||
}) => {
|
||||
const { addToast } = useToast();
|
||||
const { goTo } = useNavigation();
|
||||
const openSettings = () => goTo('Einstellungen', { tab: 'akquise' });
|
||||
const [form, setForm] = useState<FormState>(() => toFormState(editing));
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -172,7 +148,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
found_at: form.found_at || today(),
|
||||
sender: form.sender.trim() || null,
|
||||
provider: form.provider.trim() || null,
|
||||
channel: (form.channel as AcquisitionChannel) || null,
|
||||
channel: form.channel || null,
|
||||
title: form.title.trim(),
|
||||
consultant: form.consultant.trim() || null,
|
||||
link: form.link.trim() || null,
|
||||
@@ -265,43 +241,33 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
type="date"
|
||||
value={form.found_at}
|
||||
onChange={(event) => set('found_at', event.target.value)}
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<SuggestInput
|
||||
<OptionSelect
|
||||
id="acq-sender"
|
||||
label="Von"
|
||||
label={ACQUISITION_FIELD_LABELS.sender}
|
||||
value={form.sender}
|
||||
options={suggestions.senders}
|
||||
placeholder="Wer hat es geschickt?"
|
||||
options={options.sender}
|
||||
onChange={(value) => set('sender', value)}
|
||||
onOpenSettings={openSettings}
|
||||
/>
|
||||
<SuggestInput
|
||||
<OptionSelect
|
||||
id="acq-provider"
|
||||
label="Anbieter"
|
||||
label={ACQUISITION_FIELD_LABELS.provider}
|
||||
value={form.provider}
|
||||
options={suggestions.providers}
|
||||
placeholder="z. B. Hays"
|
||||
options={options.provider}
|
||||
onChange={(value) => set('provider', value)}
|
||||
onOpenSettings={openSettings}
|
||||
/>
|
||||
<OptionSelect
|
||||
id="acq-channel"
|
||||
label={ACQUISITION_FIELD_LABELS.channel}
|
||||
value={form.channel}
|
||||
options={options.channel}
|
||||
onChange={(value) => set('channel', value)}
|
||||
onOpenSettings={openSettings}
|
||||
/>
|
||||
<div>
|
||||
<label className={labelClass} htmlFor="acq-channel">
|
||||
Kanal
|
||||
</label>
|
||||
<select
|
||||
id="acq-channel"
|
||||
value={form.channel}
|
||||
onChange={(event) => set('channel', event.target.value)}
|
||||
className={inputClass}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{ACQUISITION_CHANNELS.map((channel) => (
|
||||
<option key={channel} value={channel}>
|
||||
{channel}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-[2fr_1fr_2fr]">
|
||||
@@ -316,16 +282,16 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
value={form.title}
|
||||
onChange={(event) => set('title', event.target.value)}
|
||||
placeholder="z. B. SAP FI Rollout Süddeutschland"
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<SuggestInput
|
||||
<OptionSelect
|
||||
id="acq-consultant"
|
||||
label="Berater"
|
||||
label={ACQUISITION_FIELD_LABELS.consultant}
|
||||
value={form.consultant}
|
||||
options={suggestions.consultants}
|
||||
placeholder="für wen?"
|
||||
options={options.consultant}
|
||||
onChange={(value) => set('consultant', value)}
|
||||
onOpenSettings={openSettings}
|
||||
/>
|
||||
<div>
|
||||
<label className={labelClass} htmlFor="acq-link">
|
||||
@@ -340,7 +306,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
value={form.link}
|
||||
onChange={(event) => set('link', event.target.value)}
|
||||
placeholder="hays.de/projekt/1 - macht den Titel in der Liste klickbar"
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,7 +342,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
value={form.company}
|
||||
onChange={(event) => set('company', event.target.value)}
|
||||
placeholder="Firma hinter dem Projekt"
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -388,7 +354,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
type="date"
|
||||
value={form.start_date}
|
||||
onChange={(event) => set('start_date', event.target.value)}
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -400,7 +366,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
type="date"
|
||||
value={form.end_date}
|
||||
onChange={(event) => set('end_date', event.target.value)}
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,7 +382,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
value={form.daily_rate}
|
||||
onChange={(event) => set('daily_rate', event.target.value)}
|
||||
placeholder="1150"
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -429,7 +395,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
value={form.duration_days}
|
||||
onChange={(event) => set('duration_days', event.target.value)}
|
||||
placeholder="40"
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -442,7 +408,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
value={form.fixed_price}
|
||||
onChange={(event) => set('fixed_price', event.target.value)}
|
||||
placeholder="Festpreis statt Tagessatz × Laufzeit"
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -472,7 +438,7 @@ export const AcquisitionForm: React.FC<AcquisitionFormProps> = ({
|
||||
value={form.next_step}
|
||||
onChange={(event) => set('next_step', event.target.value)}
|
||||
placeholder="z. B. Profil bis Donnerstag schicken"
|
||||
className={inputClass}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user