Files
RAPPORT-HOST/src/views/Dashboard.jsx
T
karim 6290475ea3 Initial: RAPPORT-HOST Iteration 1 (proprietär)
Kommerzielle Hosting-/Abo-Plattform für Rapport-Instanzen.

- React-Frontend (Vite/JSX): Landing, Register, Login, Plans, Dashboard
- Node/Express-Backend: Auth (bcrypt+JWT), Stripe-Billing, Provisioning
- HOST-Postgres-Schema: accounts, subscriptions, instances
- Provisioning-Interface + Modell-A-Adapter (Studio im geteilten Stack)
- MOCK-Modus: voller End-to-End-Flow ohne Stripe/Rapport-Stack testbar
- Idempotentes Fulfillment (Upsert auf stripe_subscription_id)
- docker-compose für lokale host-db; identisch auf Hetzner deploybar

E2E lokal verifiziert: Register -> Checkout(mock) -> Instanz -> Idempotenz.

Lizenz: proprietär (kein AGPL-Code eingebunden, nur Netzwerk-API zur Familie).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:37:33 +02:00

66 lines
2.7 KiB
React

import React, { useState, useEffect } from "react";
import { api, auth } from "../api.js";
export default function Dashboard({ navigate }) {
const [data, setData] = useState(null);
const [err, setErr] = useState("");
const justProvisioned = new URLSearchParams(window.location.search).get("provisioned") === "1";
useEffect(() => {
api.me().then(setData).catch((e) => setErr(e.message));
}, []);
const logout = () => { auth.clear(); navigate("/"); };
if (err) return <div className="center"><div className="card card-sm"><div className="err">{err}</div><button className="primary" onClick={logout}>Abmelden</button></div></div>;
if (!data) return <div className="center"><div className="muted">Lädt</div></div>;
const { account, subscription, instance } = data;
return (
<div className="wrap">
<div className="nav">
<div className="brand" style={{ cursor: "pointer" }} onClick={() => navigate("/")}>RAPPORT</div>
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<span className="muted" style={{ fontSize: 12 }}>{account.email}</span>
<button className="ghost" onClick={logout}>Abmelden</button>
</div>
</div>
{justProvisioned && <div className="ok">Zahlung erfolgreich Ihre Instanz wird bereitgestellt.</div>}
<h1 style={{ fontSize: 28 }}>Dashboard</h1>
<div className="card" style={{ marginTop: 16 }}>
<label>Abo</label>
{subscription ? (
<div style={{ fontSize: 14 }}>
Plan <b>{subscription.plan}</b> · Status <b>{subscription.status}</b>
{subscription.current_period_end && (
<span className="muted"> · läuft bis {new Date(subscription.current_period_end).toLocaleDateString("de-CH")}</span>
)}
</div>
) : (
<div>
<p className="muted" style={{ fontSize: 13 }}>Noch kein aktives Abo.</p>
<button className="primary" style={{ width: "auto", padding: "10px 22px" }} onClick={() => navigate("/plans")}>Abo wählen</button>
</div>
)}
</div>
{instance && (
<div className="card" style={{ marginTop: 16 }}>
<label>Ihre Instanz</label>
<div style={{ fontSize: 14, marginBottom: 12 }}>
<span className="muted">Status:</span> <b>{instance.status}</b>
</div>
<a className="primary" style={{ display: "inline-block", width: "auto", padding: "11px 24px", textDecoration: "none", textAlign: "center" }} href={instance.instance_url} target="_blank" rel="noreferrer">
Rapport öffnen
</a>
<div className="muted" style={{ fontSize: 11, marginTop: 10 }}>{instance.instance_url}</div>
</div>
)}
</div>
);
}