193 lines
6.4 KiB
Dart
193 lines
6.4 KiB
Dart
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),
|
|
],
|
|
);
|
|
}
|
|
}
|