import {OperationsMonitoring} from './OperationsMonitoring'; import {useUiText} from './UiLanguage'; import {WarehouseUsers} from './WarehouseUsers'; import type {User} from './types'; import type {WorkspaceEntity} from './WorkspaceHeadquarters'; import {HeadquartersSettings} from './HeadquartersSettings'; import {PdaLocatingSettings} from './PdaLocatingSettings'; import {EmployeeAdministration} from './EmployeeAdministration'; import {WarehouseAdministration} from './WarehouseAdministration'; import {ArticleAdministration} from './ArticleAdministration'; import {VehicleDisposition} from './VehicleDisposition'; import { lazy, Suspense, createContext, useContext, useEffect, useRef, useState, type FormEvent } from "react"; import { CalendarDays, Clock3, FolderTree, FileText, Info, LoaderCircle, Plus, Save, Trash2 } from "lucide-react"; const FleetMap = lazy(() => import("./FleetMap").then(m=>({default:m.FleetMap}))); import { ZonePrices } from "./ZonePrices"; import { ServicePrices } from "./ServicePrices"; import { AddressMappings } from "./AddressMappings"; import { Filters } from "./Filters"; import { Evaluations } from "./Newsletters"; import { ExtrasStatistics } from "./ExtrasStatistics"; import { Reports } from "./Reports"; import { request, put, post } from "./api"; import { Field, Modal, useDiscardConfirmation } from "./FormElements"; type Option = { id: string; label: string }; type Config = { operations:boolean; warehouseUsers:boolean; warehouses:boolean; articles:boolean; headquartersSettings:boolean; employees:boolean; pdaSettings:boolean; vehicleDisposition:boolean; fleetMap:boolean; extras:boolean; postalPrices:boolean; postalDiscounts:boolean; areaPrices:boolean; areaDiscounts:boolean; servicePrices:boolean; serviceDiscounts:boolean; postalAddresses:boolean; areaAddresses:boolean; filters:boolean; evaluations:boolean; headquartersId: number; holidays: boolean; travelTimes: boolean; groups: boolean; sharedGroups: boolean; customerReports: boolean; courierReports: boolean; groupKinds: Option[] }; type Section = "operations" | "warehouseUsers" | "warehouses" | "articles" | "headquartersSettings" | "pdaSettings" | "employees" | "vehicleDisposition" | "fleetMap" | "extras" | "postalPrices" | "postalDiscounts" | "areaPrices" | "areaDiscounts" | "servicePrices" | "serviceDiscounts" | "postalAddresses" | "areaAddresses" | "filters" | "evaluations" | "customerReports" | "courierReports" | "holidays" | "travelTimes" | "groups" | "imprint"; const sections = [{id:"warehouseUsers",label:"Lagermitarbeiter",icon:FolderTree},{id:"warehouses",label:"Lagerorte",icon:FolderTree},{id:"articles",label:"Artikel",icon:FolderTree},{id:"headquartersSettings",label:"Niederlassungen",icon:FolderTree},{id:"employees",label:"Mitarbeiter",icon:FolderTree},{id:"pdaSettings",label:"PDA-Ortung",icon:Clock3},{id:"vehicleDisposition",label:"Fahrzeugdisposition",icon:CalendarDays},{id:"fleetMap",label:"Kartenübersicht",icon:FolderTree},{id:"postalPrices",label:"PLZ-Preise",icon:FileText},{id:"postalDiscounts",label:"PLZ-Rabatte",icon:FileText},{id:"areaPrices",label:"Bereichspreise",icon:FileText},{id:"areaDiscounts",label:"Bereichsrabatte",icon:FileText},{id:"servicePrices",label:"Servicepreise",icon:FileText},{id:"serviceDiscounts",label:"Servicerabatte",icon:FileText},{id:"postalAddresses",label:"PLZ-Adressen",icon:FolderTree},{id:"areaAddresses",label:"Bereichsadressen",icon:FolderTree},{id:"filters",label:"Auftragsfilter",icon:FolderTree},{id:"evaluations",label:"Bewertungen",icon:FileText},{ id: "customerReports", label: "Kundenberichte", icon: FileText }, { id: "courierReports", label: "Transporteursberichte", icon: FileText }, { id: "holidays", label: "Feiertage", icon: CalendarDays }, { id: "travelTimes", label: "Anfahrtszeiten", icon: Clock3 }, { id: "groups", label: "Gruppen", icon: FolderTree }, {id:"extras",label:"Kessel Buntes",icon:FileText}, {id:"operations",label:"Betriebsstatus",icon:Clock3}, { id: "imprint", label: "Impressum", icon: Info }] as const; const dateString = (year: number, month: number, day: number) => `${year}-${String(month).padStart(2,"0")}-${String(day).padStart(2,"0")}`; const monthName = (month: number) => new Date(2024,month-1,1).toLocaleDateString("de-DE",{month:"long"}); const DraftContext=createContext<(changed:boolean)=>void>(()=>{}); function useDraftGuard(changed:boolean) { const setDirty=useContext(DraftContext); useEffect(()=>{setDirty(changed);return()=>setDirty(false);},[changed,setDirty]); } function Message({ error, notice }: { error: string; notice?: string }) { return <>{error &&
{error}
}{notice &&

{notice}

}; } function useResource(path: string, refresh: number) { const [data,setData] = useState(null), [error,setError] = useState(""), [loading,setLoading] = useState(true); useEffect(()=>{let active=true;setLoading(true);setError("");setData(null);request(path).then(d=>{if(active)setData(d);}).catch(e=>{if(active)setError(e.message);}).finally(()=>{if(active)setLoading(false);});return()=>{active=false;};},[path,refresh]); return {data,setData,error,setError,loading}; } export function Administration({ refresh,onDirtyChange,workspaceHeadquarters,onWorkspaceChanged }: { refresh: number;onDirtyChange:(dirty:boolean)=>void;workspaceHeadquarters:number;onWorkspaceChanged:(user:User,target:WorkspaceEntity)=>void }) { const uiText=useUiText("admin/menu.php"); const [dirty,setDirty]=useState(false); const [confirmDiscard,discardDialog]=useDiscardConfirmation(); useEffect(()=>{onDirtyChange(dirty);return()=>onDirtyChange(false);},[dirty,onDirtyChange]); const {data:config,error,loading}=useResource("/administration",refresh); const [section,setSection]=useState(null); const active=section||sections.find(s=>s.id==="imprint"||config?.[s.id])?.id||"imprint"; if(loading)return
Verwaltung wird geladen …
; if(!config)return ; return
{active==="operations"&&} {active==="warehouseUsers"&& } {active==="warehouses"&&} {active==="articles"&&} {active==="headquartersSettings"&&} {active==="pdaSettings"&&} {active==="employees"&&} {active==="vehicleDisposition"&&} {active==="fleetMap"&&Karte wird geladen …

}>
} {active==="extras"&&} {active==="postalPrices"&&} {active==="postalDiscounts"&&} {active==="areaPrices"&&} {active==="areaDiscounts"&&} {active==="servicePrices"&&} {active==="serviceDiscounts"&&} {active==="postalAddresses"&&} {active==="areaAddresses"&&} {active==="filters"&&} {active==="evaluations"&&} {active==="customerReports"&&} {active==="courierReports"&&} {active==="holidays"&&} {active==="travelTimes"&&} {active==="groups"&&} {active==="imprint"&&<>

{uiText("Impressum")}

Assecutor Data Service GmbH

Carsten Annacker
Gunnar Timm
Marc Vollmann

Gerhart-Hauptmann-Weg 14
21502 Geesthacht

{uiText("E-Mail: ")}contact@assecutor.de
Web: www.assecutor.de

}
{discardDialog}
; } type Holiday = {month:number;day:number;name:string}; type HolidayData = {year:number;today:string;revision:string;days:Holiday[];years:{year:number}[]}; function Holidays({refresh}:{refresh:number}) { const uiText=useUiText("admin/menu.php"); const [confirmDiscard,discardDialog]=useDiscardConfirmation(); const now=new Date(), [year,setYear]=useState(now.getFullYear()),[month,setMonth]=useState(now.getMonth()+1),[copyYear,setCopyYear]=useState("0"),[busy,setBusy]=useState(false),[notice,setNotice]=useState(""); const {data,setData,error,setError,loading}=useResource(`/administration/holidays/${year}`,refresh); const [draft,setDraft]=useState([]); useEffect(()=>{if(data){setDraft(data.days);}},[data]); const changed=!!data&&JSON.stringify(draft)!==JSON.stringify(data.days); useDraftGuard(changed); const selectable=[...new Set([0,...Array.from({length:11},(_,i)=>now.getFullYear()+i),...(data?.years.map(y=>y.year)||[])])].sort((a,b)=>b-a); function update(m:number,d:number,checked:boolean,name:string) {setDraft(v=>[...v.filter(x=>x.month!==m||x.day!==d),...(checked?[{month:m,day:d,name}]:[])].sort((a,b)=>a.month-b.month||a.day-b.day));} async function copy() { if(!data)return;setBusy(true);setError(""); try { const source=await request(`/administration/holidays/${copyYear}`); const next=[...draft];let count=0; for(const d of source.days) { if(d.day>new Date(year,d.month,0).getDate()||dateString(year,d.month,d.day)<=data.today||next.some(x=>x.month===d.month&&x.day===d.day))continue; next.push(d);count++; } setDraft(next.sort((a,b)=>a.month-b.month||a.day-b.day));setNotice(`${count} Feiertage übernommen. Zum Übernehmen der Änderungen bitte speichern.`); }catch(e){setError((e as Error).message);}finally{setBusy(false);} } async function save(e:FormEvent) {e.preventDefault();if(!data)return;setBusy(true);setError("");try{setData(await put(`/administration/holidays/${year}`,{revision:data.revision,days:draft}));setNotice("Feiertage gespeichert.");}catch(e){setError((e as Error).message);}finally{setBusy(false);}} return <>

{uiText("Feiertage")}

Jahreskalender und Übernahme aus einer Vorlage oder einem Vorjahr.

{draft.length} {uiText(" Feiertage")}
{loading?:data&&<>

Vergangene Tage und heute bleiben geschützt.

{Array.from({length:new Date(year||2000,month,0).getDate()},(_,i)=>{ const d=i+1,h=draft.find(v=>v.month===month&&v.day===d),label=dateString(year||2000,month,d),locked=!year||label<=data.today; return ; })}
DatumFeiertag{uiText("Bezeichnung")}
{new Date(year||2000,month-1,d).toLocaleDateString("de-DE",{weekday:"short",day:"2-digit",month:"2-digit"})}update(month,d,e.target.checked,h?.name||"")}/>update(month,d,true,e.target.value)}/>
}
{discardDialog}; } type TravelRow={id:number;postalCode:string;country:string;minutes:number|null;revision:string}; function TravelTimes({refresh}:{refresh:number}) { const uiText=useUiText("admin/menu.php"); const [confirmDiscard,discardDialog]=useDiscardConfirmation(); const [mode,setMode]=useState("CLASSIC"),[from,setFrom]=useState(""),[to,setTo]=useState(""),[rows,setRows]=useState([]),[values,setValues]=useState>({}),[busy,setBusy]=useState(false),[error,setError]=useState(""),[notice,setNotice]=useState(""),[loaded,setLoaded]=useState(false),[defaultMinutes,setDefaultMinutes]=useState(null); const changed=rows.some(r=>String(r.minutes??"")!==values[r.id]); useDraftGuard(changed); const lastRefresh=useRef(refresh); useEffect(()=>{if(lastRefresh.current!==refresh){lastRefresh.current=refresh;setRows([]);setValues({});setLoaded(false);}},[refresh]); async function load(e:FormEvent) {e.preventDefault();if(changed&&!(await confirmDiscard("Ungespeicherte Anfahrtszeiten verwerfen?")))return;setBusy(true);setError("");setNotice("");try{const data=await request<{items:TravelRow[];defaultMinutes:number|null}>("/administration/travel-times?"+new URLSearchParams({mode,from,to}));setDefaultMinutes(data.defaultMinutes);setRows(data.items);setValues(Object.fromEntries(data.items.map(r=>[r.id,String(r.minutes??"")])));setLoaded(true);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function save(row:TravelRow) {setBusy(true);setError("");setNotice("");try{const saved=await put(`/administration/travel-times/${row.id}?mode=${mode}`,{revision:row.revision,minutes:Number(values[row.id])});setRows(v=>v.map(r=>r.id===saved.id?saved:r));setValues(v=>({...v,[saved.id]:String(saved.minutes)}));setNotice(`Anfahrtszeit für ${saved.postalCode} gespeichert.`);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} return <>

{uiText("Anfahrtszeiten")}

{mode==="CLASSIC"?"Vorgaben je PLZ für Ihre Niederlassung.":"BWV-Vorgaben gelten zentral für alle Niederlassungen."}

setFrom(e.target.value)}/>setTo(e.target.value)}/>
{rows.map(r=>)}
{uiText("PLZ")}{uiText("Land")}Zeit in Minuten{uiText("Aktion")}
{r.postalCode}{r.country}setValues(v=>({...v,[r.id]:e.target.value}))}/>
{loaded&&

{rows.length} PLZ gefunden · Änderungen werden pro Zeile gespeichert. {defaultMinutes!==null?`Leer oder 0 übernimmt den Niederlassungsstandard: ${defaultMinutes} Minuten.`:"Kein gültiger Niederlassungsstandard konfiguriert."}

}{busy&&}{discardDialog}; } type Group = { id:number;name:string;shared:boolean;readOnly:boolean;revision:string }; type Member = { id:number;label:string;member:boolean;revision:string }; function Groups({refresh,config}:{refresh:number;config:Config}) { const uiText=useUiText("admin/menu.php"); const [hq,setHq]=useState(config.headquartersId),[reload,setReload]=useState(0); const suffix=`?headquartersId=${hq}`; const {data:headquarters}=useResource<{id:number;label:string}[]>("/administration/groups/headquarters",refresh); const {data:groups,error,setError,loading}=useResource("/administration/groups"+suffix,refresh+reload); const {data:operations}=useResource<{token:string;name:string}[]>("/administration/groups/operations"+suffix,refresh+reload); const [editor,setEditor]=useState(null),[selected,setSelected]=useState(null),[removing,setRemoving]=useState(null),[busy,setBusy]=useState(false),[notice,setNotice]=useState(""); const [plan,setPlan]=useState<{revision:string;total:number;counts:{label:string;count:number}[]}|null>(null),[deleteToken,setDeleteToken]=useState(""),[uncertain,setUncertain]=useState(false); useDraftGuard(busy||uncertain||!!editor); async function prepare(group:Group){setRemoving(group);setPlan(null);setBusy(true);setError("");setDeleteToken(crypto.randomUUID());try{setPlan(await request(`/administration/groups/${group.id}/deletion`+suffix));}catch(e){setError((e as Error).message);setRemoving(null);}finally{setBusy(false);}} async function remove(resumeToken?:string) { if(!resumeToken&&(!removing||!plan))return;setBusy(true);setError(""); try{ const result=resumeToken?await post<{state:string;message?:string}>(`/administration/groups/operations/${resumeToken}`+suffix,{}):await request<{state:string;message?:string}>(`/administration/groups/${removing!.id}`+suffix,{method:"DELETE",body:JSON.stringify({requestId:deleteToken,revision:plan!.revision})}); setUncertain(false);setRemoving(null);setSelected(null);setReload(v=>v+1);setNotice(result.state==="DONE"?"Gruppe und ihre Zuordnungen entfernt.":result.message||"Löschvorgang bitte fortsetzen."); }catch(e){setError((e as Error).message);if(!resumeToken)setUncertain(true);}finally{setBusy(false);} } return <>

{uiText("Gruppen")}

Gruppen und ihre Zuordnungen zu Kunden, Transporteuren und weiteren Stammdaten.

{operations?.map(o=>
Offene Löschung: {o.name}
)} {loading?:
{groups?.map(g=>)}
GruppeSichtbarkeitAktionen
{g.name}{g.readOnly&& · geschützt}{g.shared?"Alle Niederlassungen":headquarters?.find(h=>h.id===hq)?.label||"Gewählte Niederlassung"}
} {editor&&setEditor(null)} onSaved={()=>{setEditor(null);setReload(v=>v+1);setNotice("Gruppe gespeichert.");}}/>} {selected&&setSelected(null)}/>} {removing&&{if(!busy&&!uncertain)setRemoving(null);}}>

