feat(kontakte): Art einer Verbindung nachtraeglich aendern
Bisher liess sich eine angelegte Verbindung nur loeschen und neu anlegen, nur um etwa "Kollege" in "Vorgesetzter" zu drehen. Die Art steht jetzt als Auswahlfeld in jeder Verbindungszeile - im Kontaktdialog und in der Seitenleiste der Netzwerkansicht. Gespeichert wird sofort per PATCH /api/connections/:id, unabhaengig vom Speichern-Knopf des Formulars: eine Verbindung ist ein eigener Datensatz und haengt nicht am Kontaktformular, genau wie das Anlegen und Entfernen daneben. Backend und connectionsApi.update() gab es bereits, nur die Oberflaeche bot es nirgends an. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
46983a7e2c
commit
f678e988ac
@@ -576,6 +576,11 @@ Knoten ziehen fixiert ihn (grüner Punkt), Doppelklick löst die Fixierung.
|
||||
Über *Verbindung erstellen* zwei Kontakte anklicken, um eine Kante anzulegen;
|
||||
Esc bricht ab. Die Ansicht respektiert Suche und Statusfilter.
|
||||
|
||||
Die Art einer bestehenden Verbindung lässt sich sowohl im Kontaktdialog als auch
|
||||
in der Seitenleiste der Netzwerkansicht direkt umstellen
|
||||
(`PATCH /api/connections/:id`, ohne Speichern-Knopf). Vorher musste man sie
|
||||
löschen und neu anlegen.
|
||||
|
||||
Ein Klick auf einen Knoten hebt ihn samt Nachbarn hervor und blendet den Rest
|
||||
auf 22 % Deckkraft ab. Ein Klick auf eine **freie Fläche** hebt die Auswahl
|
||||
wieder auf - der Ausgangszustand der Ansicht. Damit das Verschieben nicht
|
||||
|
||||
@@ -482,6 +482,12 @@ Drei Ansichten, davon eine besondere:
|
||||
Ein Klick auf eine Karte öffnet den Kontakt zum Bearbeiten - mit Foto, Anschrift,
|
||||
Art, Zuständigkeit, Tags, Notizen und den Verbindungen zu anderen Kontakten.
|
||||
|
||||
Bei den **Verbindungen** steht die Art (Kollege, Vorgesetzter, Kunde, …) als
|
||||
Auswahlfeld neben dem Namen. Umstellen genügt: Die Änderung wird sofort
|
||||
gespeichert, unabhängig vom Knopf *Speichern* darunter - eine Verbindung ist ein
|
||||
eigener Eintrag und nicht Teil des Kontaktformulars. Zum Löschen bleibt das
|
||||
Kreuz rechts.
|
||||
|
||||
Die **Art** sagt, mit wem man es zu tun hat: **Recruiter**, **Partner** oder
|
||||
**Endkunde**. Früher stand an dieser Stelle ein Vertriebsstatus (Neu,
|
||||
Kontaktiert, …); der sagte über den Kontakt selbst nichts. Bei der Umstellung am
|
||||
@@ -522,6 +528,10 @@ setzt ihn fest (grüner Punkt), Doppelklick löst ihn wieder. Über *Verbindung
|
||||
erstellen* zwei Kontakte anklicken, um eine Verbindung anzulegen; `Esc` bricht ab.
|
||||
Suche und Artfilter wirken auch hier.
|
||||
|
||||
Im Feld rechts, das nach dem Klick auf einen Kontakt erscheint, steht unter
|
||||
jeder Verbindung deren **Art** als Auswahlfeld - umstellen genügt, gespeichert
|
||||
wird sofort.
|
||||
|
||||
Ein Klick auf einen Knoten hebt ihn und seine Nachbarn hervor, alles andere wird
|
||||
blass. **Ein Klick auf eine freie Stelle nimmt die Hervorhebung wieder zurück** -
|
||||
das Bild sieht danach aus wie beim Wechsel in die Netzwerkansicht. Verschieben
|
||||
|
||||
@@ -318,8 +318,8 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| `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>`. Dazu `ConsultantSelect` (mehrere Berater: Auswahlfeld hängt an, Plaketten nehmen weg) und `ConsultantList` für die Anzeige in der Liste |
|
||||
| `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen |
|
||||
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass` (Beschriftung als eigene Zeile), `labelTextClass` + `labelRowClass` (Zeile mit Knopf rechts, z. B. Diktat - `min-h-7` haelt sie so hoch wie den Knopf, damit Felder in Nachbarspalten auf einer Linie beginnen), `KindBadge`, `Avatar`, Farben der Kontaktart |
|
||||
| `components/leads/LeadModal.tsx` | Kontaktformular. Beim Neuanlegen ist der angemeldete Benutzer *Zuständig* (`useAuth()`, Rückfallebene `users[0]`) - beim Bearbeiten bleibt die gespeicherte Zuständigkeit stehen. Neben dem LinkedIn-Feld holt *Foto* das Profilbild; beim Verlassen des Feldes geschieht das einmal je Adresse von selbst, aber nur solange noch kein Bild gesetzt ist (`triedProfiles`) - ein hochgeladenes Foto wird nie überschrieben. Fehler des automatischen Versuchs stehen als Hinweis unter dem Feld, nur der Klick auf *Foto* meldet sich als Toast |
|
||||
| `components/leads/LeadNetwork.tsx` | Netzwerkdiagramm (d3-force). Ein Klick auf leere Fläche hebt die Auswahl auf, das Diagramm ist danach wieder so hell wie beim Wechsel in die Ansicht; ein Ziehen des Hintergrunds (mehr als 4 px) verschiebt nur und lässt die Auswahl stehen. Knoten stoppen das `pointerdown`, ihre Klicks erreichen den Hintergrund also nicht |
|
||||
| `components/leads/LeadModal.tsx` | Kontaktformular. Beim Neuanlegen ist der angemeldete Benutzer *Zuständig* (`useAuth()`, Rückfallebene `users[0]`) - beim Bearbeiten bleibt die gespeicherte Zuständigkeit stehen. Neben dem LinkedIn-Feld holt *Foto* das Profilbild; beim Verlassen des Feldes geschieht das einmal je Adresse von selbst, aber nur solange noch kein Bild gesetzt ist (`triedProfiles`) - ein hochgeladenes Foto wird nie überschrieben. Fehler des automatischen Versuchs stehen als Hinweis unter dem Feld, nur der Klick auf *Foto* meldet sich als Toast. In der Liste der Verbindungen ist die **Art** ein Auswahlfeld: die Änderung geht als `PATCH /api/connections/:id` sofort raus, ohne den Speichern-Knopf - eine Verbindung ist ein eigener Datensatz und hängt nicht am Formular |
|
||||
| `components/leads/LeadNetwork.tsx` | Netzwerkdiagramm (d3-force). Ein Klick auf leere Fläche hebt die Auswahl auf, das Diagramm ist danach wieder so hell wie beim Wechsel in die Ansicht; ein Ziehen des Hintergrunds (mehr als 4 px) verschiebt nur und lässt die Auswahl stehen. Knoten stoppen das `pointerdown`, ihre Klicks erreichen den Hintergrund also nicht. In der Seitenleiste des gewählten Kontakts ist die Art jeder Verbindung umstellbar, genau wie im Kontaktdialog |
|
||||
| `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs |
|
||||
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Übernehmen, Sammelübernahme |
|
||||
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergänzen eines vorhandenen Kontakts |
|
||||
|
||||
@@ -119,6 +119,8 @@ export const LeadModal: React.FC<LeadModalProps> = ({
|
||||
const [newConnectionId, setNewConnectionId] = useState('');
|
||||
const [newConnectionType, setNewConnectionType] = useState<ConnectionType>('Netzwerk');
|
||||
const [isLinking, setIsLinking] = useState(false);
|
||||
/** Verbindung, deren Art gerade gespeichert wird - sperrt nur diese Zeile. */
|
||||
const [changingConnectionId, setChangingConnectionId] = useState<number | null>(null);
|
||||
|
||||
const leadById = useMemo(() => new Map(leads.map((l) => [l.id, l])), [leads]);
|
||||
|
||||
@@ -246,6 +248,24 @@ export const LeadModal: React.FC<LeadModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Die Art einer bestehenden Verbindung aendern. Das geht ohne Speichern-Knopf:
|
||||
* die Verbindung haengt nicht am Formular, sondern ist ein eigener Datensatz -
|
||||
* genau wie das Anlegen und das Entfernen daneben.
|
||||
*/
|
||||
const handleChangeConnectionType = async (id: number, type: ConnectionType) => {
|
||||
setChangingConnectionId(id);
|
||||
try {
|
||||
await connectionsApi.update(id, { type });
|
||||
addToast('Verbindung geändert.', 'success');
|
||||
onSaved();
|
||||
} catch (err) {
|
||||
addToast(err instanceof Error ? err.message : 'Ändern fehlgeschlagen.', 'error');
|
||||
} finally {
|
||||
setChangingConnectionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveConnection = async (id: number) => {
|
||||
try {
|
||||
await connectionsApi.remove(id);
|
||||
@@ -572,10 +592,31 @@ export const LeadModal: React.FC<LeadModalProps> = ({
|
||||
<Avatar src={other!.avatar} initials={leadInitials(other!)} size="sm" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-bold text-ink-900">{leadFullName(other!)}</p>
|
||||
<p className="truncate text-xs text-ink-400">
|
||||
{connection.type}
|
||||
{other!.company ? ` · ${other!.company}` : ''}
|
||||
</p>
|
||||
<div className="mt-0.5 flex items-center gap-2">
|
||||
{/*
|
||||
* Die Art ist hier direkt umstellbar - vorher musste man die
|
||||
* Verbindung loeschen und neu anlegen, nur um "Kollege" in
|
||||
* "Vorgesetzter" zu aendern.
|
||||
*/}
|
||||
<select
|
||||
value={connection.type}
|
||||
disabled={changingConnectionId === connection.id}
|
||||
onChange={(e) =>
|
||||
handleChangeConnectionType(connection.id, e.target.value as ConnectionType)
|
||||
}
|
||||
aria-label={`Art der Verbindung zu ${leadFullName(other!)}`}
|
||||
className="max-w-[10rem] rounded-lg border border-ink-200 bg-surface px-2 py-1 text-xs font-bold text-ink-600 transition hover:border-brand-300 focus:border-brand-400 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{CONNECTION_TYPES.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{other!.company && (
|
||||
<span className="truncate text-xs text-ink-400">{other!.company}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -95,6 +95,8 @@ export const LeadNetwork: React.FC<LeadNetworkProps> = ({
|
||||
const [linkMode, setLinkMode] = useState(false);
|
||||
const [linkSourceId, setLinkSourceId] = useState<number | null>(null);
|
||||
const [linkType, setLinkType] = useState<ConnectionType>('Netzwerk');
|
||||
/** Verbindung, deren Art gerade gespeichert wird - sperrt nur diese Zeile. */
|
||||
const [changingConnectionId, setChangingConnectionId] = useState<number | null>(null);
|
||||
|
||||
const leadById = useMemo(() => new Map(leads.map((lead) => [lead.id, lead])), [leads]);
|
||||
|
||||
@@ -505,6 +507,20 @@ export const LeadNetwork: React.FC<LeadNetworkProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
/** Art einer bestehenden Verbindung umstellen - wie im Kontaktdialog. */
|
||||
const changeConnectionType = async (connectionId: number, type: ConnectionType) => {
|
||||
setChangingConnectionId(connectionId);
|
||||
try {
|
||||
await connectionsApi.update(connectionId, { type });
|
||||
addToast('Verbindung geändert.', 'success');
|
||||
reload();
|
||||
} catch (err) {
|
||||
addToast(err instanceof Error ? err.message : 'Ändern fehlgeschlagen.', 'error');
|
||||
} finally {
|
||||
setChangingConnectionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const resetView = () => {
|
||||
setTransform({ x: 0, y: 0, k: 1 });
|
||||
};
|
||||
@@ -963,15 +979,28 @@ export const LeadNetwork: React.FC<LeadNetworkProps> = ({
|
||||
key={connection.id}
|
||||
className="flex items-center gap-2 rounded-xl bg-ink-50 px-3 py-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<button
|
||||
onClick={() => setSelectedId(nodeId('lead', other!.id))}
|
||||
className="min-w-0 flex-1 text-left"
|
||||
className="block w-full truncate text-left text-xs font-bold text-ink-800"
|
||||
>
|
||||
<p className="truncate text-xs font-bold text-ink-800">{leadFullName(other!)}</p>
|
||||
<p className="truncate text-[10px] font-semibold uppercase tracking-wider text-ink-400">
|
||||
{connection.type}
|
||||
</p>
|
||||
{leadFullName(other!)}
|
||||
</button>
|
||||
{/* Art direkt umstellbar - ohne den Umweg ueber Loeschen und neu Anlegen. */}
|
||||
<select
|
||||
value={connection.type}
|
||||
disabled={changingConnectionId === connection.id}
|
||||
onChange={(e) => changeConnectionType(connection.id, e.target.value as ConnectionType)}
|
||||
aria-label={`Art der Verbindung zu ${leadFullName(other!)}`}
|
||||
className="mt-1 w-full rounded-lg border border-ink-200 bg-surface px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-ink-500 transition hover:border-brand-300 focus:border-brand-400 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{CONNECTION_TYPES.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeConnection(connection.id)}
|
||||
className="shrink-0 rounded-lg p-1.5 text-ink-400 transition hover:bg-red-50 hover:text-red-600"
|
||||
|
||||
Reference in New Issue
Block a user