Reports
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
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 (0–23).
|
||||
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 (0–23).
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user