Reports
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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';
|
||||
|
||||
/// Grouped vertical bar chart — assigned vs completed tasks per IT staff member.
|
||||
class StaffWorkloadChart extends ConsumerWidget {
|
||||
const StaffWorkloadChart({super.key, this.repaintKey});
|
||||
final GlobalKey? repaintKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asyncData = ref.watch(staffWorkloadReportProvider);
|
||||
|
||||
return asyncData.when(
|
||||
loading: () => ReportCardWrapper(
|
||||
title: 'IT Staff Workload',
|
||||
isLoading: true,
|
||||
height: 300,
|
||||
repaintBoundaryKey: repaintKey,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
error: (e, _) => ReportCardWrapper(
|
||||
title: 'IT Staff Workload',
|
||||
error: e.toString(),
|
||||
repaintBoundaryKey: repaintKey,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
data: (data) => _build(context, data),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context, List<StaffWorkload> data) {
|
||||
if (data.isEmpty) {
|
||||
return ReportCardWrapper(
|
||||
title: 'IT Staff Workload',
|
||||
repaintBoundaryKey: repaintKey,
|
||||
child: const SizedBox(
|
||||
height: 200,
|
||||
child: Center(child: Text('No data for selected period')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final text = Theme.of(context).textTheme;
|
||||
|
||||
final maxY = data.fold<int>(
|
||||
0,
|
||||
(m, e) => [
|
||||
m,
|
||||
e.assignedCount,
|
||||
e.completedCount,
|
||||
].reduce((a, b) => a > b ? a : b),
|
||||
);
|
||||
|
||||
return ReportCardWrapper(
|
||||
title: 'IT Staff Workload',
|
||||
repaintBoundaryKey: repaintKey,
|
||||
height: 320,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LegendDot(color: colors.primary, label: 'Assigned'),
|
||||
const SizedBox(width: 16),
|
||||
_LegendDot(color: colors.tertiary, label: 'Completed'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: maxY * 1.2,
|
||||
barTouchData: BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipItem: (group, groupIdx, rod, rodIdx) {
|
||||
final item = data[group.x];
|
||||
final label = rodIdx == 0 ? 'Assigned' : 'Completed';
|
||||
return BarTooltipItem(
|
||||
'${item.staffName}\n$label: ${rod.toY.toInt()}',
|
||||
text.bodySmall!.copyWith(
|
||||
color: colors.onInverseSurface,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 36,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
value.toInt().toString(),
|
||||
style: text.labelSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final idx = value.toInt();
|
||||
if (idx < 0 || idx >= data.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final name = data[idx].staffName;
|
||||
// Show first name only to save space
|
||||
final short = name.split(' ').first;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
short.length > 10
|
||||
? '${short.substring(0, 8)}…'
|
||||
: short,
|
||||
style: text.labelSmall,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: maxY > 0 ? (maxY / 4).ceilToDouble() : 1,
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: List.generate(data.length, (i) {
|
||||
return BarChartGroupData(
|
||||
x: i,
|
||||
barsSpace: 4,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: data[i].assignedCount.toDouble(),
|
||||
width: 14,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(4),
|
||||
),
|
||||
color: colors.primary,
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: data[i].completedCount.toDouble(),
|
||||
width: 14,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(4),
|
||||
),
|
||||
color: colors.tertiary,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LegendDot extends StatelessWidget {
|
||||
const _LegendDot({required this.color, required this.label});
|
||||
final Color color;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user