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,27 @@
|
||||
import {useUiText} from './UiLanguage';
|
||||
import {useEffect,useRef,useState} from 'react';
|
||||
import {request,downloadBusinessFile} from './api';
|
||||
import {Modal} from './FormElements';
|
||||
type Field={label:string;value:string};
|
||||
type ArticleTable={columns:{label:string;type:string}[];rows:{id:number;cells:string[]}[]};
|
||||
type Picture={key:string;name:string};
|
||||
type Evidence={pricePending?:boolean;summary:Field[];services:{id:number;center:string;name:string;price:number|null}[];serviceTotal:number|null;toll?:number|null;totalWithToll?:number|null;serviceBasis:string;partial:boolean;stations:{label?:string;articles?:ArticleTable;id:number;sequence:number;fields:Field[];signature:string;photos:Picture[];warnings:string[]}[];warnings:string[];canPdf:boolean;revision:string};
|
||||
type Image={name:string;mime:string;data:string};
|
||||
const money=(n:number|null|undefined)=>n==null?"–":n.toLocaleString('de-DE',{style:'currency',currency:'EUR'});
|
||||
function Fields({values}:{values:Field[]}){return <dl className="customer-order-fields">{values.map((f,i)=><div key={i}><dt>{f.label}</dt><dd style={{whiteSpace:'pre-wrap'}}>{f.value}</dd></div>)}</dl>;}
|
||||
export function CustomerOrderEvidence({job}:{job:number}){
|
||||
const uiText=useUiText();
|
||||
|
||||
const base=`/customer-portal/orders/${job}`,[data,setData]=useState<Evidence|null>(null),[error,setError]=useState(''),[image,setImage]=useState<Image|null>(null),[imageError,setImageError]=useState(''),[busy,setBusy]=useState(false),[pdf,setPdf]=useState(false);const loadingImage=useRef<AbortController|null>(null);
|
||||
useEffect(()=>{const c=new AbortController();setData(null);setError('');request<Evidence>(base+'/evidence',{signal:c.signal}).then(d=>{if(!c.signal.aborted)setData(d);}).catch(e=>{if(!c.signal.aborted)setError(e.message);});return()=>{c.abort();loadingImage.current?.abort();};},[base]);
|
||||
async function photo(station:number,p:Picture){if(busy)return;const c=new AbortController();loadingImage.current=c;setBusy(true);setImage(null);setImageError('');try{const i=await request<Image>(`${base}/stations/${station}/photos/${p.key}`,{signal:c.signal});if(!c.signal.aborted)setImage(i);}catch(e){if(!c.signal.aborted)setImageError((e as Error).message);}finally{if(!c.signal.aborted)setBusy(false);}}
|
||||
return <section className="customer-order-evidence"><h3>Auftragsdaten und Abliefernachweise</h3>{error&&<p className="alert error" role="alert">{error}</p>}{!data&&!error&&<p>Nachweise werden geladen …</p>}{data&&<><Fields values={data.summary}/>{data.warnings.map((w,i)=><p className="alert info" key={i}>{w}</p>)}<h4>Leistungen · {data.serviceBasis}</h4>{data.services.length?<><div className="admin-table-wrap"><table className="admin-table"><thead><tr><th>{uiText("Kostenstelle")}</th><th>{uiText("Leistung")}</th><th>{uiText("Betrag")}</th></tr></thead><tbody>{data.services.map(s=><tr key={s.id}><td>{s.center}</td><td>{s.name}</td><td>{data.pricePending?'Preis in Prüfung':money(s.price)}</td></tr>)}</tbody></table></div><p><strong>Leistungssumme: {data.pricePending?'Preis in Prüfung':money(data.serviceTotal)}</strong>{data.partial?' · freigegebene Kostenstellen':''}</p></>:<p>Keine Leistungen für Ihre freigegebenen Kostenstellen vorhanden.</p>}{data.toll!=null&&<p>Maut: {money(data.toll)} · <strong>Gesamt einschließlich Maut: {data.pricePending?'Preis in Prüfung':money(data.totalWithToll??data.serviceTotal)}</strong>{data.partial?' · freigegebene Kostenstellen':''}</p>}{data.stations.map(s=><section key={s.id} style={{marginTop:20}}><h4>Nachweise · {s.label??`Station ${s.sequence}`}</h4><Fields values={s.fields}/>{s.articles&&s.articles.rows.length>0&&<div className="admin-table-wrap"><table className="admin-table"><caption>Artikel · {s.label??`Station ${s.sequence}`}</caption><thead><tr>{s.articles.columns.map((c,i)=><th key={i}>{c.label}</th>)}</tr></thead><tbody>{s.articles.rows.map(r=><tr key={r.id}>{r.cells.map((v,i)=><td key={i}>{s.articles!.columns[i].type==='money'&&v!==''?money(Number(v)):v}</td>)}</tr>)}</tbody></table></div>}{s.warnings.map((w,i)=><p key={i} className="alert info">{w}</p>)}{s.signature&&<figure style={{margin:'12px 0'}}><figcaption>Unterschrift · {s.label??`Station ${s.sequence}`}</figcaption><img alt={`Unterschrift · ${s.label??`Station ${s.sequence}`}`} src={s.signature} style={{maxWidth:'100%',maxHeight:130,objectFit:'contain'}}/></figure>}<div className="admin-toolbar">{s.photos.map(p=><button className="button" key={p.key} disabled={busy} onClick={()=>void photo(s.id,p)}>{s.label??`Station ${s.sequence}`}: {p.name}</button>)}</div>{!s.signature&&!s.fields.length&&!s.photos.length&&!s.warnings.length&&<p>Keine zusätzlichen Nachweise gespeichert.</p>}</section>)}{busy&&<p>Stationsfoto wird geladen …</p>}{imageError&&<p className="alert error" role="alert">{imageError}</p>}{image&&<figure style={{margin:0}}><img alt={image.name} src={image.data} style={{maxWidth:'100%',maxHeight:420,objectFit:'contain'}}/><figcaption><a className="button" href={image.data} download={image.name}>Stationsfoto herunterladen</a> <button className="button" onClick={()=>setImage(null)}>Stationsfoto schließen</button></figcaption></figure>}{data.canPdf&&<div className="admin-toolbar" style={{marginTop:20}}><button className="button" onClick={()=>setPdf(true)}>Auftragsbeleg als PDF</button></div>}{pdf&&<CustomerOrderPdf job={job} revision={data.revision} onClose={()=>setPdf(false)}/>}</>}</section>;
|
||||
}
|
||||
function CustomerOrderPdf({job,revision,onClose}:{job:number;revision:string;onClose:()=>void}){
|
||||
const uiText=useUiText();
|
||||
|
||||
const [page,setPage]=useState(0),[preview,setPreview]=useState<{page:number;pages:number;png:string}|null>(null),[error,setError]=useState(''),[busy,setBusy]=useState(false),[loading,setLoading]=useState(false);const base=`/customer-portal/orders/${job}`;
|
||||
useEffect(()=>{const c=new AbortController();setPreview(null);setError('');setLoading(true);request<{page:number;pages:number;png:string}>(`${base}/pdf-page?`+new URLSearchParams({revision,page:String(page)}),{signal:c.signal}).then(p=>{if(!c.signal.aborted)setPreview(p);}).catch(e=>{if(!c.signal.aborted)setError(e.message);}).finally(()=>{if(!c.signal.aborted)setLoading(false);});return()=>c.abort();},[base,revision,page]);
|
||||
async function download(){if(busy||!preview)return;setBusy(true);setError('');try{await downloadBusinessFile(`${base}/pdf?`+new URLSearchParams({revision}),undefined,`Auftrag-${job}.pdf`,'GET');}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
|
||||
return <Modal title={`Auftragsbeleg #${job}`} wide onClose={()=>{if(!busy)onClose();}}><div className="modal-body">{error&&<p className="alert error" role="alert">{error}</p>}{loading&&<p>PDF-Vorschau wird erstellt …</p>}{preview&&<><p>Seite {preview.page+1} / {preview.pages}</p><img alt={`Auftragsbeleg Seite ${preview.page+1}`} src={'data:image/png;base64,'+preview.png} style={{display:'block',width:'100%',height:'auto'}}/></>}</div><div className="modal-footer"><button className="button" disabled={busy} onClick={onClose}>{uiText("Schließen")}</button><button className="button" disabled={loading||busy||page===0||!preview} onClick={()=>setPage(p=>p-1)}>Vorherige Seite</button><button className="button" disabled={loading||busy||!preview||page+1>=preview.pages} onClick={()=>setPage(p=>p+1)}>Nächste Seite</button><button className="button primary" disabled={!preview||busy||loading} onClick={()=>void download()}>PDF herunterladen</button></div></Modal>;
|
||||
}
|
||||
Reference in new issue
Block a user