Stand vor Einführung des Nacht-Agenten

This commit is contained in:
Kundencenter 2026-09-27 00:51:32 +02:00
commit 4763548bfb
168 changed files with 12726 additions and 0 deletions

View file

@ -0,0 +1,33 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
import { api, errMsg } from '@/lib/api';
import { useSession } from '@/lib/session';
import { Alert, Empty, fmt, OrderStatusBadge, type Price, PriceText } from '@/components/ui';
interface O { id: string; number: string; orgName: string; status: string; createdAt: string; failureNote: string | null; failureAmbiguous: boolean; note: string | null; items: { id: string; quantity: number; contractId: string | null; snapshot: Price & { name: string; customerType: string } }[] }
export default function Bestellungen() {
const { can, me } = useSession(); const staff = can('orders.read'); const approver = can('orders.approve');
const [list, setList] = useState<O[] | null>(null); const [msg, setMsg] = useState<{ k: 'ok' | 'err'; t: string } | null>(null); const [busy, setBusy] = useState(''); const [chk, setChk] = useState<Record<string, boolean>>({});
const load = useCallback(() => api<O[]>('GET', '/orders').then(setList).catch((e) => setMsg({ k: 'err', t: errMsg(e) })), []);
useEffect(() => { void load(); const t = setInterval(() => void load(), 5000); return () => clearInterval(t); }, [load]);
async function act(o: O, path: string, body?: unknown, ok = 'Erledigt.') { setBusy(o.id); try { await api('POST', path, body); setMsg({ k: 'ok', t: ok }); void load(); } catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } finally { setBusy(''); } }
const canOrder = !!me && (staff ? can('orders.write') : me.organizations.some((o) => o.role !== 'member'));
return (<>
<div className="row between"><h1>Bestellungen</h1>{canOrder && <Link className="btn primary" href="/bestellen">Neue Bestellung</Link>}</div>
{msg && <Alert kind={msg.k}>{msg.t}</Alert>}
{list === null ? <p className="muted" role="status">Wird geladen …</p> : list.length === 0 ? <div className="card"><Empty title="Keine Bestellungen" /></div> : list.map((o) => (
<div className="card" key={o.id}><div className="row between"><h2 style={{ margin: 0 }}><span className="mono">{o.number}</span>{staff && <span className="muted"> · {o.orgName}</span>}</h2><OrderStatusBadge value={o.status} /></div>
<p className="small muted">Bestellt am {fmt(o.createdAt)}{o.note ? ` · „${o.note}“` : ''}</p>
{o.items.map((i) => <div key={i.id} style={{ marginBottom: 8 }}><strong>{i.quantity} × {i.snapshot.name}</strong>{i.contractId && <> · <Link href={`/vertraege/${i.contractId}`}>Vertrag</Link></>}<div className="small"><PriceText p={i.snapshot} priv={i.snapshot.customerType === 'private'} /></div></div>)}
{o.status === 'failed' && o.failureNote && <Alert kind="err">{o.failureNote}</Alert>}
{o.status === 'provisioning' && <p className="small muted" role="status">Die Bereitstellung läuft im Hintergrund. Diese Seite aktualisiert sich automatisch.</p>}
<div className="row">
{approver && o.status === 'pending_approval' && <><button className="btn primary" disabled={busy === o.id} onClick={() => act(o, `/admin/orders/${o.id}/approve`, undefined, 'Bestellung freigegeben, die Bereitstellung wurde gestartet.')}>Freigeben</button>
<button className="btn danger" disabled={busy === o.id} onClick={() => act(o, `/admin/orders/${o.id}/reject`, {}, 'Bestellung abgelehnt.')}>Ablehnen</button></>}
{approver && o.status === 'failed' && <>{o.failureAmbiguous && <label className="row small"><input type="checkbox" style={{ width: 20, minHeight: 20 }} checked={!!chk[o.id]} onChange={(e) => setChk({ ...chk, [o.id]: e.target.checked })} />Ich habe beim Anbieter geprüft, dass nichts angelegt wurde</label>}
<button className="btn" disabled={busy === o.id || (o.failureAmbiguous && !chk[o.id])} onClick={() => act(o, `/admin/orders/${o.id}/retry`, { confirmChecked: !!chk[o.id] }, 'Die Bereitstellung wurde erneut gestartet.')}>Erneut versuchen</button></>}
{(o.status === 'pending_approval' || (can('orders.write') && ['approved', 'failed'].includes(o.status))) && <button className="btn" disabled={busy === o.id} onClick={() => act(o, `/orders/${o.id}/cancel`, undefined, 'Bestellung storniert.')}>{staff ? 'Stornieren' : 'Zurückziehen'}</button>}
</div></div>))}
</>);
}