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:
Sascha Wolf
2026-09-10 00:35:42 +02:00
co-authored by Claude Opus 5
parent 46983a7e2c
commit f678e988ac
5 changed files with 100 additions and 15 deletions
+45 -4
View File
@@ -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"
+38 -9
View File
@@ -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"
>
<button
onClick={() => setSelectedId(nodeId('lead', other!.id))}
className="min-w-0 flex-1 text-left"
>
<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>
</button>
<div className="min-w-0 flex-1">
<button
onClick={() => setSelectedId(nodeId('lead', other!.id))}
className="block w-full truncate text-left text-xs font-bold text-ink-800"
>
{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"