add dashboard to phone app

This commit is contained in:
root
2026-05-25 05:10:43 -04:00
parent d04c8ce437
commit 277db06d26
67 changed files with 7872 additions and 570 deletions
@@ -0,0 +1,296 @@
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;
Map<String, String> 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),
),
),
],
),
),
);
}
}