A whole lot of stuff
This commit is contained in:
parent
faf319322a
commit
ad850d2be6
50 changed files with 3013 additions and 213 deletions
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue