diff --git a/web/src/components/Dashboard.tsx b/web/src/components/Dashboard.tsx new file mode 100644 index 0000000..f8e435b --- /dev/null +++ b/web/src/components/Dashboard.tsx @@ -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 }; + +export default function Dashboard() { + const [stats, setStats] = useState(null); + const [health, setHealth] = useState(null); + const [error, setError] = useState(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 ( +
+ + {error &&

{error}

} + {stats && ( +
+
+ {stats.conversations}Conversations +
+
+ {stats.tasks}Tasks +
+
+ {stats.documents}Documents +
+
+ )} + {health && ( + + + + + + + + + + {Object.entries(health.components).map(([name, c]) => ( + + + + + + ))} + +
ServiceStatusms
{name}{c.status}{c.response_time_ms}
+ )} + + + +
+ ); +} diff --git a/web/src/components/OrdersWidget.tsx b/web/src/components/OrdersWidget.tsx new file mode 100644 index 0000000..f6d2acc --- /dev/null +++ b/web/src/components/OrdersWidget.tsx @@ -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(null); + const [error, setError] = useState(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 ( +
+

Bestellungen

+ {error &&

{error}

} + {orders && orders.length === 0 &&

Keine Bestellungen.

} + {orders && orders.length > 0 && ( +
    + {orders.map((o) => ( +
  • {formatOrder(o)}
  • + ))} +
+ )} +
+ ); +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..febbfb1 --- /dev/null +++ b/web/src/index.css @@ -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; }