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

128 lines
3.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../providers/reports_provider.dart';
import 'report_card_wrapper.dart';
/// KPI card showing ticket-to-task conversion rate, with a visual gauge arc.
class ConversionRateCard extends ConsumerWidget {
const ConversionRateCard({super.key, this.repaintKey});
final GlobalKey? repaintKey;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncData = ref.watch(ticketToTaskRateReportProvider);
return asyncData.when(
loading: () => ReportCardWrapper(
title: 'Ticket-to-Task Conversion',
isLoading: true,
height: 160,
repaintBoundaryKey: repaintKey,
child: const SizedBox.shrink(),
),
error: (e, _) => ReportCardWrapper(
title: 'Ticket-to-Task Conversion',
error: e.toString(),
repaintBoundaryKey: repaintKey,
child: const SizedBox.shrink(),
),
data: (data) => _build(context, data),
);
}
Widget _build(BuildContext context, ConversionRate data) {
final colors = Theme.of(context).colorScheme;
final text = Theme.of(context).textTheme;
return ReportCardWrapper(
title: 'Ticket-to-Task Conversion',
repaintBoundaryKey: repaintKey,
child: Row(
children: [
// Gauge
SizedBox(
width: 100,
height: 100,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 90,
height: 90,
child: CircularProgressIndicator(
value: data.conversionRate / 100,
strokeWidth: 8,
backgroundColor: colors.surfaceContainerHighest,
color: colors.primary,
strokeCap: StrokeCap.round,
),
),
Text(
'${data.conversionRate.toStringAsFixed(1)}%',
style: text.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
color: colors.primary,
),
),
],
),
),
const SizedBox(width: 24),
// Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_MetricRow(
label: 'Total Tickets',
value: data.totalTickets.toString(),
text: text,
),
const SizedBox(height: 8),
_MetricRow(
label: 'Promoted to Task',
value: data.promotedTickets.toString(),
text: text,
),
const SizedBox(height: 8),
_MetricRow(
label: 'Not Promoted',
value: (data.totalTickets - data.promotedTickets).toString(),
text: text,
),
],
),
),
],
),
);
}
}
class _MetricRow extends StatelessWidget {
const _MetricRow({
required this.label,
required this.value,
required this.text,
});
final String label;
final String value;
final TextTheme text;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(label, style: text.bodySmall)),
Text(
value,
style: text.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
],
);
}
}