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.
This commit is contained in:
1 parent
363bc27c26
commit
781a2b82dc
1165 files changed
+74986
-1028
No files matched your search
@@ -0,0 +1,23 @@
|
||||
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}</>;
|
||||
}
|
||||
Reference in new issue
Block a user