Files
2026-05-27 03:11:45 -04:00

297 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../providers/dashboard_providers.dart';
import '../../../widgets/error_view.dart';
class ReportsScreen extends ConsumerStatefulWidget {
const ReportsScreen({super.key});
@override
ConsumerState<ReportsScreen> createState() => _ReportsScreenState();
}
class _ReportsScreenState extends ConsumerState<ReportsScreen> {
DateTime _start = DateTime.now().subtract(const Duration(days: 30));
DateTime _end = DateTime.now();
String? _statusFilter;
ReportQuery get _params => (
startDate:
'${_start.toIso8601String().substring(0, 10)}T00:00:00.000Z',
endDate: '${_end.toIso8601String().substring(0, 10)}T23:59:59.000Z',
status: _statusFilter,
);
Future<void> _pickRange() async {
final range = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(start: _start, end: _end),
);
if (range != null) {
setState(() {
_start = range.start;
_end = range.end;
});
}
}
@override
Widget build(BuildContext context) {
final fmt = DateFormat('MMM d, yyyy');
final fmtMoney = NumberFormat.currency(symbol: '\$', decimalDigits: 2);
final reportAsync = ref.watch(reportProvider(_params));
return Scaffold(
appBar: AppBar(
title: const Text('Reports'),
actions: [
PopupMenuButton<String?>(
icon: Icon(
Icons.filter_list,
color: _statusFilter != null ? const Color(0xFF1A56DB) : null,
),
onSelected: (v) => setState(() => _statusFilter = v),
itemBuilder: (_) => const [
PopupMenuItem(value: null, child: Text('All statuses')),
PopupMenuItem(value: 'CONFIRMED', child: Text('Confirmed')),
PopupMenuItem(value: 'ACTIVE', child: Text('Active')),
PopupMenuItem(value: 'CLOSED', child: Text('Closed')),
PopupMenuItem(value: 'CANCELLED', child: Text('Cancelled')),
],
),
],
),
body: Column(
children: [
_DateRangeBar(
start: _start,
end: _end,
fmt: fmt,
onTap: _pickRange,
),
Expanded(
child: reportAsync.when(
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorView(
message: 'Failed to load report',
onRetry: () => ref.invalidate(reportProvider(_params)),
),
data: (report) => CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverToBoxAdapter(
child: Column(
children: [
Row(
children: [
Expanded(
child: _MetricCard(
label: 'Reservations',
value:
report.totalReservations.toString(),
icon: Icons.event_note,
color: const Color(0xFF1A56DB),
),
),
const SizedBox(width: 12),
Expanded(
child: _MetricCard(
label: 'Revenue',
value: fmtMoney
.format(report.rentalRevenue / 100),
icon: Icons.attach_money,
color: const Color(0xFF0E9F6E),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _MetricCard(
label: 'Collected',
value: fmtMoney
.format(report.totalPaid / 100),
icon: Icons.check_circle_outline,
color: const Color(0xFF0E9F6E),
),
),
const SizedBox(width: 12),
Expanded(
child: _MetricCard(
label: 'Outstanding',
value: fmtMoney
.format(report.totalOutstanding / 100),
icon: Icons.pending_outlined,
color: const Color(0xFFE02424),
),
),
],
),
],
),
),
),
if (report.items.isNotEmpty) ...[
const SliverPadding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
sliver: SliverToBoxAdapter(
child: Text(
'Reservations',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverList.separated(
itemCount: report.items.length,
separatorBuilder: (_, _) =>
const Divider(height: 1),
itemBuilder: (_, i) {
final item = report.items[i];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
item.customerName ?? 'Walk-in',
style: const TextStyle(
fontWeight: FontWeight.w500),
),
subtitle: Text(
'${item.vehicleName ?? 'Vehicle'}${fmt.format(item.startDate)} ${fmt.format(item.endDate)}',
style: const TextStyle(fontSize: 12),
),
trailing: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
fmtMoney.format(item.totalAmount / 100),
style: const TextStyle(
fontWeight: FontWeight.w600,
),
),
Text(
item.paymentStatus,
style: TextStyle(
fontSize: 11,
color: item.paymentStatus == 'PAID'
? const Color(0xFF0E9F6E)
: const Color(0xFF9CA3AF),
),
),
],
),
);
},
),
),
],
],
),
),
),
],
),
);
}
}
class _DateRangeBar extends StatelessWidget {
final DateTime start;
final DateTime end;
final DateFormat fmt;
final VoidCallback onTap;
const _DateRangeBar({
required this.start,
required this.end,
required this.fmt,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
color: Color(0xFFF9FAFB),
border: Border(bottom: BorderSide(color: Color(0xFFE5E7EB))),
),
child: Row(
children: [
const Icon(Icons.date_range_outlined,
size: 18, color: Color(0xFF6B7280)),
const SizedBox(width: 8),
Text(
'${fmt.format(start)}${fmt.format(end)}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
const Spacer(),
const Text('Change',
style: TextStyle(
color: Color(0xFF1A56DB), fontSize: 13)),
],
),
),
);
}
}
class _MetricCard extends StatelessWidget {
final String label;
final String value;
final IconData icon;
final Color color;
const _MetricCard({
required this.label,
required this.value,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: color),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
);
}
}