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