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:
parent
ebbeb0b42c
commit
bd73effc73
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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; }
|
||||
Loading…
Reference in New Issue