A whole lot of stuff

This commit is contained in:
Courtney Arnold 2026-08-18 11:04:58 -05:00
parent faf319322a
commit ad850d2be6
50 changed files with 3013 additions and 213 deletions

View file

@ -7,10 +7,12 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../services/app_state.dart';
import '../services/estimated_refund.dart';
import '../services/fuel_report.dart';
import '../services/fuel_report_images.dart';
import '../services/fuel_report_pdf.dart';
import '../services/onboarding_keys.dart';
import 'faq_screen.dart';
/// Missouri's Motor Fuel Refund Claim form isn't something this app can
/// file for the user — it's a state form, submitted to the state — so the
@ -51,13 +53,26 @@ class _ReportScreenState extends State<ReportScreen> with AutomaticKeepAliveClie
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
final (start, end) = defaultReportDateRange(DateTime.now());
_startDate = start;
_endDate = end;
}
Future<void> _pickDate({required bool isStart}) async {
final now = DateTime.now();
// The default range's end can fall in the future (it covers the
// fuel-tax-refund period currently underway, per defaultReportDateRange)
// — showDatePicker asserts initialDate <= lastDate, so lastDate has to
// stretch to cover it too, not just "today".
final lastDate = _endDate != null && _endDate!.isAfter(now) ? _endDate! : now;
final picked = await showDatePicker(
context: context,
initialDate: (isStart ? _startDate : _endDate) ?? now,
firstDate: DateTime(2000),
lastDate: now,
lastDate: lastDate,
);
if (picked == null) return;
setState(() {
@ -118,6 +133,22 @@ class _ReportScreenState extends State<ReportScreen> with AutomaticKeepAliveClie
}
}
Future<void> _preview(FuelReport report) async {
setState(() => _busy = true);
Uint8List bytes;
try {
bytes = await _buildPdfBytes(report);
} finally {
if (mounted) setState(() => _busy = false);
}
if (!mounted) return;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => _ReportPreviewScreen(bytes: bytes, fileName: fuelReportFileName(report)),
),
);
}
Future<void> _openRefundFormLink() async {
final launched = await launchUrl(_refundFormUrl, mode: LaunchMode.externalApplication);
if (!launched && mounted) {
@ -161,7 +192,10 @@ class _ReportScreenState extends State<ReportScreen> with AutomaticKeepAliveClie
);
return Scaffold(
appBar: AppBar(title: const Text('Fuel Report')),
appBar: AppBar(
title: const Text('Fuel Report'),
actions: const [FaqActionButton()],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
@ -233,6 +267,7 @@ class _ReportScreenState extends State<ReportScreen> with AutomaticKeepAliveClie
_VehicleSelectionCard(
row: row,
selected: _selectedVehicleIds.contains(row.vehicle.id),
showEstimatedRefund: appState.showEstimatedFuelRefund,
onTap: () => setState(() {
if (_selectedVehicleIds.contains(row.vehicle.id)) {
_selectedVehicleIds.remove(row.vehicle.id);
@ -252,7 +287,9 @@ class _ReportScreenState extends State<ReportScreen> with AutomaticKeepAliveClie
title: const Text('Total', style: TextStyle(fontWeight: FontWeight.bold)),
trailing: Text(
'${report.totalGallons.toStringAsFixed(3)} gal · '
'${NumberFormat.simpleCurrency().format(report.totalCost)}',
'${NumberFormat.simpleCurrency().format(report.totalCost)}'
'${appState.showEstimatedFuelRefund ? ' · Est. '
'${NumberFormat.simpleCurrency().format(estimatedFuelRefund(report.totalGallons))}' : ''}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
@ -260,6 +297,14 @@ class _ReportScreenState extends State<ReportScreen> with AutomaticKeepAliveClie
Row(
key: OnboardingKeys.reportShareAndPrintRow,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _busy ? null : () => _preview(report),
icon: const Icon(Icons.visibility_outlined),
label: const Text('Preview'),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton.icon(
onPressed: _busy ? null : () => _share(report),
@ -334,9 +379,15 @@ class _ReportScreenState extends State<ReportScreen> with AutomaticKeepAliveClie
class _VehicleSelectionCard extends StatelessWidget {
final VehicleReportRow row;
final bool selected;
final bool showEstimatedRefund;
final VoidCallback onTap;
const _VehicleSelectionCard({required this.row, required this.selected, required this.onTap});
const _VehicleSelectionCard({
required this.row,
required this.selected,
required this.showEstimatedRefund,
required this.onTap,
});
@override
Widget build(BuildContext context) {
@ -379,6 +430,13 @@ class _VehicleSelectionCard extends StatelessWidget {
NumberFormat.simpleCurrency().format(row.totalCost),
style: TextStyle(color: foreground),
),
if (showEstimatedRefund) ...[
const SizedBox(height: 2),
Text(
'Est. ${NumberFormat.simpleCurrency().format(estimatedFuelRefund(row.totalGallons))}',
style: TextStyle(color: foregroundVariant, fontSize: 12),
),
],
],
),
],
@ -388,3 +446,31 @@ class _VehicleSelectionCard extends StatelessWidget {
);
}
}
/// Shows the already-built report PDF in-app, with [PdfPreview]'s own
/// built-in Share/Print actions — so Preview isn't a dead end, the same
/// two actions available back on [ReportScreen] are still reachable from
/// here without backing out first. The report's format is fixed
/// ([buildFuelReportPdf] always renders US Letter), so the page
/// format/orientation controls [PdfPreview] would otherwise offer are
/// turned off since they wouldn't actually change anything.
class _ReportPreviewScreen extends StatelessWidget {
final Uint8List bytes;
final String fileName;
const _ReportPreviewScreen({required this.bytes, required this.fileName});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Report Preview')),
body: PdfPreview(
build: (format) async => bytes,
pdfFileName: fileName,
canChangePageFormat: false,
canChangeOrientation: false,
canDebug: false,
),
);
}
}