import {useUiText} from './UiLanguage'; import {useEffect,useRef,useState} from 'react'; import {Field,Modal,useDiscardConfirmation} from './FormElements'; import {request,post} from './api'; import {ErrorMessage,uncertainWrite} from './BusinessForms'; type Kind='customers'|'couriers'; type Row={customer:number;courier:number;customerLabel:string;courierLabel:string;relation:number;position:number;commission:string;description:string;revision:string}; type Data={rows:Row[];revision:string;canEditCommission:boolean;operations:{token:string;canResume:boolean;description:string}[]}; type Target=Pick; type Command={requestId:string;action:string;targets:Target[];relation:number;description:string;commission:string|null;position:number;planRevision:string}; type Draft={action:string;targets:Target[];relation:number;description:string;commission:string;originalCommission:string;position:number;maxPosition:number;label:string}; type Preview={descriptions:string[];planRevision:string;commissionChanges:number}; type Choice={id:number;label:string}; const base='/master/relations'; const key=(r:Target)=>`${r.customer}-${r.courier}`; const rate=(r:string)=>Number(r).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); const target=(r:Row):Target=>({customer:r.customer,courier:r.courier,revision:r.revision}); const blank=(action:string):Draft=>({action,targets:[],relation:0,description:'',commission:'0',originalCommission:'0',position:1,maxPosition:1,label:''}); export function CustomerCourierRelations({kind,id,onClose}:{kind:Kind;id:number;onClose:()=>void}){ const uiText=useUiText("admin/customer_courier.php"); const [data,setData]=useState(null),[error,setError]=useState(''),[notice,setNotice]=useState(''),[busy,setBusy]=useState(false),[uncertain,setUncertain]=useState(false); const [query,setQuery]=useState(''),[filter,setFilter]=useState('all'),[page,setPage]=useState(0),[selected,setSelected]=useState([]); const [draft,setDraft]=useState(null),[preview,setPreview]=useState(null),[search,setSearch]=useState(''),[choices,setChoices]=useState([]),[more,setMore]=useState(false),[searched,setSearched]=useState(false); const [history,setHistory]=useState<{label:string;rows:{eid:string;value:string;at:string}[]}|null>(null); const pending=useRef(null),original=useRef('');const [discard,discardDialog]=useDiscardConfirmation(); async function load(){const d=await request(`${base}/${kind}/${id}`);setData(d);setSelected([]);setPage(0);} useEffect(()=>{setBusy(true);void load().catch(e=>setError(e.message)).finally(()=>setBusy(false));},[kind,id]); const dirty=!!draft&&(JSON.stringify(draft)!==original.current||!!preview); async function closeEditor(){if(busy||uncertain)return;if(!dirty||await discard('Ungespeicherte Kunden–Transporteur-Beziehung verwerfen?')){setDraft(null);setPreview(null);pending.current=null;setError('');}} function open(d:Draft){original.current=JSON.stringify(d);setDraft(d);setPreview(null);pending.current=null;setSearch('');setChoices([]);setSearched(false);setMore(false);setError('');} function change(p:Partial){setDraft(d=>d?{...d,...p}:d);setPreview(null);pending.current=null;setError('');} async function edit(action:string,row:Row){setBusy(true);setError('');try{const context=await request(`${base}/customers/${row.customer}`);const current=context.rows.find(r=>r.courier===row.courier);if(!current)throw new Error('Diese Beziehung besteht nicht mehr. Bitte neu laden.');open({...blank(action),targets:[target(current)],relation:current.relation,description:current.description,commission:current.commission,originalCommission:current.commission,position:current.position,maxPosition:Math.max(1,...context.rows.filter(r=>r.relation===1).map(r=>r.position)),label:`${current.customerLabel} ↔ ${current.courierLabel}`});}catch(e){setError((e as Error).message);}finally{setBusy(false);}} function remove(rows:Row[]){open({...blank('REMOVE'),targets:rows.map(target),label:rows.map(r=>`${r.customerLabel} ↔ ${r.courierLabel}`).join('\n')});} async function find(){setBusy(true);setError('');try{const r=await request<{items:Choice[];more:boolean}>(`${base}/search?kind=${kind==='customers'?'couriers':'customers'}&q=${encodeURIComponent(search)}`);setChoices(r.items);setMore(r.more);setSearched(true);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function choose(c:Choice){setBusy(true);setError('');try{const customer=kind==='customers'?id:c.id,courier=kind==='couriers'?id:c.id;const context=await request(`${base}/customers/${customer}`);if(context.rows.some(r=>r.courier===courier))throw new Error('Zwischen diesem Kunden und Transporteur besteht bereits eine Beziehung. Bitte den vorhandenen Eintrag bearbeiten.');change({targets:[{customer,courier,revision:context.revision}],label:c.label});setChoices([]);setSearched(false);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function prepare(){if(!draft||!draft.targets.length)return;setBusy(true);setError('');try{const command:Command={requestId:crypto.randomUUID(),action:draft.action,targets:draft.targets,relation:draft.relation,description:draft.description,commission:data?.canEditCommission&&draft.relation===1&&draft.commission!==draft.originalCommission?draft.commission:null,position:draft.position,planRevision:''};const p=await post(`${base}/preview`,command);pending.current={...command,planRevision:p.planRevision};setPreview(p);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function save(token?:string){if(!token&&!pending.current)return;setBusy(true);setError('');try{const r=await post<{state:string;message?:string}>(token?`${base}/operations/${token}`:base,token?undefined:pending.current);pending.current=null;setUncertain(false);setDraft(null);setPreview(null);if(r.state==='DONE')setNotice('Kunden–Transporteur-Beziehungen gespeichert.');else{setNotice('');setError(r.message||'Der gesicherte Vorgang ist noch offen.');}await load();}catch(e){setUncertain(!!pending.current&&uncertainWrite(e));setError((e as Error).message);}finally{setBusy(false);}} async function showHistory(row:Row){setBusy(true);setError('');try{const rows=await request<{eid:string;value:string;at:string}[]>(`${base}/history?customer=${row.customer}&courier=${row.courier}`);setHistory({label:`${row.customerLabel} ↔ ${row.courierLabel}`,rows});}catch(e){setError((e as Error).message);}finally{setBusy(false);}} const blocked=busy||uncertain||!!data?.operations.length; const filtered=data?.rows.filter(r=>(filter==='all'||String(r.relation)===filter)&&`${r.customerLabel} ${r.courierLabel} ${r.description}`.toLocaleLowerCase('de').includes(query.toLocaleLowerCase('de')))??[]; const shown=filtered.slice(page*100,page*100+100); const titles:Record={ADD:'Beziehung anlegen',UPDATE:'Beziehung bearbeiten',REMOVE:'Beziehungen entfernen',MOVE:'Favoritenrang ändern'}; return <>{if(!draft&&!history&&!busy&&!uncertain)onClose();}}>
{!draft&&} {notice&&

{notice}

}

Kunden und Transporteure als „Gesperrt“ oder „Besonders erwünscht“ verknüpfen. Die Favoritenreihenfolge gilt je Kunde.

{data?.operations.map(o=>

Ein gesicherter Beziehungsvorgang ist noch offen. {o.description}

{!o.canResume&&

Der ursprüngliche Bearbeiter muss den Vorgang fortsetzen.

}
)}
{setQuery(e.target.value);setPage(0);}}/>
{filtered.length} Beziehungen
{!data&&

Beziehungen werden geladen …

}{data&&
{shown.map(r=>)}
selected.includes(key(r)))} onChange={e=>setSelected(s=>e.target.checked?[...new Set([...s,...shown.map(key)])]:s.filter(k=>!shown.some(r=>key(r)===k)))}/>{kind==='customers'?'Transporteur':uiText("Kunde")}Status / Rang{uiText("Provision")}{uiText("Bemerkung")}Aktionen
setSelected(s=>e.target.checked?[...s,key(r)]:s.filter(k=>k!==key(r)))}/>{kind==='customers'?r.courierLabel:r.customerLabel}{r.relation===1?`Besonders erwünscht · Rang ${r.position}`:r.relation===0?'Gesperrt':`Bestandsstatus ${r.relation}`}{rate(r.commission)} %{r.description||'–'}
{r.relation===1&&}
} {data&&!filtered.length&&

Keine Beziehungen gefunden.

}{filtered.length>100&&
Seite {page+1} {uiText(" von ")}{Math.ceil(filtered.length/100)}
}
{draft&&void closeEditor()}>
{draft.action==='ADD'&&<>
{setSearch(e.target.value);setChoices([]);setSearched(false);}} onKeyDown={e=>{if(e.key==='Enter'){e.preventDefault();void find();}}}/>
{choices.map(c=>)}
{more&&

Mehr als 50 Treffer. Bitte die Suche eingrenzen.

}{searched&&!choices.length&&

Keine passenden Stammdaten gefunden.

}} {draft.label&&

{draft.label}

} {['ADD','UPDATE'].includes(draft.action)&&<>
change({commission:e.target.value})}/>
{!data?.canEditCommission&&

Provisionen können mit dem zusätzlichen Provisionsrecht geändert werden. Der bestehende Wert bleibt erhalten.

}