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:
Sven committed 2026-10-20 11:19:43 +02:00
1 parent 363bc27c26
commit 781a2b82dc
1165 files changed
+74986 -1028

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
import {useUiText} from './UiLanguage';
import {useEffect,useState} from 'react';
import {request} from './api';
import {Modal} from './FormElements';
import type {Order} from './types';
type RunningOrder={id:number;at:string;status:number;longhaul:boolean;headquarters:string;canOpen:boolean;occupied:boolean;stations:{id:number;position:number;postalCode:string;completed:boolean}[]};
type Result={sid:string;total:number;page:number;pageSize:number;items:RunningOrder[]};
const time=(v:string|null|undefined)=>{if(!v)return '–';const date=new Date(v.replace(' ','T'));return Number.isNaN(date.getTime())?v:date.toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'});};
export function AssignmentOrders({job,vehicle,courier,sid,refresh}:{job:number;vehicle:number;courier:number;sid:string;refresh:number}){
const [open,setOpen]=useState(false),[page,setPage]=useState(1),[reload,setReload]=useState(0),[result,setResult]=useState<Result|null>(null),[error,setError]=useState(''),[loading,setLoading]=useState(false),[detail,setDetail]=useState<number|null>(null);
useEffect(()=>{setOpen(false);setPage(1);setResult(null);setDetail(null);},[job,vehicle,courier,refresh]);
useEffect(()=>{if(!open)return;const c=new AbortController();setLoading(true);setResult(null);setError('');request<Result>(`/orders/${job}/courier-assignment/vehicles/${vehicle}/orders?courierId=${courier}&page=${page}`,{signal:c.signal}).then(r=>{if(!c.signal.aborted)setResult(r);}).catch(e=>{if(!c.signal.aborted)setError(e.message);}).finally(()=>{if(!c.signal.aborted)setLoading(false);});return()=>c.abort();},[job,vehicle,courier,refresh,open,page,reload]);
return <div className="assignment-orders"><button type="button" className="button" aria-expanded={open} onClick={()=>setOpen(v=>!v)}>{open?'Aufträge ausblenden':`Aufträge von ${sid} anzeigen`}</button>
{open&&<section aria-label={`Laufende Aufträge von ${sid}`}>
{loading&&<p role="status">Aufträge und Stationsfolge werden geladen …</p>}
{error&&<p role="alert" className="alert error">{error}</p>}
<button type="button" className="button" disabled={loading} onClick={()=>setReload(v=>v+1)}>Auftragsstand aktualisieren</button>
{result&&<><p>{result.total} laufende / vorgemerkte Aufträge</p><ul className="assignment-order-list">{result.items.map(o=><li key={o.id}>
<div>{o.canOpen?<button type="button" className="button" onClick={()=>setDetail(o.id)}>Auftrag #{o.id} öffnen</button>:<strong>Auftrag #{o.id}</strong>} {o.longhaul?'BWV · ':''}{o.headquarters}</div>
<div>{time(o.at)} · {o.status===0?'Angeboten':'Angenommen'} · {o.occupied?'Belegt im Vorlauffenster':'Außerhalb des Vorlauffensters'}</div>
<ol className="assignment-route" aria-label={`Stationsfolge Auftrag ${o.id}`}>{o.stations.map(t=><li key={t.id} className={t.completed?'completed':''}><span>{t.position}. {t.postalCode||'PLZ fehlt'}</span>{t.completed&&<span className="assignment-stop-state"> ✓ erledigt</span>}</li>)}</ol>
{!o.canOpen&&<p className="form-hint">Auftrag einer anderen Niederlassung. Vollständige Details sind hier nicht freigegeben.</p>}
</li>)}</ul>{!result.total&&<p>Derzeit keine weiteren laufenden Aufträge.</p>}
{result.total>result.pageSize&&<div className="admin-row-actions"><button type="button" className="button" disabled={page<=1} onClick={()=>setPage(v=>v-1)}>Vorherige Aufträge</button><span>Seite {page} / {Math.ceil(result.total/result.pageSize)}</span><button type="button" className="button" disabled={page*result.pageSize>=result.total} onClick={()=>setPage(v=>v+1)}>Weitere Aufträge</button></div>}
</>}
</section>}{detail!==null&&<AssignmentOrderDetails id={detail} onClose={()=>setDetail(null)}/>}</div>;
}
function AssignmentOrderDetails({id,onClose}:{id:number;onClose:()=>void}){
const uiText=useUiText();
const [order,setOrder]=useState<Order|null>(null),[error,setError]=useState(''),[reload,setReload]=useState(0);
useEffect(()=>{const c=new AbortController();setOrder(null);setError('');request<Order>(`/orders/${id}`,{signal:c.signal}).then(v=>{if(!c.signal.aborted)setOrder(v);}).catch(e=>{if(!c.signal.aborted)setError(e.message);});return()=>c.abort();},[id,reload]);
return <Modal title={`Auftragsdetails #${id}`} onClose={onClose}><div className="modal-body">
{error?<p role="alert" className="alert error">{error}</p>:!order?<p role="status">Auftrag wird geladen …</p>:<>
<p><strong>{order.legacyStatusLabel||order.status}</strong> · {order.service} · {order.vehicle}</p>
<p>{order.customerName||'Kunde nicht hinterlegt'} · {time(order.pickupAt)}<br/>Referenz: {order.reference||'–'} · Kostenstelle: {order.costCenter||'–'}</p>
{!!order.notes&&<p className="pre-line">{order.notes}</p>}
<ol className="assignment-detail-stops">{order.stops.map((s,i)=><li key={s.id||i}>
<strong>{s.sequence}. {s.name}</strong> · {s.type==='PICKUP'?uiText("Abholung"):s.type==='DELIVERY'?'Zustellung':s.type==='BOTH'?'Abholung und Zustellung':'Station'}
<p>{s.street}<br/>{s.postalCode} {s.city}</p>
{(s.contact||s.phone)&&<p>{s.contact} {s.phone}</p>}{s.note&&<p className="pre-line">{s.note}</p>}
{s.completedAt&&<p>Erledigt: {time(s.completedAt)}{s.recipientName?` · ${s.recipientName}`:''}</p>}
</li>)}</ol>
</>}
</div><div className="modal-footer"><button type="button" className="button" onClick={()=>setReload(v=>v+1)}>Details aktualisieren</button><button type="button" className="button primary" onClick={onClose}>Zurück zur Kurierauswahl</button></div></Modal>;
}