Erweitere VoTiAn NG um Auftrags-, Stamm- und Verwaltungsfunktionen
Ergänzt Auftragserfassung und -bearbeitung, Unternehmerbelege, Stornos, Lagerkorrekturen, Stammdaten, Abrechnung, CRM, Formulare, Berichte und Preisverwaltung auf Basis der aktuellen PHP-Anwendung. Enthält Integrations- und Oberflächentests sowie dokumentierte Abgleiche und Prüfstände.
This commit is contained in:
1 parent
42c87282ee
commit
363bc27c26
190 files changed
+14815
-1857
No files matched your search
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useState, type FormEvent } from "react";
|
||||
import { LoaderCircle, MessageSquare, Radio, Plus, Send } from "lucide-react";
|
||||
import { Field, Modal, useDiscardConfirmation } from "./FormElements";
|
||||
import { request, post, put } from "./api";
|
||||
|
||||
import { Newsletters } from "./Newsletters";
|
||||
|
||||
type Option={id:number;label:string;headquarters?:number;device?:string;available?:string};
|
||||
type Options={newsletters?:boolean;devices:boolean;bulletins:boolean;manageGroups:boolean;allActive:boolean;today:string;headquartersId:number;headquarters:Option[];employees:Option[];maxBulletin:number;maxMessage:number};
|
||||
type Group={id:number;name:string;groups:number[];revision:string};
|
||||
type Member=Option&{member:boolean;revision:string};
|
||||
type Bulletin={id:number;created:string;from:string;to:string;text:string;sender:string;senderId:number;recipient:string;headquarters:string;removable:boolean;revision:string};
|
||||
type Message={id:number;created:string;delivery:string|null;acknowledged:string|null;state:number;priority:number;sender:string;recipient:string;reader:string;answerer:string;subject:string;body:string;replyTo:number;canRead:boolean;canReply:boolean;revision:string};
|
||||
type Preview={token:string;recipients:Option[];subject:string;body:string;replyTo:number};
|
||||
type SendResult={queued:number;total:number;uncertain:number;unattempted:number;warning:string};
|
||||
type Page<T>={total:number;items:T[]};
|
||||
const when=(v:string|null)=>v?new Date(v).toLocaleString("de-DE"):"–";
|
||||
const states=["Noch nicht ausgeliefert","Ausgeliefert, ungelesen","Gelesen","Beantwortet"];
|
||||
function ErrorMessage({error}:{error:string}){return error?<div className="alert error" role="alert">{error}</div>:null;}
|
||||
function Pages({page,total,onChange,busy=false}:{page:number;total:number;onChange:(page:number)=>void;busy?:boolean}){return <div className="admin-toolbar"><span>{total} Einträge · Seite {page+1} von {Math.max(1,Math.ceil(total/100))}</span><button className="button" disabled={!page||busy} onClick={()=>onChange(page-1)}>Zurück</button><button className="button" disabled={(page+1)*100>=total||busy} onClick={()=>onChange(page+1)}>Weiter</button></div>;}
|
||||
export function Messages({refresh,onDirtyChange}:{refresh:number;onDirtyChange:(dirty:boolean)=>void}){
|
||||
const [options,setOptions]=useState<Options|null>(null),[error,setError]=useState(""),[view,setView]=useState(""),[dirty,setDirty]=useState(false);
|
||||
const [discard,dialog]=useDiscardConfirmation();
|
||||
useEffect(()=>{let live=true;request<Options>("/messages/options").then(o=>{if(live){setOptions(o);setError("");}}).catch(e=>{if(live)setError(e.message);});return()=>{live=false;};},[refresh]);
|
||||
useEffect(()=>{onDirtyChange(dirty);return()=>onDirtyChange(false);},[dirty,onDirtyChange]);
|
||||
const active=view||(options?.devices?"devices":options?.bulletins?"bulletins":"newsletters");
|
||||
return <><ErrorMessage error={error}/>{options?<div className="administration"><aside className="admin-navigation" aria-label="Nachrichtenbereiche">{options.devices&&<button className={active==="devices"?"active":""} onClick={async()=>{if(!dirty||await discard("Ungespeicherte Nachricht verwerfen?"))setView("devices");}}><Radio size={18}/>Endgeräte</button>}{options.bulletins&&<button className={active==="bulletins"?"active":""} onClick={async()=>{if(!dirty||await discard("Ungespeicherte Nachricht verwerfen?"))setView("bulletins");}}><MessageSquare size={18}/>Mitteilungen</button>}{options.newsletters&&<button className={active==="newsletters"?"active":""} onClick={async()=>{if(!dirty||await discard("Ungespeicherten Newsletter verwerfen?"))setView("newsletters");}}><MessageSquare size={18}/>Newsletter</button>}</aside><section className="panel admin-panel" key={`${active}:${refresh}`}>{active==="devices"?<DeviceMessages options={options} onDirty={setDirty}/>:active==="bulletins"?<Bulletins options={options} onDirty={setDirty}/>:<Newsletters onDirty={setDirty}/>}</section></div>:!error&&<LoaderCircle className="spin"/>}{dialog}</>;
|
||||
}
|
||||
function Bulletins({options,onDirty}:{options:Options;onDirty:(v:boolean)=>void}){
|
||||
const [data,setData]=useState<Page<Bulletin>>({items:[],total:0}),[page,setPage]=useState(0),[expired,setExpired]=useState(false),[reload,setReload]=useState(0),[error,setError]=useState(""),[notice,setNotice]=useState(""),[busy,setBusy]=useState(false),[editor,setEditor]=useState<{employee:number}|null>(null),[removing,setRemoving]=useState<Bulletin|null>(null);
|
||||
useEffect(()=>{let live=true;setBusy(true);request<Page<Bulletin>>(`/messages/bulletins?expired=${expired}&page=${page}`).then(d=>{if(live){setData(d);setError("");}}).catch(e=>{if(live)setError(e.message);}).finally(()=>{if(live)setBusy(false);});return()=>{live=false;};},[expired,page,reload]);
|
||||
async function remove(){if(!removing)return;setBusy(true);try{await request(`/messages/bulletins/${removing.id}`,{method:"DELETE",body:JSON.stringify({revision:removing.revision})});setRemoving(null);setReload(v=>v+1);setNotice("Mitteilung entfernt.");}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
|
||||
return <><div className="admin-title"><div><h2>Mitteilungen</h2><p>Nachrichten an Mitarbeiter und Niederlassungen mit Gültigkeitszeitraum.</p></div><button className="button primary" onClick={()=>setEditor({employee:0})}><Plus size={16}/>Neue Mitteilung</button></div><ErrorMessage error={error}/>{notice&&<p className="admin-success" role="status">{notice}</p>}<label className="admin-check"><input type="checkbox" checked={expired} onChange={e=>{setExpired(e.target.checked);setPage(0);}}/>Abgelaufene Mitteilungen anzeigen</label>{busy&&<LoaderCircle className="spin"/>}<div className="admin-table-wrap"><table className="admin-table"><thead><tr><th>Gültigkeit</th><th>Absender / Empfänger</th><th>Mitteilung</th><th>Aktionen</th></tr></thead><tbody>{data.items.map(b=><tr key={b.id}><td>{when(b.from)}<br/>bis {when(b.to)}<small>Erstellt: {when(b.created)}</small></td><td>{b.sender||"Unbekannt"}<br/>An: {b.recipient||b.headquarters||"Alle"}</td><td className="message-text">{b.text}</td><td><button className="button" disabled={!options.employees.some(e=>e.id===b.senderId)} onClick={()=>setEditor({employee:b.senderId})}>Antworten</button>{b.removable&&<button className="button danger" onClick={()=>setRemoving(b)}>Entfernen</button>}</td></tr>)}</tbody></table>{!busy&&!data.items.length&&<p>Keine Mitteilungen vorhanden.</p>}</div><Pages page={page} total={data.total} onChange={setPage} busy={busy}/>
|
||||
{editor&&<BulletinEditor options={options} employee={editor.employee} onDirty={onDirty} onClose={()=>setEditor(null)} onSaved={()=>{setEditor(null);setReload(v=>v+1);setNotice("Mitteilung veröffentlicht.");}}/>}
|
||||
{removing&&<Modal title="Mitteilung entfernen" onClose={()=>{if(!busy)setRemoving(null);}}><div className="admin-dialog-body"><p>Diese Mitteilung dauerhaft entfernen?</p><p className="message-text">{removing.text}</p><ErrorMessage error={error}/><div className="admin-row-actions"><button className="button" disabled={busy} onClick={()=>setRemoving(null)}>Abbrechen</button><button className="button danger" disabled={busy} onClick={()=>void remove()}>Entfernen</button></div></div></Modal>}</>;
|
||||
}
|
||||
function BulletinEditor({options,employee,onDirty,onClose,onSaved}:{options:Options;employee:number;onDirty:(v:boolean)=>void;onClose:()=>void;onSaved:()=>void}){
|
||||
const [text,setText]=useState(""),[from,setFrom]=useState(options.today),[to,setTo]=useState(options.today),[hq,setHq]=useState(options.headquartersId),[recipient,setRecipient]=useState(employee),[error,setError]=useState(""),[busy,setBusy]=useState(false),[preview,setPreview]=useState(false);const [discard,dialog]=useDiscardConfirmation();
|
||||
const dirty=!!text||from!==options.today||to!==options.today||hq!==options.headquartersId||recipient!==employee;
|
||||
useEffect(()=>{onDirty(dirty||busy);return()=>onDirty(false);},[dirty,busy,onDirty]);
|
||||
async function close(){if(!busy&&(!dirty||await discard("Ungespeicherte Mitteilung verwerfen?")))onClose();}
|
||||
async function send(){setBusy(true);setError("");try{await post("/messages/bulletins",{from,to,headquarters:hq,employee:recipient,text});onSaved();}catch(e){setError((e as Error).message);setPreview(false);}finally{setBusy(false);}}
|
||||
return <><Modal title={employee?"Auf Mitteilung antworten":"Neue Mitteilung"} onClose={close} wide><form className="admin-dialog-body" onSubmit={e=>{e.preventDefault();setPreview(true);}}><ErrorMessage error={error}/><fieldset disabled={busy||preview}><div className="admin-toolbar"><Field label="Gültig von"><input type="date" required value={from} onInput={e=>setFrom(e.currentTarget.value)}/></Field><Field label="Gültig bis"><input type="date" required min={from>options.today?from:options.today} value={to} onInput={e=>setTo(e.currentTarget.value)}/></Field><Field label="Niederlassung"><select value={hq} disabled={recipient>0} onChange={e=>setHq(Number(e.target.value))}>{options.headquarters.length>1&&<option value={0}>Alle freigegebenen Niederlassungen</option>}{options.headquarters.map(h=><option key={h.id} value={h.id}>{h.label}</option>)}</select></Field><Field label="Empfänger"><select value={recipient} onChange={e=>setRecipient(Number(e.target.value))}><option value={0}>Alle Mitarbeiter der Niederlassung</option>{options.employees.map(p=><option key={p.id} value={p.id}>{p.label} [{options.headquarters.find(h=>h.id===p.headquarters)?.label}]</option>)}</select></Field></div><Field label="Mitteilung"><textarea required rows={6} maxLength={options.maxBulletin} value={text} onChange={e=>setText(e.target.value)}/></Field><p className="form-hint">{text.length} / {options.maxBulletin} Zeichen</p><div className="admin-row-actions"><button type="button" className="button" onClick={close}>Abbrechen</button><button className="button primary">Mitteilung prüfen</button></div></fieldset></form></Modal>{preview&&<Modal title="Mitteilung veröffentlichen" onClose={()=>{if(!busy)setPreview(false);}}><div className="admin-dialog-body"><p><strong>An:</strong> {recipient?options.employees.find(e=>e.id===recipient)?.label:hq?options.headquarters.find(h=>h.id===hq)?.label:"Alle freigegebenen Niederlassungen"}</p><p>{from} bis {to}</p><p className="message-text">{text}</p><div className="admin-row-actions"><button className="button" disabled={busy} onClick={()=>setPreview(false)}>Zurück</button><button className="button primary" disabled={busy} onClick={()=>void send()}>Jetzt veröffentlichen</button></div></div></Modal>}{dialog}</>;
|
||||
}
|
||||
function DeviceMessages({options,onDirty}:{options:Options;onDirty:(v:boolean)=>void}){
|
||||
const initial={from:options.today,to:options.today,states:[0,1],maxPriority:3,direction:"EXCLUDE_HQ",courier:null as number|null,page:0};
|
||||
const [filter,setFilter]=useState(initial),[query,setQuery]=useState(initial),[data,setData]=useState<Page<Message>>({items:[],total:0}),[groups,setGroups]=useState<Group[]>([]),[reload,setReload]=useState(0),[error,setError]=useState(""),[busy,setBusy]=useState(false),[composer,setComposer]=useState<Message|"new"|null>(null),[groupEditor,setGroupEditor]=useState<Group|"new"|null>(null),[members,setMembers]=useState<Group|null>(null),[removing,setRemoving]=useState<Group|null>(null),[courier,setCourier]=useState<Option|null>(null);
|
||||
useEffect(()=>{let live=true;setBusy(true);Promise.all([post<Page<Message>>("/messages/history",query),request<Group[]>("/messages/groups")]).then(([d,g])=>{if(live){setData(d);setGroups(g);setError("");}}).catch(e=>{if(live)setError(e.message);}).finally(()=>{if(live)setBusy(false);});return()=>{live=false;};},[query,reload]);
|
||||
async function read(m:Message){setBusy(true);try{await post(`/messages/${m.id}/read`,{revision:m.revision});setReload(v=>v+1);}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
|
||||
async function remove(){if(!removing)return;setBusy(true);try{await request(`/messages/groups/${removing.id}`,{method:"DELETE",body:JSON.stringify({revision:removing.revision})});setRemoving(null);setReload(v=>v+1);}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
|
||||
return <><div className="admin-title"><div><h2>Endgeräte und Nachrichten</h2><p>Nachrichtenhistorie, Empfangsbestätigungen und Versand an Transporteure.</p></div><button className="button primary" onClick={()=>setComposer("new")}><Send size={16}/>Neue Nachricht</button></div><ErrorMessage error={error}/>
|
||||
<form onSubmit={e=>{e.preventDefault();setQuery({...filter,courier:courier?.id||null,page:0});}}><div className="admin-toolbar"><Field label="Von"><input type="date" required value={filter.from} onInput={e=>{const value=e.currentTarget.value;setFilter(v=>({...v,from:value}));}}/></Field><Field label="Bis"><input type="date" required value={filter.to} min={filter.from} onInput={e=>{const value=e.currentTarget.value;setFilter(v=>({...v,to:value}));}}/></Field><Field label="Nachrichtenart"><select value={filter.direction} onChange={e=>setFilter(v=>({...v,direction:e.target.value}))}><option value="ALL">Alle Nachrichten</option><option value="EXCLUDE_HQ">Zentralennachrichten ausblenden</option><option value="ONLY_HQ">Nur Zentralennachrichten</option></select></Field><Field label="Maximale Prioritätsstufe"><select value={filter.maxPriority} onChange={e=>setFilter(v=>({...v,maxPriority:Number(e.target.value)}))}>{[0,1,2,3].map(n=><option key={n}>{n}</option>)}</select></Field><button className="button primary" disabled={busy||!filter.states.length}>Anzeigen</button></div><div className="admin-toolbar">{states.map((s,i)=><label key={s} className="admin-check"><input type="checkbox" checked={filter.states.includes(i)} onChange={e=>setFilter(v=>({...v,states:e.target.checked?[...v.states,i]:v.states.filter(n=>n!==i)}))}/>{s}</label>)}</div><CourierSearch label="Transporteur / Endgerät filtern" value={courier} onSelect={setCourier}/></form>
|
||||
{busy&&<LoaderCircle className="spin"/>}<div className="admin-table-wrap"><table className="admin-table"><thead><tr><th>Zeit / Status</th><th>Absender / Empfänger</th><th>Nachricht</th><th>Aktionen</th></tr></thead><tbody>{data.items.map(m=><tr key={m.id}><td>#{m.id} · {when(m.created)}<br/>{states[m.state]||m.state}<small>Zustellung: {when(m.delivery)}<br/>Bestätigung: {when(m.acknowledged)}{m.reader&&<><br/>Gelesen von: {m.reader}</>}{m.answerer&&<><br/>Beantwortet von: {m.answerer}</>}</small></td><td>{m.sender||"Unbekannt"}<br/>→ {m.recipient||"Zentrale"}</td><td><strong>{m.subject}</strong><p className="message-text">{m.body}</p>{m.replyTo>0&&<small>Antwort auf #{m.replyTo}</small>}</td><td>{m.canRead&&<button className="button" disabled={busy} onClick={()=>void read(m)}>Als gelesen markieren</button>}{m.canReply&&<button className="button" disabled={busy} onClick={()=>setComposer(m)}>Antworten</button>}</td></tr>)}</tbody></table>{!busy&&!data.items.length&&<p>Keine Nachrichten für diese Filter.</p>}</div><Pages page={query.page} total={data.total} onChange={p=>setQuery(v=>({...v,page:p}))} busy={busy}/>
|
||||
<div className="admin-title"><h3>Nachrichtengruppen</h3>{options.manageGroups&&<button className="button" onClick={()=>setGroupEditor("new")}>Neue Nachrichtengruppe</button>}</div><div className="admin-table-wrap"><table className="admin-table"><thead><tr><th>Gruppe</th><th>Zugeordnete Gruppen</th>{options.manageGroups&&<th>Aktionen</th>}</tr></thead><tbody>{groups.map(g=><tr key={g.id}><td>{g.name}</td><td>{g.groups.length}</td>{options.manageGroups&&<td><button className="button" onClick={()=>setGroupEditor(g)}>Bearbeiten</button><button className="button" onClick={()=>setMembers(g)}>Zuordnungsliste</button><button className="button danger" onClick={()=>setRemoving(g)}>Löschen</button></td>}</tr>)}</tbody></table></div>
|
||||
{composer&&<MessageComposer options={options} groups={groups} reply={composer==="new"?null:composer} onDirty={onDirty} onClose={()=>{setComposer(null);setReload(v=>v+1);}}/>}
|
||||
{groupEditor&&<MessageGroupEditor group={groupEditor} onDirty={onDirty} onClose={()=>setGroupEditor(null)} onSaved={()=>{setGroupEditor(null);setReload(v=>v+1);}}/>}
|
||||
{members&&<MessageGroupMembers group={members} onClose={()=>{setMembers(null);setReload(v=>v+1);}}/>}
|
||||
{removing&&<Modal title="Nachrichtengruppe löschen" onClose={()=>{if(!busy)setRemoving(null);}}><div className="admin-dialog-body"><p>„{removing.name}“ und ihre Transporteurszuordnungen dauerhaft entfernen?</p><ErrorMessage error={error}/><div className="admin-row-actions"><button className="button" disabled={busy} onClick={()=>setRemoving(null)}>Abbrechen</button><button className="button danger" disabled={busy} onClick={()=>void remove()}>Gruppe löschen</button></div></div></Modal>}</>;
|
||||
}
|
||||
function CourierSearch({label,value,onSelect}:{label:string;value:Option|null;onSelect:(v:Option|null)=>void}){
|
||||
const [q,setQ]=useState(""),[items,setItems]=useState<Option[]>([]),[error,setError]=useState("");
|
||||
useEffect(()=>{if(value)return;let live=true;const timer=setTimeout(()=>request<Option[]>(`/messages/couriers?q=${encodeURIComponent(q)}`).then(d=>{if(live){setItems(d);setError("");}}).catch(e=>{if(live)setError(e.message);}),250);return()=>{live=false;clearTimeout(timer);};},[q,value]);
|
||||
return <div className="message-lookup"><ErrorMessage error={error}/>{value?<div className="admin-toolbar"><strong>{label}: {value.label}</strong>{value.device&&<span>Endgerät: {value.device}</span>}<button className="button" type="button" onClick={()=>onSelect(null)}>Auswahl ändern</button></div>:<div className="admin-toolbar"><Field label={label}><input type="search" value={q} placeholder="Fahrzeugnummer oder Name" onChange={e=>setQ(e.target.value)}/></Field><Field label="Treffer auswählen"><select value="" onChange={e=>onSelect(items.find(v=>v.id===Number(e.target.value))||null)}><option value="">Keine Auswahl</option>{items.map(c=><option key={c.id} value={c.id}>{c.label}{c.device?` · ${c.device}`:""}</option>)}</select></Field><small>Bis zu 100 Treffer; Suche bei Bedarf eingrenzen.</small></div>}</div>;
|
||||
}
|
||||
function MessageComposer({options,groups,reply,onDirty,onClose}:{options:Options;groups:Group[];reply:Message|null;onDirty:(v:boolean)=>void;onClose:()=>void}){
|
||||
const [subject,setSubject]=useState(reply?.subject?`AW: ${reply.subject}`.slice(0,40):""),[body,setBody]=useState(""),[mode,setMode]=useState("courier"),[courier,setCourier]=useState<Option|null>(null),[selected,setSelected]=useState<number[]>([]),[preview,setPreview]=useState<Preview|null>(null),[result,setResult]=useState<SendResult|null>(null),[error,setError]=useState(""),[busy,setBusy]=useState(false),[attempted,setAttempted]=useState(false);const [discard,dialog]=useDiscardConfirmation();
|
||||
const dirty=!result&&!!(body||subject||courier||selected.length);useEffect(()=>{onDirty(dirty||busy);return()=>onDirty(false);},[dirty,busy,onDirty]);
|
||||
async function close(){if(!busy&&(!dirty||await discard("Ungespeicherte Nachricht verwerfen?")))onClose();}
|
||||
async function prepare(e:FormEvent){e.preventDefault();setBusy(true);setError("");try{setPreview(await post<Preview>("/messages/preview",{subject,body,courier:!reply&&mode==="courier"?courier?.id:null,groups:!reply&&mode==="groups"?selected:[],allActive:!reply&&mode==="all",replyTo:reply?.id||null,revision:reply?.revision||null}));setAttempted(false);}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
|
||||
async function send(){if(!preview)return;setBusy(true);setAttempted(true);setError("");try{setResult(await post<SendResult>(`/messages/send/${preview.token}`));}catch(e){setError(`${(e as Error).message} Die Übergabe ist möglicherweise schon erfolgt. „Ergebnis abrufen“ verwendet denselben Versandauftrag.`);}finally{setBusy(false);}}
|
||||
return <><Modal title={reply?`Antwort auf Nachricht #${reply.id}`:"Neue Endgerätenachricht"} onClose={close} wide><div className="admin-dialog-body"><ErrorMessage error={error}/>{result?<><h3>{result.queued} von {result.total} Nachrichten übergeben</h3><p>Die Historie zeigt den weiteren Zustell- und Lesestatus.</p>{result.warning&&<div className="alert error" role="alert">{result.warning}</div>}{(result.uncertain>0||result.unattempted>0)&&<p>{result.uncertain} unbestätigte Übergabe · {result.unattempted} Empfänger noch nicht versucht</p>}<button className="button primary" onClick={onClose}>Zur Historie</button></>:preview?<><p><strong>{preview.recipients.length} Empfänger</strong></p><div className="message-recipients">{preview.recipients.map(p=><div key={p.id}>{p.label}</div>)}</div><h3>{preview.subject||"Ohne Betreff"}</h3><p className="message-text">{preview.body}</p><p className="form-hint">Mit „Jetzt senden“ wird die Nachricht zur Zustellung an diese Endgeräte übergeben.</p><div className="admin-row-actions"><button className="button" disabled={busy||attempted} onClick={()=>setPreview(null)}>Bearbeiten</button><button className="button primary" disabled={busy} onClick={()=>void send()}>{attempted?"Ergebnis abrufen":"Jetzt senden"}</button></div></>:<form onSubmit={prepare}><fieldset disabled={busy}>{!reply&&<><Field label="Empfängerauswahl"><select value={mode} onChange={e=>setMode(e.target.value)}><option value="courier">Einzelner Transporteur</option><option value="groups">Nachrichtengruppen</option>{options.allActive&&<option value="all">Alle aktiven Fahrer der freigegebenen Niederlassungen</option>}</select></Field>{mode==="courier"&&<CourierSearch label="Transporteur" value={courier} onSelect={setCourier}/>} {mode==="groups"&&<div className="admin-toolbar">{groups.map(g=><label className="admin-check" key={g.id}><input type="checkbox" checked={selected.includes(g.id)} onChange={e=>setSelected(v=>e.target.checked?[...v,g.id]:v.filter(id=>id!==g.id))}/>{g.name}</label>)}</div>}</>}<Field label="Betreff"><input maxLength={40} value={subject} onChange={e=>setSubject(e.target.value)}/></Field><Field label="Nachricht"><textarea required rows={6} maxLength={options.maxMessage} value={body} onChange={e=>setBody(e.target.value)}/></Field><p className="form-hint">{body.length} / {options.maxMessage} Zeichen</p><div className="admin-row-actions"><button className="button" type="button" onClick={close}>Abbrechen</button><button className="button primary">Empfänger und Nachricht prüfen</button></div></fieldset></form>}</div></Modal>{dialog}</>;
|
||||
}
|
||||
function MessageGroupEditor({group,onDirty,onClose,onSaved}:{group:Group|"new";onDirty:(v:boolean)=>void;onClose:()=>void;onSaved:()=>void}){
|
||||
const [name,setName]=useState(group==="new"?"":group.name),[selected,setSelected]=useState(group==="new"?[]:group.groups),[available,setAvailable]=useState<Option[]>([]),[error,setError]=useState(""),[busy,setBusy]=useState(false);const [discard,dialog]=useDiscardConfirmation();const dirty=name!==(group==="new"?"":group.name)||JSON.stringify(selected)!==JSON.stringify(group==="new"?[]:group.groups);
|
||||
useEffect(()=>{onDirty(dirty||busy);return()=>onDirty(false);},[dirty,busy,onDirty]);useEffect(()=>{let live=true;request<Option[]>("/messages/generic-groups").then(v=>{if(live)setAvailable(v);}).catch(e=>{if(live)setError(e.message);});return()=>{live=false;};},[]);
|
||||
async function close(){if(!busy&&(!dirty||await discard("Ungespeicherte Gruppendaten verwerfen?")))onClose();}
|
||||
async function save(e:FormEvent){e.preventDefault();setBusy(true);setError("");try{const body={name,groups:selected,revision:group==="new"?null:group.revision};if(group==="new")await post("/messages/groups",body);else await put(`/messages/groups/${group.id}`,body);onSaved();}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
|
||||
return <><Modal title={group==="new"?"Neue Nachrichtengruppe":"Nachrichtengruppe bearbeiten"} onClose={close}><form className="admin-dialog-body" onSubmit={save}><ErrorMessage error={error}/><fieldset disabled={busy}><Field label="Bezeichnung"><input required maxLength={100} value={name} onChange={e=>setName(e.target.value)}/></Field><h3>Zugeordnete Gruppen</h3><p className="form-hint">Transporteure dieser Gruppen und ihrer Untergruppen werden bei der Empfängerauswahl berücksichtigt.</p><div className="message-recipients">{available.map(g=><label className="admin-check" key={g.id}><input type="checkbox" checked={selected.includes(g.id)} onChange={e=>setSelected(v=>e.target.checked?[...v,g.id]:v.filter(id=>id!==g.id))}/>{g.label}</label>)}</div><div className="admin-row-actions"><button type="button" className="button" onClick={close}>Abbrechen</button><button className="button primary">Speichern</button></div></fieldset></form></Modal>{dialog}</>;
|
||||
}
|
||||
function MessageGroupMembers({group,onClose}:{group:Group;onClose:()=>void}){
|
||||
const [items,setItems]=useState<Member[]>([]),[q,setQ]=useState(""),[only,setOnly]=useState(true),[busy,setBusy]=useState(false),[reload,setReload]=useState(0),[error,setError]=useState("");
|
||||
useEffect(()=>{let live=true;setBusy(true);request<Member[]>(`/messages/groups/${group.id}/members`).then(v=>{if(live)setItems(v);}).catch(e=>{if(live)setError(e.message);}).finally(()=>{if(live)setBusy(false);});return()=>{live=false;};},[group.id,reload]);
|
||||
async function change(m:Member){setBusy(true);setError("");try{await put(`/messages/groups/${group.id}/members/${m.id}`,{revision:m.revision,member:!m.member});setReload(v=>v+1);}catch(e){setError((e as Error).message);}finally{setBusy(false);}}
|
||||
return <Modal title={`Zuordnungsliste: ${group.name}`} onClose={()=>{if(!busy)onClose();}} wide><div className="admin-dialog-body"><ErrorMessage error={error}/><div className="admin-toolbar"><Field label="Transporteur suchen"><input value={q} onChange={e=>setQ(e.target.value)}/></Field><label className="admin-check"><input type="checkbox" checked={only} onChange={e=>setOnly(e.target.checked)}/>Nur direkt zugeordnete Transporteure</label></div><p className="form-hint">Zusätzliche Empfänger aus verknüpften Gruppen werden in der Versandvorschau angezeigt.</p><div className="message-recipients"><table className="admin-table"><tbody>{items.filter(i=>(!only||i.member)&&i.label.toLowerCase().includes(q.toLowerCase())).map(m=><tr key={m.id}><td>{m.label}</td><td><button className="button" disabled={busy} onClick={()=>void change(m)}>{m.member?"Zuordnung entfernen":"Zuordnen"}</button></td></tr>)}</tbody></table></div><button className="button" disabled={busy} onClick={onClose}>Schließen</button></div></Modal>;
|
||||
}
|
||||
Reference in new issue
Block a user