„{removing.name}“ dauerhaft löschen?

{plan?<>

{plan.total} Zuordnungen werden entfernt. Die zugeordneten Stammdaten bleiben erhalten.

    {plan.counts.map(c=>
  • {c.label}: {c.count}
  • )}
:

Zuordnungen werden geprüft …

}{uncertain&&

Der Ausgang ist noch unklar. Bitte denselben Vorgang erneut prüfen.

}
} ; } function GroupEditor({group,headquarters,sharedAllowed,onClose,onSaved}:{group:Group|"new";headquarters:number;sharedAllowed:boolean;onClose:()=>void;onSaved:()=>void}) { const uiText=useUiText("admin/menu.php"); const [name,setName]=useState(group==="new"?"":group.name),[shared,setShared]=useState(group==="new"?false:group.shared),[busy,setBusy]=useState(false),[error,setError]=useState(""); const changed=name!==(group==="new"?"":group.name)||shared!==(group==="new"?false:group.shared); useDraftGuard(changed); const [confirmDiscard,discardDialog]=useDiscardConfirmation(); async function close(){if(!busy&&(!changed||await confirmDiscard("Ungespeicherte Gruppenänderungen verwerfen?")))onClose();} async function save(e:FormEvent){e.preventDefault();setBusy(true);setError("");try{const body={name,shared,revision:group==="new"?undefined:group.revision};if(group==="new")await post(`/administration/groups?headquartersId=${headquarters}`,body);else await put(`/administration/groups/${group.id}?headquartersId=${headquarters}`,body);onSaved();}catch(e){setError((e as Error).message);}finally{setBusy(false);}} return <>
setName(e.target.value)}/>{sharedAllowed&&}
{discardDialog}; } function GroupMembers({group,headquarters,kinds,onClose}:{group:Group;headquarters:number;kinds:Option[];onClose:()=>void}) { const uiText=useUiText("admin/menu.php"); const [kind,setKind]=useState(kinds[0]?.id||"CUSTOMER"),[q,setQ]=useState(""),[search,setSearch]=useState(""),[onlyMembers,setOnlyMembers]=useState(true),[reload,setReload]=useState(0),[busy,setBusy]=useState(false); const {data,error,setError,loading}=useResource<{items:Member[];truncated:boolean}>(`/administration/groups/${group.id}/members?`+new URLSearchParams({headquartersId:String(headquarters),kind,q:search,onlyMembers:String(onlyMembers)}),reload); async function change(member:Member){setBusy(true);setError("");try{await put(`/administration/groups/${group.id}/members/${kind}/${member.id}?headquartersId=${headquarters}`,{revision:member.revision,member:!member.member});setReload(v=>v+1);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} return {if(!busy)onClose();}} wide>
{e.preventDefault();setSearch(q);}}>
setQ(e.target.value)} maxLength={100}/>
{loading?:
{data?.items.map(m=>)}
{uiText("Bezeichnung")}Mitgliedschaft
{m.label||`#${m.id}`}
{!data?.items.length&&

Keine Einträge gefunden.

}
}{data?.truncated&&

Die ersten 500 Treffer werden angezeigt. Bitte die Suche eingrenzen.

}
; }