import { useEffect, useRef, useState, type FormEvent } from "react"; import { LoaderCircle, RefreshCw, Save, Settings2 } from "lucide-react"; import { ApiError, post, put, request } from "./api"; import { Field, Modal, useDiscardConfirmation } from "./FormElements"; type Kind="POSTAL"|"AREA"; type Item={id:number;name:string}; type Query={kind:Kind;customer:number;vehicle:number;rowStart:string;columnStart:string;rowOffset:number;columnOffset:number}; type Options={customers:boolean;vehicleEnabled:boolean;vehicles:{id:number;label:string}[]}; type Cell={from:number;to:number;price:string;discount:string;courier:string;courierDiscount:string;generalPrice:string;generalDiscount:string;generalCourier:string;generalCourierDiscount:string;ambiguous:boolean;generalAmbiguous:boolean}; type Grid={rows:Item[];columns:Item[];moreRows:boolean;moreColumns:boolean;cells:Cell[];revision:string;vehicle:number}; type Draft={from:number;to:number;value:string;courier:string}; type SaveRequest={requestId:string;query:Query;revision:string;discount:boolean;cells:Draft[]}; const base="/administration/zone-prices"; const key=(c:{from:number;to:number})=>`${c.from}:${c.to}`; const equal=(a:unknown,b:unknown)=>JSON.stringify(a)===JSON.stringify(b); const normalize=(s:string)=>{const v=s.trim().replace(",",".");return !v?"0.00":/^-?[0-9]{1,5}(\.[0-9]{1,2})?$/.test(v)?Number(v).toFixed(2):"invalid:"+s;}; const safeFailure=(e:unknown)=>e instanceof ApiError&&e.status>=400&&e.status<500; export function ZonePrices({kind,discount,onDirty}:{kind:Kind;discount:boolean;onDirty:(value:boolean)=>void}) { const initial:Query={kind,customer:0,vehicle:0,rowStart:"",columnStart:"",rowOffset:0,columnOffset:0}; const [query,setQuery]=useState(initial),[loaded,setLoaded]=useState(null),[options,setOptions]=useState(null),[grid,setGrid]=useState(null),[draft,setDraft]=useState>({}); const [busy,setBusy]=useState(false),[error,setError]=useState(""),[notice,setNotice]=useState(""),[pending,setPending]=useState(null),[manager,setManager]=useState(false); const [customers,setCustomers]=useState<{id:number;label:string}[]>([]),[customerSearch,setCustomerSearch]=useState(""),[searchBusy,setSearchBusy]=useState(false),[showCustomer,setShowCustomer]=useState(true),[showCourier,setShowCourier]=useState(false); const [confirmDiscard,discardDialog]=useDiscardConfirmation(),gate=useRef(false); const changed=grid?.cells.filter(c=>{const d=draft[key(c)];return d&&(normalize(d.value)!==normalize(discount?c.discount:c.price)||normalize(d.courier)!==normalize(discount?c.courierDiscount:c.courier));})||[]; const dirty=!!pending||changed.length>0; useEffect(()=>{onDirty(dirty||manager);return()=>onDirty(false);},[dirty,manager,onDirty]); function accept(g:Grid,q:Query){setGrid(g);setLoaded(q);setQuery(q);setDraft(Object.fromEntries(g.cells.map(c=>[key(c),{from:c.from,to:c.to,value:discount?c.discount:c.price,courier:discount?c.courierDiscount:c.courier}])));setPending(null);} useEffect(()=>{let active=true;setBusy(true);request(base).then(async o=>{if(!active)return;setOptions(o);const g=await post(base+"/query",initial);if(active)accept(g,initial);}).catch(e=>{if(active)setError(e.message);}).finally(()=>{if(active)setBusy(false);});return()=>{active=false;};},[]); async function load(q:Query=query,guard=true){if(gate.current)return;if(guard&&dirty&&!await confirmDiscard(pending?"Speicherstand unklar. Entwurf verwerfen und gespeicherte Tarife laden?":"Ungespeicherte Tarifänderungen verwerfen?"))return;gate.current=true;setBusy(true);setError("");try{accept(await post(base+"/query",q),q);setNotice("");}catch(e){setError((e as Error).message);}finally{gate.current=false;setBusy(false);}} async function save(){if(!grid||!loaded||gate.current)return;gate.current=true;const body=pending||{requestId:crypto.randomUUID(),query:loaded,revision:grid.revision,discount,cells:changed.map(c=>draft[key(c)])};setPending(body);setBusy(true);setError("");try{await put(base,body);setPending(null);setGrid(null);setDraft({});setNotice("Tarife und Historie gespeichert.");accept(await post(base+"/query",loaded),loaded);}catch(e){if(safeFailure(e))setPending(null);setError((e as Error).message);}finally{gate.current=false;setBusy(false);}} async function search(e:FormEvent){e.preventDefault();setSearchBusy(true);setError("");try{setCustomers(await request<{id:number;label:string}[]>("/administration/service-prices/customers?q="+encodeURIComponent(customerSearch)));}catch(e){setError((e as Error).message);}finally{setSearchBusy(false);}} const selected=equal(query,loaded),frozen=busy||!!pending||!selected,priceLabel=discount?"Rabatt (%)":"Preis",courierLabel=discount?"Frachtvergütungsrabatt (%)":"Frachtvergütung"; const indexed=new Map(grid?.cells.map(c=>[key(c),c])); function update(k:string,field:"value"|"courier",value:string){setDraft(d=>({...d,[k]:{...d[k],[field]:value}}));} return <>

{kind==="AREA"?(discount?"Bereichsrabatte":"Bereichspreise"):(discount?"PLZ-Rabatte":"PLZ-Preise")}

Von-/Nach-Tarife mit getrennten Kunden- und Kurierwerten.

{error&&
{error}
}{notice&&

{notice}

} {options?.customers&&
setCustomerSearch(e.target.value)} placeholder="Kundennummer oder Firma"/>
}
{e.preventDefault();void load();}}>
{options?.vehicleEnabled&&} setQuery(q=>({...q,rowStart:e.target.value,rowOffset:0}))} placeholder={kind==="AREA"?"Bereichsname":"PLZ"}/>setQuery(q=>({...q,columnStart:e.target.value,columnOffset:0}))} placeholder={kind==="AREA"?"Bereichsname":"PLZ"}/>
{grid&&loaded&&<>

{loaded.customer?`Kunde ${loaded.customer} · Allgemeine Vergleichswerte in Klammern`:"Allgemeine Tarife"}{grid.vehicle!==loaded.vehicle?` · Zugeordneter Fahrzeugtarif: ${options?.vehicles.find(v=>v.id===grid.vehicle)?.label||grid.vehicle}`:""}. Die Richtung VON → NACH wird separat gespeichert.

{!selected&&

Auswahl geändert. Bitte „Tabelle anzeigen“ wählen.

}
{grid.columns.map(c=>)}{grid.rows.map(r=>{grid.columns.map(col=>{const k=key({from:r.id,to:col.id}),c=indexed.get(k),d=draft[k];return ;})})}
VON ↓ / NACH →{c.name}
{r.name}{c&&d&&<>
{showCustomer&&}{showCourier&&}
{c.ambiguous&&Bestand mehrdeutig}}
{(!grid.rows.length||!grid.columns.length)&&

Für diesen Startwert sind keine Einträge vorhanden.

}
Zeilen {loaded.rowOffset+1}–{loaded.rowOffset+grid.rows.length}Spalten {loaded.columnOffset+1}–{loaded.columnOffset+grid.columns.length}
}{busy&&

Tarife werden verarbeitet …

}{pending&&!busy&&

Die Speicherung wurde nicht bestätigt. Bitte mit derselben Anfrage erneut prüfen oder den gespeicherten Stand laden.

} {manager&&{setManager(false);void load(query,false);}}/>}{discardDialog} ; } type Detail={item:Item;neighbors:(Item&{symmetric:boolean})[];postals:Item[];revision:string}; type Action="CREATE"|"RENAME"|"LINK"|"UNLINK"|"NEIGHBOR_ADD"|"NEIGHBOR_REMOVE"; type Change={requestId:string;kind:Kind;id:number;revision:string;action:Action;name:string;targets:number[]}; function ZoneManager({kind,onClose}:{kind:Kind;onClose:()=>void}) { const [items,setItems]=useState([]),[term,setTerm]=useState(""),[detail,setDetail]=useState(null),[name,setName]=useState(""),[newName,setNewName]=useState(""); const [targetTerm,setTargetTerm]=useState(""),[targets,setTargets]=useState([]),[target,setTarget]=useState(0),[targetKind,setTargetKind]=useState<"postal"|"neighbor">("neighbor"); const [selectedPostals,setSelectedPostals]=useState([]),[selectedNeighbors,setSelectedNeighbors]=useState([]),[error,setError]=useState(""),[notice,setNotice]=useState(""),[busy,setBusy]=useState(false); const [review,setReview]=useState<{body:Change;description:string}|null>(null),[pending,setPending]=useState(null),[confirmDiscard,discardDialog]=useDiscardConfirmation(),gate=useRef(false); const dirty=!!review||!!pending||!!newName||!!name&&name!==detail?.item.name||selectedPostals.length>0||selectedNeighbors.length>0; const itemUrl=(k:Kind,q:string)=>base+`/items?kind=${k}&q=${encodeURIComponent(q)}`; useEffect(()=>{let active=true;request(itemUrl(kind,"")).then(d=>{if(active)setItems(d);}).catch(e=>{if(active)setError(e.message);});return()=>{active=false;};},[]); async function close(){if(busy)return;if(!dirty||await confirmDiscard("Ungespeicherte Bereichs- oder Nachbarschaftsänderungen verwerfen?"))onClose();} async function search(e:FormEvent){e.preventDefault();setBusy(true);setError("");try{setItems(await request(itemUrl(kind,term)));}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function select(id:number){if(busy)return;if(dirty&&!await confirmDiscard("Ungespeicherte Bereichs- oder Nachbarschaftsänderungen verwerfen?"))return;setBusy(true);setError("");try{const d=await request(base+`/details?kind=${kind}&id=${id}`);setDetail(d);setName(d.item.name);setNewName("");setSelectedPostals([]);setSelectedNeighbors([]);setReview(null);setPending(null);setTargets([]);setTarget(0);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function findTarget(e:FormEvent){e.preventDefault();setBusy(true);setError("");try{let t=targetTerm.trim();if(targetKind==="postal"&&/^\d{1,4}$/.test(t))t=t.padStart(5,"0");setTargets(await request(itemUrl(targetKind==="postal"?"POSTAL":kind,t)));setTarget(0);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} function prepare(action:Action,description:string,ids:number[]=[],value=""){setReview({body:{requestId:crypto.randomUUID(),kind,id:detail?.item.id||0,revision:detail?.revision||"",action,name:value,targets:ids},description});setError("");setNotice("");} async function save(){if(!review||gate.current)return;gate.current=true;setBusy(true);setError("");const body=pending||review.body;setPending(body);try{await put(base+"/management",body);setPending(null);setReview(null);setNewName("");setSelectedNeighbors([]);setSelectedPostals([]);setNotice("Änderung gespeichert.");setItems(await request(itemUrl(kind,term)));if(detail){const d=await request(base+`/details?kind=${kind}&id=${detail.item.id}`);setDetail(d);setName(d.item.name);}}catch(e){if(safeFailure(e))setPending(null);setError((e as Error).message);}finally{gate.current=false;setBusy(false);}} const toggle=(ids:number[],id:number)=>ids.includes(id)?ids.filter(v=>v!==id):[...ids,id]; return void close()} wide>
{error&&
{error}
}{notice&&

{notice}

} {review?<>

{review.description}

{(review.body.action==="NEIGHBOR_ADD"||review.body.action==="NEIGHBOR_REMOVE")&&

Die Nachbarschaft wird in beiden Richtungen geändert. Weitere Nachbarschaften bleiben unverändert.

}
{error&&}:
setTerm(e.target.value)} placeholder="Anfang des Namens oder der PLZ"/>
{kind==="AREA"&&
{e.preventDefault();prepare("CREATE",`Bereich „${newName}“ anlegen?`,[],newName);}}>setNewName(e.target.value)}/>
} {detail&&<> {kind==="AREA"&&<>
{e.preventDefault();prepare("RENAME",`Bereich „${detail.item.name}“ in „${name}“ umbenennen?`,[],name);}}>setName(e.target.value)}/>

Zugehörige PLZ

{detail.postals.map((p,i)=>)}{!detail.postals.length&&

Keine PLZ zugeordnet.

}
}

Nachbarschaften

{detail.neighbors.map(n=>)}{!detail.neighbors.length&&

Keine Nachbarn verknüpft.

}

Verknüpfung hinzufügen

{kind==="AREA"&&}setTargetTerm(e.target.value)}/>
}
}{busy&&

Wird verarbeitet …

}{discardDialog}
; }