{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")}
{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."}
{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.