Referrals: 4 neue Beziehungen + Bearbeiten-Stift pro Eintrag
Beziehungs-Dropdown erweitert um Schwiegertochter/Schwiegersohn, Schwiegermutter/Schwiegervater, Oma/Opa, Uroma/Uropa (Whitelist front- und backend synchron). Bearbeiten-Stift pro Zeile (vor der Muelltonne): Beziehung und/oder Gegen-Kunde aenderbar. Neuer PUT /:customerId/referrals/:referralId mit Whitelist-Pruefung, Doppel-Werber-409 (Self-Ausschluss), Portal-Block und UPDATE-Auditeintrag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Plus, Search, Trash2, ExternalLink, X, Check } from 'lucide-react';
|
||||
import { Plus, Search, Trash2, ExternalLink, X, Check, Pencil } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { referralApi } from '../../services/api';
|
||||
import { REFERRAL_RELATIONSHIPS } from '../../types';
|
||||
@@ -177,24 +177,99 @@ function DraftRow({
|
||||
);
|
||||
}
|
||||
|
||||
// Eine gespeicherte Zeile: Kunde (Link) + Beziehung + löschen + Externtab.
|
||||
// Eine gespeicherte Zeile: Kunde (Link) + Beziehung + bearbeiten/löschen + Externtab.
|
||||
function SavedRow({
|
||||
customerId,
|
||||
entry,
|
||||
direction,
|
||||
canEdit,
|
||||
onDeleted,
|
||||
onChanged,
|
||||
}: {
|
||||
customerId: number;
|
||||
entry: ReferralEntry;
|
||||
direction: 'recruitedBy' | 'recruited';
|
||||
canEdit: boolean;
|
||||
onDeleted: () => void;
|
||||
onChanged: () => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
// Im Bearbeiten-Modus veränderbar: Gegen-Kunde und Beziehung.
|
||||
const [selected, setSelected] = useState<CustomerSummary>(entry.customer);
|
||||
const [relationship, setRelationship] = useState(entry.relationship);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
|
||||
const startEdit = () => {
|
||||
setSelected(entry.customer);
|
||||
setRelationship(entry.relationship);
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: () => referralApi.remove(customerId, entry.id),
|
||||
onSuccess: () => onDeleted(),
|
||||
onSuccess: () => onChanged(),
|
||||
onError: (err: Error) => toast.error(err.message || 'Löschen fehlgeschlagen'),
|
||||
});
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
referralApi.update(customerId, entry.id, {
|
||||
direction,
|
||||
otherCustomerId: selected.id,
|
||||
relationship,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setEditing(false);
|
||||
onChanged();
|
||||
},
|
||||
onError: (err: Error) => toast.error(err.message || 'Speichern fehlgeschlagen'),
|
||||
});
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-2 border-b bg-amber-50/50 px-2 rounded">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{customerLabel(selected)}</span>
|
||||
<button
|
||||
onClick={() => setShowSearch(true)}
|
||||
className="text-gray-400 hover:text-blue-600 p-1 rounded"
|
||||
title="Anderen Kunden wählen"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<Select
|
||||
value={relationship}
|
||||
onChange={(e) => setRelationship(e.target.value)}
|
||||
options={REFERRAL_RELATIONSHIPS.map((r) => ({ value: r, label: r }))}
|
||||
placeholder="Bitte auswählen!"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={!relationship || saveMutation.isPending}
|
||||
className="text-green-600 hover:text-green-700 disabled:text-gray-300 p-1 rounded"
|
||||
title="Speichern"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditing(false)}
|
||||
className="text-gray-400 hover:text-red-600 p-1 rounded"
|
||||
title="Abbrechen"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<CustomerSearchModal
|
||||
customerId={customerId}
|
||||
isOpen={showSearch}
|
||||
onClose={() => setShowSearch(false)}
|
||||
onSelect={(c) => setSelected(c)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-2 border-b px-2">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
@@ -218,14 +293,23 @@ function SavedRow({
|
||||
</div>
|
||||
<div className="w-56 text-sm">{entry.relationship}</div>
|
||||
{canEdit ? (
|
||||
<button
|
||||
onClick={() => deleteMutation.mutate()}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="text-gray-400 hover:text-red-600 p-1 rounded"
|
||||
title="Eintrag entfernen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={startEdit}
|
||||
className="text-gray-400 hover:text-blue-600 p-1 rounded"
|
||||
title="Eintrag bearbeiten"
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteMutation.mutate()}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="text-gray-400 hover:text-red-600 p-1 rounded"
|
||||
title="Eintrag entfernen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="w-6" />
|
||||
)}
|
||||
@@ -271,7 +355,7 @@ export default function ReferralsTab({ customerId, customerName, canEdit }: Refe
|
||||
</h3>
|
||||
{recruitedBy || addingRecruitedBy ? columnHeader : null}
|
||||
{recruitedBy ? (
|
||||
<SavedRow customerId={customerId} entry={recruitedBy} canEdit={canEdit} onDeleted={refresh} />
|
||||
<SavedRow customerId={customerId} entry={recruitedBy} direction="recruitedBy" canEdit={canEdit} onChanged={refresh} />
|
||||
) : addingRecruitedBy ? (
|
||||
<DraftRow
|
||||
customerId={customerId}
|
||||
@@ -303,7 +387,7 @@ export default function ReferralsTab({ customerId, customerName, canEdit }: Refe
|
||||
</h3>
|
||||
{recruited.length > 0 || addingRecruited ? columnHeader : null}
|
||||
{recruited.map((entry) => (
|
||||
<SavedRow key={entry.id} customerId={customerId} entry={entry} canEdit={canEdit} onDeleted={refresh} />
|
||||
<SavedRow key={entry.id} customerId={customerId} entry={entry} direction="recruited" canEdit={canEdit} onChanged={refresh} />
|
||||
))}
|
||||
{addingRecruited && (
|
||||
<DraftRow
|
||||
|
||||
@@ -249,6 +249,14 @@ export const referralApi = {
|
||||
const res = await api.post<ApiResponse<unknown>>(`/customers/${customerId}/referrals`, payload);
|
||||
return res.data;
|
||||
},
|
||||
update: async (
|
||||
customerId: number,
|
||||
referralId: number,
|
||||
payload: { direction: 'recruitedBy' | 'recruited'; otherCustomerId: number; relationship: string },
|
||||
) => {
|
||||
const res = await api.put<ApiResponse<unknown>>(`/customers/${customerId}/referrals/${referralId}`, payload);
|
||||
return res.data;
|
||||
},
|
||||
remove: async (customerId: number, referralId: number) => {
|
||||
const res = await api.delete<ApiResponse<void>>(`/customers/${customerId}/referrals/${referralId}`);
|
||||
return res.data;
|
||||
|
||||
@@ -36,6 +36,10 @@ export const REFERRAL_RELATIONSHIPS = [
|
||||
'Freund/Kumpel',
|
||||
'Eltern',
|
||||
'Bruder/Schwester',
|
||||
'Schwiegertochter/Schwiegersohn',
|
||||
'Schwiegermutter/Schwiegervater',
|
||||
'Oma/Opa',
|
||||
'Uroma/Uropa',
|
||||
'Bekannte',
|
||||
'sonstige',
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user