174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { fetchOverview, toggleFlag, type Overview } from "@/lib/api";
|
|
|
|
const STATUS_LABEL: Record<string, string> = {
|
|
ok: "In Ordnung",
|
|
warning: "Nahe am Limit",
|
|
exceeded: "Limit überschritten",
|
|
};
|
|
|
|
const STATUS_COLOR: Record<string, string> = {
|
|
ok: "#2e7d32",
|
|
warning: "#ed6c02",
|
|
exceeded: "#c62828",
|
|
};
|
|
|
|
export default function Page() {
|
|
const [tenantId, setTenantId] = useState("");
|
|
const [overview, setOverview] = useState<Overview | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [toggleError, setToggleError] = useState<string | null>(null);
|
|
|
|
async function load() {
|
|
if (!tenantId.trim()) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const data = await fetchOverview(tenantId.trim());
|
|
setOverview(data);
|
|
} catch (e: any) {
|
|
setError(e.message ?? "Unbekannter Fehler");
|
|
setOverview(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function onToggle(key: string, next: boolean) {
|
|
setToggleError(null);
|
|
try {
|
|
await toggleFlag(tenantId.trim(), key, next);
|
|
await load();
|
|
} catch (e: any) {
|
|
setToggleError(e.message ?? "Umschalten fehlgeschlagen");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main style={{ maxWidth: 800, margin: "0 auto", padding: "2rem 1rem" }}>
|
|
<h1>Lizenz- und Modulverwaltung</h1>
|
|
|
|
<div style={{ display: "flex", gap: "0.5rem", marginBottom: "1.5rem" }}>
|
|
<input
|
|
value={tenantId}
|
|
onChange={(e) => setTenantId(e.target.value)}
|
|
placeholder="Tenant-ID eingeben"
|
|
style={{ flex: 1, padding: "0.5rem", fontSize: "1rem" }}
|
|
/>
|
|
<button onClick={load} disabled={loading} style={{ padding: "0.5rem 1rem" }}>
|
|
{loading ? "Lädt…" : "Anzeigen"}
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<p style={{ color: "#c62828" }} role="alert">
|
|
Fehler: {error}
|
|
</p>
|
|
)}
|
|
|
|
{overview && (
|
|
<>
|
|
<section style={{ background: "white", padding: "1rem", borderRadius: 8, marginBottom: "1.5rem" }}>
|
|
<h2>Lizenzstatus</h2>
|
|
<p>
|
|
<strong>Plan:</strong> {overview.plan}{" "}
|
|
{overview.expired && (
|
|
<span style={{ color: "#c62828", fontWeight: "bold" }}>
|
|
(abgelaufen)
|
|
</span>
|
|
)}
|
|
</p>
|
|
<p>
|
|
<strong>Gültig bis:</strong>{" "}
|
|
{new Date(overview.valid_until).toLocaleDateString("de-DE")}
|
|
</p>
|
|
<p>
|
|
<strong>Freigeschaltete Module:</strong>{" "}
|
|
{overview.modules.length > 0 ? overview.modules.join(", ") : "keine"}
|
|
</p>
|
|
</section>
|
|
|
|
<section style={{ background: "white", padding: "1rem", borderRadius: 8, marginBottom: "1.5rem" }}>
|
|
<h2>Feature-Flags</h2>
|
|
{toggleError && (
|
|
<p style={{ color: "#c62828" }} role="alert">
|
|
{toggleError}
|
|
</p>
|
|
)}
|
|
{overview.flags.length === 0 && <p>Keine Feature-Flags definiert.</p>}
|
|
<ul style={{ listStyle: "none", padding: 0 }}>
|
|
{overview.flags.map((f) => (
|
|
<li
|
|
key={f.key}
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
padding: "0.5rem 0",
|
|
borderBottom: "1px solid #eee",
|
|
}}
|
|
>
|
|
<div>
|
|
<div>{f.key}</div>
|
|
{!f.licensed && f.deny_grund && (
|
|
<div style={{ fontSize: "0.85rem", color: "#c62828" }}>
|
|
{f.deny_grund}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={f.enabled}
|
|
disabled={!f.licensed && !f.enabled}
|
|
onChange={(e) => onToggle(f.key, e.target.checked)}
|
|
/>{" "}
|
|
aktiv
|
|
</label>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
<section style={{ background: "white", padding: "1rem", borderRadius: 8 }}>
|
|
<h2>Nutzungsstand</h2>
|
|
{overview.usage.map((u) => {
|
|
const pct = u.limit > 0 ? Math.min(100, (u.value / u.limit) * 100) : 0;
|
|
return (
|
|
<div key={u.metric} style={{ marginBottom: "1rem" }}>
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
<span>{u.metric}</span>
|
|
<span style={{ color: STATUS_COLOR[u.status] }}>
|
|
{u.value} / {u.limit > 0 ? u.limit : "unbegrenzt"} —{" "}
|
|
{STATUS_LABEL[u.status]}
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
background: "#eee",
|
|
borderRadius: 4,
|
|
height: 8,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: `${u.limit > 0 ? pct : 0}%`,
|
|
background: STATUS_COLOR[u.status],
|
|
height: "100%",
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
</>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|