Files

225 lines
6.9 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import '../../../core/network/signalr_service.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/glass_container.dart';
import '../models/admin_user_model.dart';
/// Modern KPI Header Card Row for Admin Panel Dashboard Overview.
/// Displays real-time live metrics for Users, Administrators, Microservices Health, and MQTT Bus.
/// Continuously updates live EXCLUSIVELY over SignalR WebSockets (`/hubs/health`).
class AdminKpiHeader extends StatefulWidget {
final List<AdminUserModel> users;
final SignalRService? signalRService;
const AdminKpiHeader({
super.key,
required this.users,
this.signalRService,
});
@override
State<AdminKpiHeader> createState() => _AdminKpiHeaderState();
}
class _AdminKpiHeaderState extends State<AdminKpiHeader> {
int? _totalServices;
int? _onlineServices;
bool _mqttConnected = false;
StreamSubscription<List<Map<String, dynamic>>>? _healthSub;
@override
void initState() {
super.initState();
if (widget.signalRService != null) {
_healthSub = widget.signalRService!.healthStream.listen((data) {
if (mounted && data.isNotEmpty) {
final int total = data.length;
final int online = data.where((item) => item['status']?.toString().toLowerCase() == 'online').length;
setState(() {
_totalServices = total;
_onlineServices = online;
_mqttConnected = online > 0;
});
}
});
}
}
@override
void dispose() {
_healthSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final totalUsers = widget.users.length;
final activeUsers = widget.users.where((u) => u.isActive).length;
final adminCount = widget.users.where((u) => u.role.toLowerCase() == 'admin').length;
final isMobile = MediaQuery.of(context).size.width < 700;
final String servicesValue = _totalServices != null ? '$_onlineServices / $_totalServices' : 'SignalR...';
final String servicesSubtitle = (_onlineServices == _totalServices && _totalServices != null && _totalServices! > 0
? 'Alle Dienste online (WebSocket)'
: (_onlineServices != null ? '$_onlineServices von $_totalServices erreichbar' : 'Verbinde WebSocket...'));
final Color servicesColor = (_onlineServices == _totalServices && _totalServices != null && _totalServices! > 0)
? AppTheme.primaryEmerald
: (_onlineServices != null && _onlineServices! > 0 ? AppTheme.accentCyan : AppTheme.accentRed);
final cards = [
_KpiCard(
title: 'Benutzer Gesamt',
value: totalUsers.toString(),
subtitle: '$activeUsers aktiv • ${totalUsers - activeUsers} gesperrt',
icon: Icons.people_alt_rounded,
accentColor: AppTheme.primaryEmerald,
),
_KpiCard(
title: 'Administratoren',
value: adminCount.toString(),
subtitle: 'Vollzugriff auf System',
icon: Icons.admin_panel_settings_rounded,
accentColor: const Color(0xFFA855F7), // Purple accent
),
_KpiCard(
title: 'Mikrodienste',
value: servicesValue,
subtitle: servicesSubtitle,
icon: Icons.dns_rounded,
accentColor: servicesColor,
showPulse: _onlineServices != null && _onlineServices! > 0,
),
_KpiCard(
title: 'MQTT Live-Bus',
value: _mqttConnected ? 'Aktiv' : 'Offline',
subtitle: _mqttConnected ? 'SignalR & RPC Bereit' : 'Warte auf WebSocket',
icon: Icons.sensors_rounded,
accentColor: _mqttConnected ? const Color(0xFF10B981) : AppTheme.accentRed,
),
];
if (isMobile) {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1.5,
children: cards,
);
}
return Row(
children: cards
.map((card) => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: card,
),
))
.toList(),
);
}
}
class _KpiCard extends StatelessWidget {
final String title;
final String value;
final String subtitle;
final IconData icon;
final Color accentColor;
final bool showPulse;
const _KpiCard({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.accentColor,
this.showPulse = false,
});
@override
Widget build(BuildContext context) {
return GlassContainer(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppTheme.textMuted,
),
),
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.15),
shape: BoxShape.circle,
border: Border.all(color: accentColor.withValues(alpha: 0.3)),
),
child: Icon(icon, size: 16, color: accentColor),
),
],
),
const SizedBox(height: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
letterSpacing: -0.5,
),
),
if (showPulse) ...[
const SizedBox(width: 8),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppTheme.primaryEmerald,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppTheme.primaryEmerald.withValues(alpha: 0.6),
blurRadius: 6,
spreadRadius: 2,
),
],
),
),
],
],
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}