Stand vor Einführung des Nacht-Agenten
This commit is contained in:
commit
4763548bfb
168 changed files with 12726 additions and 0 deletions
33
apps/web/src/app/(app)/bestellungen/page.tsx
Normal file
33
apps/web/src/app/(app)/bestellungen/page.tsx
Normal 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>))}
|
||||
</>);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue