import 'package:flutter/material.dart'; import '../../../core/network/api_client.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/glass_container.dart'; import '../../../core/widgets/status_badge.dart'; import '../models/service_setting_dto.dart'; import '../repositories/admin_repository.dart'; import '../widgets/live_log_console.dart'; class ServiceDetailScreen extends StatefulWidget { final String serviceName; final ApiClient apiClient; final AdminRepository? repository; const ServiceDetailScreen({ super.key, required this.serviceName, required this.apiClient, this.repository, }); @override State createState() => _ServiceDetailScreenState(); } class _ServiceDetailScreenState extends State { late final AdminRepository _repository; bool _isLoading = true; bool _isSaving = false; String _error = ''; List _settings = []; final Map _controllers = {}; @override void initState() { super.initState(); _repository = widget.repository ?? AdminRepository(apiClient: widget.apiClient); _fetchServiceDetails(); } @override void dispose() { for (var ctrl in _controllers.values) { ctrl.dispose(); } super.dispose(); } Future _fetchServiceDetails() async { try { final groupedSettings = await _repository.fetchSettings(); final serviceSettings = groupedSettings[widget.serviceName] ?? []; setState(() { _settings = serviceSettings; for (var s in _settings) { final key = s.key; final val = s.value; if (!_controllers.containsKey(key)) { _controllers[key] = TextEditingController(text: val); } else { _controllers[key]!.text = val; } } _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } Future _saveSettings() async { setState(() => _isSaving = true); try { final payload = {}; _controllers.forEach((k, v) { final text = v.text.trim(); if (text.toLowerCase() == 'true') { payload[k] = true; } else if (text.toLowerCase() == 'false') { payload[k] = false; } else if (int.tryParse(text) != null) { payload[k] = int.parse(text); } else if (double.tryParse(text) != null) { payload[k] = double.parse(text); } else { payload[k] = text; } }); await _repository.updateServiceSettings(widget.serviceName, payload); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.check_circle_outline, color: Colors.black), const SizedBox(width: 8), Expanded( child: Text( 'Einstellungen für ${widget.serviceName} gespeichert & via MQTT synchronisiert.', style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600), ), ), ], ), backgroundColor: AppTheme.primaryEmerald, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Fehler beim Speichern: $e'), backgroundColor: AppTheme.accentRed, behavior: SnackBarBehavior.floating, ), ); } } finally { if (mounted) setState(() => _isSaving = false); } } String _formatLabel(String key) { // Strip the "Logging.Channel." prefix for display - the section header already says "Logging-Kanäle", // repeating it on every single chip label added visual noise without any extra information. final withoutChannelPrefix = key.startsWith('Logging.Channel.') ? key.substring('Logging.Channel.'.length) : key; return withoutChannelPrefix .replaceAll(RegExp(r'(? _categoryOrder = ['Logging-Kanäle', 'Umschalter', 'Zahlenwerte', 'Text']; String _categoryFor(ServiceSettingDto s) { if (s.key.startsWith('Logging.Channel.')) return 'Logging-Kanäle'; final type = s.type.toLowerCase(); final looksBoolean = type == 'bool' || s.value.toLowerCase() == 'true' || s.value.toLowerCase() == 'false'; if (looksBoolean) return 'Umschalter'; final looksNumeric = type == 'int' || type == 'double' || type == 'number' || type == 'decimal'; if (looksNumeric) return 'Zahlenwerte'; return 'Text'; } Map> get _groupedSettings { final groups = >{}; for (final s in _settings) { groups.putIfAbsent(_categoryFor(s), () => []).add(s); } return groups; } Widget _buildSectionHeader(String title, IconData icon) { return Padding( padding: const EdgeInsets.only(bottom: 10, top: 4), child: Row( children: [ Icon(icon, size: 15, color: AppTheme.textMuted), const SizedBox(width: 6), Text( title, style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: AppTheme.textMuted, letterSpacing: 0.4), ), ], ), ); } /// Compact toggle "chip" for a single boolean setting - used for logging channels, which can easily number /// a dozen+ per service, so a full-width `SwitchListTile` per entry (the previous, only, layout for every /// setting regardless of category or count) made the card feel "gequetscht"/cramped and pushed the actually /// important numeric settings far down the page. Widget _buildToggleChip(ServiceSettingDto s, TextEditingController controller) { final boolVal = controller.text.toLowerCase() == 'true'; return Tooltip( message: s.description.isNotEmpty ? s.description : _formatLabel(s.key), triggerMode: TooltipTriggerMode.tap, textStyle: const TextStyle(color: Colors.white, fontSize: 12), decoration: BoxDecoration( color: AppTheme.cardSurface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppTheme.glassBorder), ), child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () => setState(() => controller.text = (!boolVal).toString()), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: boolVal ? AppTheme.primaryEmerald.withValues(alpha: 0.15) : AppTheme.glassSurface, borderRadius: BorderRadius.circular(20), border: Border.all(color: boolVal ? AppTheme.primaryEmerald.withValues(alpha: 0.5) : AppTheme.glassBorder), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( boolVal ? Icons.check_circle : Icons.circle_outlined, size: 14, color: boolVal ? AppTheme.primaryEmerald : AppTheme.textMuted, ), const SizedBox(width: 6), Text( _formatLabel(s.key), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: boolVal ? Colors.white : AppTheme.textMuted, ), ), ], ), ), ), ); } Widget _buildSwitchSetting(ServiceSettingDto s, TextEditingController controller) { final boolVal = controller.text.toLowerCase() == 'true'; return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(12), border: Border.all(color: boolVal ? AppTheme.primaryEmerald.withValues(alpha: 0.4) : AppTheme.glassBorder), ), child: SwitchListTile( contentPadding: EdgeInsets.zero, title: Text(_formatLabel(s.key), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), subtitle: s.description.isNotEmpty ? Text(s.description, style: TextStyle(fontSize: 11, color: AppTheme.textMuted)) : null, value: boolVal, activeThumbColor: AppTheme.primaryEmerald, activeTrackColor: AppTheme.primaryEmerald.withValues(alpha: 0.3), onChanged: (val) => setState(() => controller.text = val.toString()), ), ); } Widget _buildTextFieldSetting(ServiceSettingDto s, TextEditingController controller, {required bool isNumeric}) { return Padding( padding: const EdgeInsets.only(bottom: 14), child: TextField( controller: controller, keyboardType: isNumeric ? const TextInputType.numberWithOptions(decimal: true) : TextInputType.text, style: const TextStyle(color: Colors.white), decoration: InputDecoration( labelText: _formatLabel(s.key), helperText: s.description.isNotEmpty ? s.description : null, helperMaxLines: 2, prefixIcon: Icon( isNumeric ? Icons.numbers_outlined : Icons.tune_outlined, size: 18, color: AppTheme.primaryEmerald, ), ), ), ); } List _buildGroupedSettingsSections() { final grouped = _groupedSettings; final widgets = []; for (final category in _categoryOrder) { final items = grouped[category]; if (items == null || items.isEmpty) continue; widgets.add(_buildSectionHeader( '$category (${items.length})', switch (category) { 'Logging-Kanäle' => Icons.terminal_rounded, 'Umschalter' => Icons.toggle_on_outlined, 'Zahlenwerte' => Icons.numbers_outlined, _ => Icons.tune_outlined, }, )); if (category == 'Logging-Kanäle') { final chips = []; for (final s in items) { final controller = _controllers[s.key]; if (controller != null) chips.add(_buildToggleChip(s, controller)); } widgets.add(Wrap(spacing: 8, runSpacing: 8, children: chips)); } else if (category == 'Umschalter') { for (final s in items) { final controller = _controllers[s.key]; if (controller != null) widgets.add(_buildSwitchSetting(s, controller)); } } else { for (final s in items) { final controller = _controllers[s.key]; if (controller != null) { widgets.add(_buildTextFieldSetting(s, controller, isNumeric: category == 'Zahlenwerte')); } } } widgets.add(const SizedBox(height: 14)); } return widgets; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppTheme.darkBackground, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, title: Text('${widget.serviceName} Details', style: const TextStyle(fontWeight: FontWeight.bold)), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _error.isNotEmpty ? Center(child: Text(_error, style: const TextStyle(color: Colors.red))) : SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GlassContainer( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Einstellungen & Konfiguration', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), StatusBadge(label: 'MQTT Sync', color: AppTheme.primaryEmerald), ], ), const SizedBox(height: 16), const Divider(color: Colors.white10), const SizedBox(height: 16), if (_settings.isEmpty) const Text('Keine spezifischen Einstellungen gefunden.') else ..._buildGroupedSettingsSections(), const SizedBox(height: 12), if (_settings.isNotEmpty) SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isSaving ? null : _saveSettings, icon: _isSaving ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black)) : const Icon(Icons.save_outlined), label: Text( _isSaving ? 'Speichere & Sende via MQTT...' : 'Einstellungen Speichern', style: const TextStyle(fontWeight: FontWeight.bold), ), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryEmerald, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], ), ), const SizedBox(height: 24), LiveLogConsole( serviceName: widget.serviceName, apiClient: widget.apiClient, ), ], ), ), ); } }