tasq/lib/screens/reports/widgets/hourly_charts.dart
2026-03-03 07:38:40 +08:00

272 lines
8.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../providers/reports_provider.dart';
import 'report_card_wrapper.dart';
/// Converts 24h hour int to AM/PM string.
String _hourAmPm(int h) {
if (h == 0) return '12AM';
if (h < 12) return '${h}AM';
if (h == 12) return '12PM';
return '${h - 12}PM';
}
/// Vertical bar chart showing tasks created per hour of day (023).
class TasksByHourChart extends ConsumerWidget {
const TasksByHourChart({super.key, this.repaintKey});
final GlobalKey? repaintKey;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncData = ref.watch(tasksByHourReportProvider);
return asyncData.when(
loading: () => ReportCardWrapper(
title: 'Tasks Created by Hour',
isLoading: true,
height: 260,
repaintBoundaryKey: repaintKey,
child: const SizedBox.shrink(),
),
error: (e, _) => ReportCardWrapper(
title: 'Tasks Created by Hour',
error: e.toString(),
repaintBoundaryKey: repaintKey,
child: const SizedBox.shrink(),
),
data: (data) => _build(context, data),
);
}
Widget _build(BuildContext context, List<HourCount> data) {
if (data.isEmpty) {
return ReportCardWrapper(
title: 'Tasks Created by Hour',
repaintBoundaryKey: repaintKey,
child: const SizedBox(
height: 200,
child: Center(child: Text('No data for selected period')),
),
);
}
// Fill all 24 hours
final hourMap = {for (final h in data) h.hour: h.count};
final maxCount = data
.fold<int>(0, (m, e) => e.count > m ? e.count : m)
.toDouble();
final colors = Theme.of(context).colorScheme;
return ReportCardWrapper(
title: 'Tasks Created by Hour',
repaintBoundaryKey: repaintKey,
height: 260,
child: BarChart(
BarChartData(
maxY: maxCount * 1.15,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIdx, rod, rodIdx) {
return BarTooltipItem(
'${_hourAmPm(group.x)}${rod.toY.toInt()}',
Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.onInverseSurface,
),
);
},
),
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 36,
getTitlesWidget: (value, meta) => Text(
value.toInt().toString(),
style: Theme.of(context).textTheme.labelSmall,
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final h = value.toInt();
// Show every 3rd hour label to avoid crowding
if (h % 3 != 0) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_hourAmPm(h),
style: Theme.of(context).textTheme.labelSmall,
),
);
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
drawVerticalLine: false,
horizontalInterval: maxCount > 0
? (maxCount / 4).ceilToDouble()
: 1,
),
borderData: FlBorderData(show: false),
barGroups: List.generate(24, (i) {
final count = (hourMap[i] ?? 0).toDouble();
return BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: count,
width: 10,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(4),
),
color: colors.primary,
),
],
);
}),
),
),
);
}
}
/// Vertical bar chart showing tickets created per hour of day (023).
class TicketsByHourChart extends ConsumerWidget {
const TicketsByHourChart({super.key, this.repaintKey});
final GlobalKey? repaintKey;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncData = ref.watch(ticketsByHourReportProvider);
return asyncData.when(
loading: () => ReportCardWrapper(
title: 'Tickets Created by Hour',
isLoading: true,
height: 260,
repaintBoundaryKey: repaintKey,
child: const SizedBox.shrink(),
),
error: (e, _) => ReportCardWrapper(
title: 'Tickets Created by Hour',
error: e.toString(),
repaintBoundaryKey: repaintKey,
child: const SizedBox.shrink(),
),
data: (data) => _build(context, data),
);
}
Widget _build(BuildContext context, List<HourCount> data) {
if (data.isEmpty) {
return ReportCardWrapper(
title: 'Tickets Created by Hour',
repaintBoundaryKey: repaintKey,
child: const SizedBox(
height: 200,
child: Center(child: Text('No data for selected period')),
),
);
}
final hourMap = {for (final h in data) h.hour: h.count};
final maxCount = data
.fold<int>(0, (m, e) => e.count > m ? e.count : m)
.toDouble();
final colors = Theme.of(context).colorScheme;
return ReportCardWrapper(
title: 'Tickets Created by Hour',
repaintBoundaryKey: repaintKey,
height: 260,
child: BarChart(
BarChartData(
maxY: maxCount * 1.15,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIdx, rod, rodIdx) {
return BarTooltipItem(
'${_hourAmPm(group.x)}${rod.toY.toInt()}',
Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.onInverseSurface,
),
);
},
),
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 36,
getTitlesWidget: (value, meta) => Text(
value.toInt().toString(),
style: Theme.of(context).textTheme.labelSmall,
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final h = value.toInt();
if (h % 3 != 0) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_hourAmPm(h),
style: Theme.of(context).textTheme.labelSmall,
),
);
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
drawVerticalLine: false,
horizontalInterval: maxCount > 0
? (maxCount / 4).ceilToDouble()
: 1,
),
borderData: FlBorderData(show: false),
barGroups: List.generate(24, (i) {
final count = (hourMap[i] ?? 0).toDouble();
return BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: count,
width: 10,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(4),
),
color: colors.tertiary,
),
],
);
}),
),
),
);
}
}