135 lines
4.5 KiB
Dart
135 lines
4.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../providers/reports_provider.dart';
|
|
import 'report_card_wrapper.dart';
|
|
|
|
/// Formats hours into a human-readable string.
|
|
String _formatHours(double h) {
|
|
if (h < 1) {
|
|
return '${(h * 60).round()}m';
|
|
}
|
|
if (h >= 24) {
|
|
final days = (h / 24).floor();
|
|
final rem = (h % 24).round();
|
|
return rem > 0 ? '${days}d ${rem}h' : '${days}d';
|
|
}
|
|
return '${h.toStringAsFixed(1)}h';
|
|
}
|
|
|
|
/// Horizontal bar chart — average task resolution time (hours) per office.
|
|
/// Limited to the top 10 slowest offices. Uses custom Flutter widgets so
|
|
/// labels sit genuinely inside each bar.
|
|
class AvgResolutionChart extends ConsumerWidget {
|
|
const AvgResolutionChart({super.key, this.repaintKey});
|
|
final GlobalKey? repaintKey;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final asyncData = ref.watch(avgResolutionReportProvider);
|
|
|
|
return asyncData.when(
|
|
loading: () => ReportCardWrapper(
|
|
title: 'Avg Resolution Time by Office',
|
|
isLoading: true,
|
|
height: 320,
|
|
repaintBoundaryKey: repaintKey,
|
|
child: const SizedBox.shrink(),
|
|
),
|
|
error: (e, _) => ReportCardWrapper(
|
|
title: 'Avg Resolution Time by Office',
|
|
error: e.toString(),
|
|
repaintBoundaryKey: repaintKey,
|
|
child: const SizedBox.shrink(),
|
|
),
|
|
data: (data) => _build(context, data),
|
|
);
|
|
}
|
|
|
|
Widget _build(BuildContext context, List<OfficeResolution> rawData) {
|
|
final data = rawData.length > 10 ? rawData.sublist(0, 10) : rawData;
|
|
|
|
if (data.isEmpty) {
|
|
return ReportCardWrapper(
|
|
title: 'Avg Resolution Time by Office',
|
|
repaintBoundaryKey: repaintKey,
|
|
child: const SizedBox(
|
|
height: 200,
|
|
child: Center(child: Text('No data for selected period')),
|
|
),
|
|
);
|
|
}
|
|
|
|
final maxHours = data.fold<double>(
|
|
0,
|
|
(m, e) => e.avgHours > m ? e.avgHours : m,
|
|
);
|
|
final height = (data.length * 34.0).clamp(160.0, 420.0);
|
|
final colors = Theme.of(context).colorScheme;
|
|
final barColor = colors.error.withValues(alpha: 0.75);
|
|
final onBarColor = barColor.computeLuminance() > 0.5
|
|
? Colors.black87
|
|
: Colors.white;
|
|
|
|
return ReportCardWrapper(
|
|
title: 'Avg Resolution Time by Office',
|
|
repaintBoundaryKey: repaintKey,
|
|
height: height,
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final availableWidth = constraints.maxWidth;
|
|
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
color: onBarColor,
|
|
fontWeight: FontWeight.w600,
|
|
);
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: data.map((item) {
|
|
final fraction = maxHours > 0 ? item.avgHours / maxHours : 0.0;
|
|
final barWidth = (fraction * availableWidth).clamp(
|
|
120.0,
|
|
availableWidth,
|
|
);
|
|
final timeLabel = _formatHours(item.avgHours);
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
child: Tooltip(
|
|
message: '${item.officeName}: $timeLabel',
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Container(
|
|
width: barWidth,
|
|
height: 28,
|
|
decoration: BoxDecoration(
|
|
color: barColor,
|
|
borderRadius: const BorderRadius.horizontal(
|
|
right: Radius.circular(6),
|
|
),
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
item.officeName,
|
|
style: labelStyle,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(timeLabel, style: labelStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|