ui-nyla/src/pages/Dashboard.tsx
ValueOn AG 9b99020686 feat(billing): Nutzerhinweise bei leerem Budget + Mandats-Mail (402/SSE)
Gateway
- InsufficientBalanceException: billingModel, userAction (TOP_UP_SELF /
  CONTACT_MANDATE_ADMIN), DE/EN-Texte, toClientDict(), fromBalanceCheck()
- HTTP 402 + JSON detail für globale API-Fehlerbehandlung
- AI/Chatbot: vor Raise ggf. E-Mail an BillingSettings.notifyEmails
  (PREPAY_MANDATE, Throttle 1h/Mandat) via billingExhaustedNotify
- Agent-Loop & Workspace-Route: SSE-ERROR mit strukturiertem Billing-Payload
- datamodelBilling: notifyEmails-Doku für Pool-Alerts
frontend_nyla
- useWorkspace: SSE onError für INSUFFICIENT_BALANCE mit messageDe/En
  und Hinweis auf Billing-Pfad bei TOP_UP_SELF
2026-03-21 01:34:47 +01:00

126 lines
4.1 KiB
TypeScript

/**
* Dashboard Page
*
* System-Übersicht für den User.
* Zeigt alle verfügbaren Feature-Instanzen pro Mandant als Karten an.
* Daten kommen vom Backend via GET /api/navigation.
*/
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import useNavigation from '../hooks/useNavigation';
import type { NavigationMandate, MandateFeature, FeatureInstance as NavFeatureInstance } from '../hooks/useNavigation';
import { getPageIcon } from '../config/pageRegistry';
import { FaArrowRight, FaBuilding } from 'react-icons/fa';
import styles from './Dashboard.module.css';
// =============================================================================
// INSTANCE CARD
// =============================================================================
interface InstanceCardProps {
instance: NavFeatureInstance;
feature: MandateFeature;
}
const InstanceCard: React.FC<InstanceCardProps> = ({ instance, feature }) => {
// Ersten verfügbaren View-Pfad vom Backend nehmen
const targetPath = instance.views.length > 0 ? instance.views[0].uiPath : undefined;
if (!targetPath) return null;
return (
<Link to={targetPath} className={styles.instanceCard}>
<div className={styles.cardIcon}>
{getPageIcon(feature.uiComponent)}
</div>
<div className={styles.cardContent}>
<div className={styles.cardHeader}>
<span className={styles.featureLabel}>{feature.uiLabel}</span>
</div>
<h3 className={styles.instanceLabel}>{instance.uiLabel}</h3>
</div>
<div className={styles.cardArrow}>
<FaArrowRight />
</div>
</Link>
);
};
// =============================================================================
// DASHBOARD PAGE
// =============================================================================
export const DashboardPage: React.FC = () => {
const { dynamicBlock, loading } = useNavigation();
// Alle Mandate und deren Features/Instanzen aus der Navigation
const mandates: NavigationMandate[] = dynamicBlock?.mandates || [];
// Gesamtzahl Instanzen und Mandate berechnen
let totalInstances = 0;
const totalMandates = mandates.length;
mandates.forEach(m => m.features.forEach(f => {
totalInstances += f.instances.length;
}));
if (loading) {
return (
<div className={styles.dashboard}>
<header className={styles.header}>
<h1>Übersicht</h1>
<p className={styles.subtitle}>Lade...</p>
</header>
</div>
);
}
if (totalInstances === 0) {
return <Navigate to="/store" replace />;
}
return (
<div className={styles.dashboard}>
<header className={styles.header}>
<h1>Übersicht</h1>
<p className={styles.subtitle}>
Du hast Zugriff auf {totalInstances} Feature-Instanz{totalInstances !== 1 ? 'en' : ''} in {totalMandates} Mandant{totalMandates !== 1 ? 'en' : ''}.
</p>
</header>
<main className={styles.content}>
{mandates
.filter(mandate => mandate.features.some(f => f.instances.length > 0))
.map(mandate => {
// Alle Instanzen dieses Mandats sammeln (flach, ohne Feature-Gruppierung)
const mandateInstances: { instance: NavFeatureInstance; feature: MandateFeature }[] = [];
for (const feature of mandate.features) {
for (const instance of feature.instances) {
mandateInstances.push({ instance, feature });
}
}
return (
<section key={mandate.id} className={styles.featureSection}>
<h2 className={styles.sectionTitle}>
<FaBuilding />
<span>{mandate.uiLabel}</span>
</h2>
<div className={styles.instanceGrid}>
{mandateInstances.map(({ instance, feature }) => (
<InstanceCard
key={instance.id}
instance={instance}
feature={feature}
/>
))}
</div>
</section>
);
})}
</main>
</div>
);
};
export default DashboardPage;