Files
votianng/frontend/src/CustomerOrderTexts.tsx
T
Sven 781a2b82dc Erweitere VoTiAn NG um Fachfunktionen und verbessere die Listenoberfläche
Ergänze Auftragsbearbeitung, Disposition, Stammdaten, Kunden- und
Transporteurportale, Lagerverwaltung, Abrechnung, Exporte und Statistik.
Füge Kontoeinstellungen, Mitteilungen, Automatikdienste und Betriebsstatus
sowie die zugehörigen Tests und PHP-Abgleichsdokumentation hinzu.

Vergrößere die persönlichen Auftragslisten auf einheitliche Höhen und
richte Statusanzeigen sowie die Ranking-Vorschau-Steuerelemente bündig aus.

Validierung: 44 Frontendtests und Produktionsbuild erfolgreich.
Backend funktional unverändert gegenüber dem vollständigen Lauf mit 1.624
erfolgreichen und drei übersprungenen Tests. Layout im Browser und lokale
Bereitstellung geprüft.
2026-10-20 11:19:43 +02:00

24 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {useUiText} from './UiLanguage';
import {useEffect,useRef,useState} from 'react';
import {Field,Modal,useDiscardConfirmation} from './FormElements';
import {ApiError,post,request} from './api';
type Stop={id:number;sequence:number;address:string;company:string;person:string;phone:string;reference:string;remark:string};
type Options={id:number;revision:string;canEdit:boolean;reason:string;remarks:boolean;referenceRequired:boolean;reference:string;notes:string;stops:Stop[]};
type Command={requestId:string;revision:string;reference:string;notes:string;stops:Stop[];confirmed:boolean};
type Preview={changes:{field:string;before:string;after:string}[]};
export function CustomerOrderTexts({job,onClose,onSaved}:{job:number;onClose:()=>void;onSaved:()=>void}){
const uiText=useUiText();
const [data,setData]=useState<Options|null>(null),[dirty,setDirty]=useState(false),[preview,setPreview]=useState<Preview|null>(null),[confirmed,setConfirmed]=useState(false),[busy,setBusy]=useState(false),[uncertain,setUncertain]=useState(false),[stale,setStale]=useState(false),[error,setError]=useState(''),[done,setDone]=useState(false);
const pending=useRef<Command|null>(null),sending=useRef(false),base=`/customer-portal/orders/${job}/texts`,locked=busy||uncertain||stale||done;
const [discard,dialog]=useDiscardConfirmation();
useEffect(()=>{const c=new AbortController();request<Options>(base,{signal:c.signal}).then(d=>{if(!c.signal.aborted)setData(d);}).catch(e=>{if(!c.signal.aborted)setError(e.message);});return()=>c.abort();},[base]);
useEffect(()=>{if(!dirty&&!uncertain)return;const prevent=(e:BeforeUnloadEvent)=>{e.preventDefault();e.returnValue='';};window.addEventListener('beforeunload',prevent);return()=>window.removeEventListener('beforeunload',prevent);},[dirty,uncertain]);
function change(patch:Partial<Options>){if(locked)return;setData(d=>d?{...d,...patch}:d);setDirty(true);setPreview(null);setConfirmed(false);pending.current=null;setError('');}
async function reload(){if(busy||uncertain||dirty&&!await discard('Die bisherigen Änderungen verwerfen und den aktuellen Auftrag laden?'))return;setBusy(true);setError('');try{setData(await request<Options>(base));setDirty(false);setPreview(null);setConfirmed(false);setStale(false);pending.current=null;}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
async function check(){if(!data||locked)return;setBusy(true);setError('');const command={requestId:crypto.randomUUID(),revision:data.revision,reference:data.reference,notes:data.notes,stops:data.stops,confirmed:true};try{setPreview(await post<Preview>(base+'/preview',command));pending.current=command;}catch(e){setError((e as Error).message);setStale(e instanceof ApiError&&e.status===409);}finally{setBusy(false);}}
async function save(){if(sending.current||!pending.current||!confirmed)return;sending.current=true;setBusy(true);setError('');try{await post(base,pending.current);setDone(true);setDirty(false);setUncertain(false);onSaved();}catch(e){setError((e as Error).message);const unknown=!(e instanceof ApiError)||e.status===0||e.status===408||e.status>=500;setUncertain(unknown);if(!unknown){setPreview(null);setConfirmed(false);pending.current=null;setStale(e instanceof ApiError&&e.status===409);}}finally{sending.current=false;setBusy(false);}}
async function close(){if(busy||uncertain)return;if(!dirty||await discard('Ungespeicherte Änderungen am Kundenauftrag verwerfen?'))onClose();}
return <><Modal title={`Auftrag #${job}: Texte und Kontakte`} onClose={()=>void close()} wide><div className="modal-body">{error&&<p role="alert" className="alert error">{error}</p>}{!data&&!error&&<p>Auftrag wird geladen …</p>}{done?<p role="status" className="alert success">Die Änderungen an Auftrag #{job} wurden gespeichert.</p>:data&&<>{!data.canEdit?<p className="alert info">{data.reason}</p>:<><p>Kommissionsnummern, Auftragstext, Namen und Stationskontakte korrigieren. Die Rechnungsdarstellung wird zur Aktualisierung vorgemerkt.</p><form onSubmit={e=>{e.preventDefault();void check();}}><fieldset disabled={locked}><Field label={'Kommissionsnummer'+(data.referenceRequired?' *':'')}><input value={data.reference} required={data.referenceRequired} maxLength={50} onChange={e=>change({reference:e.target.value})}/></Field><Field label="Auftragstext"><textarea value={data.notes} maxLength={10000} onChange={e=>change({notes:e.target.value})}/></Field>{data.stops.map((s,i)=><section className="panel" key={s.id} style={{padding:16,marginTop:16}}><h3>Station {s.sequence}</h3><p>{s.address}</p><div className="customer-entry-grid">{([['company','Firma / Name',50],['person','Kontakt',50],['phone','Telefon',20],['reference','Kommissionsnummer',30]] as const).map(([key,label,max])=><Field key={key} label={`Station ${s.sequence}: ${label}`}><input value={s[key]} required={key==='company'} maxLength={max} onChange={e=>change({stops:data.stops.map((v,n)=>n===i?{...v,[key]:e.target.value}:v)})}/></Field>)}{data.remarks&&<Field label={`Station ${s.sequence}: Bemerkung`}><textarea value={s.remark} maxLength={10000} onChange={e=>change({stops:data.stops.map((v,n)=>n===i?{...v,remark:e.target.value}:v)})}/></Field>}</div></section>)}<p><button className="button" disabled={!dirty}>Änderungen prüfen</button></p></fieldset></form>{preview&&<section><h3>Änderungen prüfen</h3><div className="admin-table-wrap"><table className="admin-table"><thead><tr><th>Feld</th><th>Bisher</th><th>Neu</th></tr></thead><tbody>{preview.changes.map(c=><tr key={c.field}><th>{c.field}</th><td style={{whiteSpace:'pre-wrap'}}>{c.before||'–'}</td><td style={{whiteSpace:'pre-wrap'}}>{c.after||'–'}</td></tr>)}</tbody></table></div><label className="admin-check"><input type="checkbox" checked={confirmed} disabled={busy||uncertain||stale} onChange={e=>setConfirmed(e.target.checked)}/>Änderungen geprüft und bestätigt</label></section>}</>}</>}{uncertain&&<p className="alert info">Die Speicherbestätigung fehlt. Bitte dieselbe Anfrage erneut abrufen.</p>}{stale&&<p className="alert info">Der Auftrag wurde inzwischen geändert. Bitte den aktuellen Stand laden und erneut prüfen.</p>}</div><div className="modal-footer"><button className="button" disabled={busy||uncertain} onClick={()=>void close()}>{uiText("Schließen")}</button>{!done&&<button className="button" disabled={busy||uncertain} onClick={()=>void reload()}>Aktuellen Stand laden</button>}{preview&&!done&&<button className="button primary" disabled={busy||!confirmed||stale} onClick={()=>void save()}>{uncertain?'Ergebnis erneut abrufen':uiText("Änderungen speichern")}</button>}</div></Modal>{dialog}</>;
}