feat: add Bestellungen dashboard widget

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V57jSQPqwkGG8BuAXg59X5
This commit is contained in:
Jonny 2026-09-13 19:15:17 +02:00
parent ebbeb0b42c
commit bd73effc73
3 changed files with 159 additions and 0 deletions

View File

@ -0,0 +1,82 @@
import { useEffect, useState } from "react";
import { apiFetch } from "../api";
import CalendarWidget from "./CalendarWidget";
import EmailWidget from "./EmailWidget";
import OrdersWidget from "./OrdersWidget";
type Stats = { conversations: number; tasks: number; documents: number };
type HealthComponent = { status: string; response_time_ms: number; error?: string };
type Health = { status: string; components: Record<string, HealthComponent> };
export default function Dashboard() {
const [stats, setStats] = useState<Stats | null>(null);
const [health, setHealth] = useState<Health | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function load() {
setLoading(true);
setError(null);
try {
const [statsRes, healthRes] = await Promise.all([
apiFetch("/api/v1/admin/stats"),
apiFetch("/api/v1/admin/health/detailed"),
]);
setStats(await statsRes.json());
setHealth(await healthRes.json());
} catch {
setError("Server nicht erreichbar.");
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
return (
<div className="dashboard">
<button onClick={load} disabled={loading}>
Aktualisieren
</button>
{error && <p className="error">{error}</p>}
{stats && (
<div className="stat-cards">
<div className="stat-card">
<span>{stats.conversations}</span>Conversations
</div>
<div className="stat-card">
<span>{stats.tasks}</span>Tasks
</div>
<div className="stat-card">
<span>{stats.documents}</span>Documents
</div>
</div>
)}
{health && (
<table className="health-table">
<thead>
<tr>
<th>Service</th>
<th>Status</th>
<th>ms</th>
</tr>
</thead>
<tbody>
{Object.entries(health.components).map(([name, c]) => (
<tr key={name}>
<td>{name}</td>
<td className={c.status === "ok" ? "badge-ok" : "badge-error"}>{c.status}</td>
<td>{c.response_time_ms}</td>
</tr>
))}
</tbody>
</table>
)}
<CalendarWidget />
<EmailWidget />
<OrdersWidget />
</div>
);
}

View File

@ -0,0 +1,48 @@
import { useEffect, useState } from "react";
import { apiFetch } from "../api";
type Order = { id: number; customer: string; description: string; status: string; due_date: string | null };
export default function OrdersWidget() {
const [orders, setOrders] = useState<Order[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function load() {
try {
const response = await apiFetch("/api/v1/orders");
if (!response.ok) {
setError("Bestellungen nicht verfuegbar");
return;
}
const data = await response.json();
setOrders(data.orders);
} catch {
setError("Bestellungen nicht verfuegbar");
}
}
load();
}, []);
function formatOrder(o: Order): string {
const due = o.due_date
? ` (fällig ${new Date(o.due_date).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" })})`
: "";
return `[${o.status}] ${o.customer}: ${o.description}${due}`;
}
return (
<div className="orders-widget">
<h3>Bestellungen</h3>
{error && <p className="error">{error}</p>}
{orders && orders.length === 0 && <p>Keine Bestellungen.</p>}
{orders && orders.length > 0 && (
<ul>
{orders.map((o) => (
<li key={o.id}>{formatOrder(o)}</li>
))}
</ul>
)}
</div>
);
}

29
web/src/index.css Normal file
View File

@ -0,0 +1,29 @@
body { font-family: system-ui, sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
.login { display: flex; flex-direction: column; gap: 0.75rem; max-width: 320px; margin: 4rem auto; padding: 2rem; background: #1e293b; border-radius: 8px; }
.login input, .login button { padding: 0.5rem; font-size: 1rem; }
.error { color: #f87171; }
.nav { display: flex; gap: 0.5rem; padding: 1rem; background: #1e293b; }
.nav button { padding: 0.5rem 1rem; }
.weather-widget { margin-left: auto; font-size: 0.9rem; color: #94a3b8; align-self: center; }
.chat { display: flex; flex-direction: column; height: calc(100vh - 64px); padding: 1rem; box-sizing: border-box; }
.chat-header { display: flex; justify-content: flex-end; margin-bottom: 0.5rem; }
.new-chat-button { padding: 0.4rem 0.75rem; background: #1e293b; color: #e2e8f0; border: 1px solid #334155; border-radius: 6px; cursor: pointer; }
.new-chat-button:disabled { opacity: 0.5; cursor: default; }
.messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 0.5rem; }
.message { padding: 0.5rem 0.75rem; border-radius: 8px; max-width: 70%; }
.message.user { align-self: flex-end; background: #2563eb; }
.message.assistant { align-self: flex-start; background: #334155; }
.message.system { align-self: center; background: #7f1d1d; font-size: 0.875rem; }
.input-row { display: flex; gap: 0.5rem; margin-top: 1rem; }
.input-row input { flex: 1; padding: 0.5rem; }
.mic-button { padding: 0.5rem 0.75rem; }
.mic-button.listening { background: #dc2626; color: white; }
.dashboard { padding: 1rem; }
.stat-cards { display: flex; gap: 1rem; margin: 1rem 0; }
.stat-card { background: #1e293b; padding: 1rem; border-radius: 8px; flex: 1; text-align: center; }
.stat-card span { display: block; font-size: 2rem; font-weight: bold; }
.health-table { width: 100%; border-collapse: collapse; }
.health-table td, .health-table th { padding: 0.5rem; border-bottom: 1px solid #334155; text-align: left; }
.badge-ok { color: #4ade80; }
.badge-error { color: #f87171; }
.orders-widget { margin-top: 1rem; }