258 lines
11 KiB
Dart
258 lines
11 KiB
Dart
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';
|
|
|
|
class ServiceDetailScreen extends StatefulWidget {
|
|
final String serviceName;
|
|
final ApiClient apiClient;
|
|
|
|
const ServiceDetailScreen({super.key, required this.serviceName, required this.apiClient});
|
|
|
|
@override
|
|
State<ServiceDetailScreen> createState() => _ServiceDetailScreenState();
|
|
}
|
|
|
|
class _ServiceDetailScreenState extends State<ServiceDetailScreen> {
|
|
bool _isLoading = true;
|
|
bool _isSaving = false;
|
|
String _error = '';
|
|
List<dynamic> _settings = [];
|
|
final Map<String, TextEditingController> _controllers = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchServiceDetails();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
for (var ctrl in _controllers.values) {
|
|
ctrl.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _fetchServiceDetails() async {
|
|
try {
|
|
final res = await widget.apiClient.get('/api/v1/admin/settings');
|
|
if (res.statusCode == 200 && res.data != null) {
|
|
final groupedSettings = res.data as Map<String, dynamic>;
|
|
final serviceSettings = groupedSettings[widget.serviceName] ?? [];
|
|
|
|
setState(() {
|
|
_settings = serviceSettings;
|
|
for (var s in _settings) {
|
|
final key = s['key']?.toString() ?? '';
|
|
final val = s['value']?.toString() ?? '';
|
|
if (!_controllers.containsKey(key)) {
|
|
_controllers[key] = TextEditingController(text: val);
|
|
} else {
|
|
_controllers[key]!.text = val;
|
|
}
|
|
}
|
|
_isLoading = false;
|
|
});
|
|
} else {
|
|
throw Exception('Failed to load settings');
|
|
}
|
|
} catch (e) {
|
|
setState(() {
|
|
_error = e.toString();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _saveSettings() async {
|
|
setState(() => _isSaving = true);
|
|
try {
|
|
final payload = <String, String>{};
|
|
_controllers.forEach((k, v) {
|
|
payload[k] = v.text;
|
|
});
|
|
|
|
final res = await widget.apiClient.put(
|
|
'/api/v1/admin/settings/${widget.serviceName}',
|
|
data: payload,
|
|
);
|
|
|
|
if (res.statusCode == 200 || res.statusCode == 204) {
|
|
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)),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
throw Exception('Server returned status code ${res.statusCode}');
|
|
}
|
|
} 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) {
|
|
return key
|
|
.replaceAll(RegExp(r'(?<!^)(?=[A-Z])'), ' ')
|
|
.replaceAll('Minutes', '(Minuten)')
|
|
.replaceAll('Seconds', '(Sekunden)')
|
|
.replaceAll('Hours', '(Stunden)')
|
|
.replaceAll('Days', '(Tage)')
|
|
.replaceAll('Limit', 'Grenzwert')
|
|
.replaceAll('Period', 'Periode')
|
|
.replaceAll('Percentage', '(%)')
|
|
.replaceAll('Multiplier', 'Multiplikator');
|
|
}
|
|
|
|
@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
|
|
..._settings.map((s) {
|
|
final key = s['key']?.toString() ?? '';
|
|
final desc = s['description']?.toString() ?? '';
|
|
final controller = _controllers[key];
|
|
if (controller == null) return const SizedBox.shrink();
|
|
|
|
final isBoolean = controller.text.toLowerCase() == 'true' || controller.text.toLowerCase() == 'false';
|
|
|
|
if (isBoolean) {
|
|
final boolVal = controller.text.toLowerCase() == 'true';
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.glassSurface,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: AppTheme.glassBorder),
|
|
),
|
|
child: SwitchListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(_formatLabel(key), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
|
subtitle: desc.isNotEmpty ? Text(desc, style: TextStyle(fontSize: 11, color: AppTheme.textMuted)) : null,
|
|
value: boolVal,
|
|
activeThumbColor: AppTheme.primaryEmerald,
|
|
onChanged: (val) => setState(() => controller.text = val.toString()),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 14),
|
|
child: TextField(
|
|
controller: controller,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: InputDecoration(
|
|
labelText: _formatLabel(key),
|
|
helperText: desc.isNotEmpty ? desc : null,
|
|
helperMaxLines: 2,
|
|
prefixIcon: Icon(Icons.tune_outlined, size: 18, color: AppTheme.primaryEmerald),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
|
|
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),
|
|
GlassContainer(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Statistiken', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 16),
|
|
const Text('Live-Statistiken werden noch implementiert...', style: TextStyle(fontStyle: FontStyle.italic, color: Colors.white54)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|