Files
votianng/frontend/src/AssignmentOrders.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

50 lines
5.4 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,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>;
}