223 lines
6.4 KiB
Dart
223 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../app/load_state.dart';
|
||
import '../models/stats_summary.dart';
|
||
import '../providers/providers.dart';
|
||
import '../widgets/load_error_view.dart';
|
||
|
||
/// Экран просмотра статистики.
|
||
/// Показывает сводку по группам за выбранный период.
|
||
class StatsScreen extends ConsumerStatefulWidget {
|
||
const StatsScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<StatsScreen> createState() => _StatsScreenState();
|
||
}
|
||
|
||
class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||
int _days = 7;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_load();
|
||
}
|
||
|
||
Future<void> _load() async {
|
||
await ref.read(statsProvider.notifier).load(days: _days);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final statsState = ref.watch(statsProvider);
|
||
final stats = statsState.data;
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('СТАТИСТИКА')),
|
||
body: Column(
|
||
children: [
|
||
// ─── Переключатель периода ───
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
const Text('Период:', style: TextStyle(color: Colors.white54)),
|
||
const SizedBox(width: 12),
|
||
...[1, 7, 14, 30].map(
|
||
(d) => Padding(
|
||
padding: const EdgeInsets.only(right: 8),
|
||
child: ChoiceChip(
|
||
label: Text('$d д.'),
|
||
selected: _days == d,
|
||
selectedColor: Colors.deepOrange,
|
||
onSelected: (_) {
|
||
setState(() => _days = d);
|
||
_load();
|
||
},
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// ─── Содержимое ───
|
||
Expanded(child: _buildContent(statsState, stats.groups)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildContent(
|
||
LoadState<StatsSummary> statsState,
|
||
List<GroupStats> groups,
|
||
) {
|
||
if ((statsState.isIdle || statsState.isLoading) && groups.isEmpty) {
|
||
return const Center(
|
||
child: CircularProgressIndicator(color: Colors.deepOrange),
|
||
);
|
||
}
|
||
|
||
if (statsState.hasError && groups.isEmpty) {
|
||
return LoadErrorView(
|
||
title: 'Не удалось загрузить статистику',
|
||
message: statsState.errorMessage,
|
||
icon: Icons.bar_chart,
|
||
onRetry: _load,
|
||
);
|
||
}
|
||
|
||
if (groups.isEmpty) {
|
||
return const Center(
|
||
child: Text('Нет данных', style: TextStyle(color: Colors.white54)),
|
||
);
|
||
}
|
||
|
||
final hasStatusHeader = statsState.isLoading || statsState.hasError;
|
||
final statusHeaderCount = hasStatusHeader ? 1 : 0;
|
||
|
||
return RefreshIndicator(
|
||
color: Colors.deepOrange,
|
||
onRefresh: _load,
|
||
child: ListView.builder(
|
||
physics: const AlwaysScrollableScrollPhysics(),
|
||
padding: const EdgeInsets.all(12),
|
||
itemCount: groups.length + statusHeaderCount,
|
||
itemBuilder: (context, index) {
|
||
if (hasStatusHeader && index == 0) {
|
||
if (statsState.isLoading) {
|
||
return const Padding(
|
||
padding: EdgeInsets.only(bottom: 12),
|
||
child: LinearProgressIndicator(color: Colors.deepOrange),
|
||
);
|
||
}
|
||
|
||
return LoadErrorBanner(
|
||
title: 'Не удалось обновить статистику',
|
||
message: statsState.errorMessage,
|
||
onRetry: _load,
|
||
);
|
||
}
|
||
|
||
final groupIndex = index - statusHeaderCount;
|
||
return _StatsCard(data: groups[groupIndex]);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Карточка статистики одной группы
|
||
class _StatsCard extends StatelessWidget {
|
||
final GroupStats data;
|
||
|
||
const _StatsCard({required this.data});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
data.name,
|
||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_StatRow(
|
||
icon: Icons.touch_app,
|
||
label: 'Всего команд',
|
||
value: data.totalCommands.toString(),
|
||
),
|
||
const SizedBox(height: 4),
|
||
_StatRow(
|
||
icon: Icons.power_settings_new,
|
||
label: 'Включений',
|
||
value: data.togglesOn.toString(),
|
||
color: Colors.green,
|
||
),
|
||
const SizedBox(height: 4),
|
||
_StatRow(
|
||
icon: Icons.power_off,
|
||
label: 'Выключений',
|
||
value: data.togglesOff.toString(),
|
||
color: Colors.redAccent,
|
||
),
|
||
if (data.estimatedHours != null) ...[
|
||
const SizedBox(height: 4),
|
||
_StatRow(
|
||
icon: Icons.access_time,
|
||
label: 'Примерное время работы',
|
||
value: data.formattedEstimatedHours,
|
||
color: Colors.amber,
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Строка с иконкой, меткой и значением
|
||
class _StatRow extends StatelessWidget {
|
||
final IconData icon;
|
||
final String label;
|
||
final String value;
|
||
final Color? color;
|
||
|
||
const _StatRow({
|
||
required this.icon,
|
||
required this.label,
|
||
required this.value,
|
||
this.color,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Icon(icon, size: 16, color: color ?? Colors.white38),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
label,
|
||
style: const TextStyle(fontSize: 13, color: Colors.white54),
|
||
),
|
||
),
|
||
Text(
|
||
value,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: color ?? Colors.white70,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|