From 1112b045160df7316e4f30e5d48a0e4762886e7f Mon Sep 17 00:00:00 2001 From: root Date: Wed, 27 May 2026 03:11:45 -0400 Subject: [PATCH] fix phone dashboard --- android/app/src/main/AndroidManifest.xml | 2 +- android/gradle.properties | 10 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle.kts | 2 +- ios/Runner/Info.plist | 4 +- lib/app.dart | 2 +- lib/core/constants/app_constants.dart | 8 +- lib/core/models/analytics.dart | 203 ++++- lib/core/models/customer.dart | 19 +- lib/core/models/reservation.dart | 19 +- lib/core/models/vehicle.dart | 19 +- lib/core/router/app_router.dart | 35 +- lib/core/services/customer_service.dart | 2 +- lib/core/services/reservation_service.dart | 2 +- lib/core/services/vehicle_service.dart | 13 +- .../agency_dashboard_webview_screen.dart | 3 +- .../auth/screens/employee_login_screen.dart | 4 + lib/features/auth/screens/landing_screen.dart | 3 +- lib/features/auth/screens/role_screen.dart | 6 +- lib/features/auth/screens/splash_screen.dart | 2 +- .../screens/booking_confirmation_screen.dart | 2 + .../client/screens/booking_screen.dart | 6 +- .../client/screens/client_home_screen.dart | 21 +- .../screens/client_vehicle_detail_screen.dart | 2 + .../client/screens/my_bookings_screen.dart | 11 +- .../providers/dashboard_providers.dart | 66 +- .../dashboard/screens/contracts_screen.dart | 45 +- .../screens/customer_detail_screen.dart | 9 +- .../dashboard/screens/customers_screen.dart | 10 +- .../dashboard/screens/dashboard_shell.dart | 30 +- .../dashboard/screens/home_screen.dart | 742 ++++++++++++++---- .../screens/online_reservations_screen.dart | 14 +- .../dashboard/screens/reports_screen.dart | 10 +- .../screens/reservations_screen.dart | 16 +- .../dashboard/screens/vehicles_screen.dart | 95 ++- lib/l10n/app_ar.arb | 2 +- lib/l10n/app_en.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_ar.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/widgets/app_back_button.dart | 28 + lib/widgets/error_view.dart | 4 +- lib/widgets/loading_list.dart | 9 +- lib/widgets/reservation_card.dart | 32 +- lib/widgets/stat_card.dart | 50 +- lib/widgets/vehicle_card.dart | 14 +- 48 files changed, 1222 insertions(+), 368 deletions(-) create mode 100644 lib/widgets/app_back_button.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5c58c1b..8863176 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Car Rental App + KriTomobil CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - car_rental_app + KriTomobil CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/app.dart b/lib/app.dart index e25182a..a50d8e8 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -17,7 +17,7 @@ class CarRentalApp extends ConsumerWidget { final themeMode = ref.watch(themeProvider); return MaterialApp.router( - title: 'RentalDriveGo', + title: 'KriTomobil', theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: themeMode, diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index 25dc943..9062794 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -8,7 +8,7 @@ class AppConstants { // Production static const String productionApiUrl = 'https://api.rentaldrivego.ma/api/v1'; - static const String productionDashboardUrl = 'https://rentaldrivego.ma/dashboard'; + static const String productionDashboardUrl = 'https://rentaldrivego.ma'; // Development static const String devAndroidApiUrl = 'http://10.0.2.2:4000/api/v1'; @@ -18,14 +18,12 @@ class AppConstants { static String get baseUrl { if (apiBaseUrlOverride.isNotEmpty) return apiBaseUrlOverride; - if (Platform.isAndroid) return devAndroidApiUrl; - return devLocalApiUrl; + return productionApiUrl; } static String get dashboardBaseUrl { if (dashboardUrlOverride.isNotEmpty) return dashboardUrlOverride; - if (Platform.isAndroid) return devAndroidDashboardUrl; - return devLocalDashboardUrl; + return productionDashboardUrl; } static String resolveMediaUrl(String url) { diff --git a/lib/core/models/analytics.dart b/lib/core/models/analytics.dart index 054810a..84d65ac 100644 --- a/lib/core/models/analytics.dart +++ b/lib/core/models/analytics.dart @@ -1,40 +1,179 @@ class DashboardMetrics { - final int totalReservations; - final int activeReservations; - final double totalRevenue; - final int totalVehicles; - final int availableVehicles; - final int totalCustomers; - final double occupancyRate; + final DashboardKpis kpis; + final List recentReservations; + final List sourceBreakdown; + final DashboardSubscription? subscription; const DashboardMetrics({ - required this.totalReservations, - required this.activeReservations, - required this.totalRevenue, - required this.totalVehicles, - required this.availableVehicles, - required this.totalCustomers, - required this.occupancyRate, + required this.kpis, + required this.recentReservations, + required this.sourceBreakdown, + required this.subscription, }); - factory DashboardMetrics.fromJson(Map json) => - DashboardMetrics( - totalReservations: json['totalReservations'] as int? ?? 0, - activeReservations: json['activeReservations'] as int? ?? 0, - totalRevenue: (json['totalRevenue'] as num?)?.toDouble() ?? 0, - totalVehicles: json['totalVehicles'] as int? ?? 0, - availableVehicles: json['availableVehicles'] as int? ?? 0, - totalCustomers: json['totalCustomers'] as int? ?? 0, - occupancyRate: (json['occupancyRate'] as num?)?.toDouble() ?? 0, - ); + factory DashboardMetrics.fromJson(Map json) { + final kpiJson = json['kpis'] as Map?; - factory DashboardMetrics.empty() => const DashboardMetrics( - totalReservations: 0, - activeReservations: 0, - totalRevenue: 0, - totalVehicles: 0, - availableVehicles: 0, - totalCustomers: 0, - occupancyRate: 0, + return DashboardMetrics( + kpis: DashboardKpis.fromJson(kpiJson ?? json), + recentReservations: (json['recentReservations'] as List?) + ?.map( + (e) => DashboardRecentReservation.fromJson( + e as Map, + ), + ) + .toList() ?? + const [], + sourceBreakdown: (json['sourceBreakdown'] as List?) + ?.map( + (e) => DashboardSourceBreakdown.fromJson( + e as Map, + ), + ) + .toList() ?? + const [], + subscription: json['subscription'] is Map + ? DashboardSubscription.fromJson( + json['subscription'] as Map, + ) + : null, + ); + } + + factory DashboardMetrics.empty() => DashboardMetrics( + kpis: DashboardKpis.empty(), + recentReservations: const [], + sourceBreakdown: const [], + subscription: null, + ); +} + +class DashboardKpis { + final int totalBookings; + final int activeVehicles; + final double monthlyRevenue; + final int totalCustomers; + final double bookingsChange; + final double vehiclesChange; + final double revenueChange; + final double customersChange; + + const DashboardKpis({ + required this.totalBookings, + required this.activeVehicles, + required this.monthlyRevenue, + required this.totalCustomers, + required this.bookingsChange, + required this.vehiclesChange, + required this.revenueChange, + required this.customersChange, + }); + + factory DashboardKpis.fromJson(Map json) => DashboardKpis( + totalBookings: + (json['totalBookings'] as num?)?.toInt() ?? + (json['totalReservations'] as num?)?.toInt() ?? + 0, + activeVehicles: + (json['activeVehicles'] as num?)?.toInt() ?? + (json['availableVehicles'] as num?)?.toInt() ?? + (json['totalVehicles'] as num?)?.toInt() ?? + 0, + monthlyRevenue: + (json['monthlyRevenue'] as num?)?.toDouble() ?? + (json['totalRevenue'] as num?)?.toDouble() ?? + 0, + totalCustomers: (json['totalCustomers'] as num?)?.toInt() ?? 0, + bookingsChange: (json['bookingsChange'] as num?)?.toDouble() ?? 0, + vehiclesChange: (json['vehiclesChange'] as num?)?.toDouble() ?? 0, + revenueChange: (json['revenueChange'] as num?)?.toDouble() ?? 0, + customersChange: (json['customersChange'] as num?)?.toDouble() ?? 0, + ); + + factory DashboardKpis.empty() => const DashboardKpis( + totalBookings: 0, + activeVehicles: 0, + monthlyRevenue: 0, + totalCustomers: 0, + bookingsChange: 0, + vehiclesChange: 0, + revenueChange: 0, + customersChange: 0, + ); +} + +class DashboardRecentReservation { + final String id; + final String bookingRef; + final String customerName; + final String vehicleName; + final DateTime startDate; + final DateTime endDate; + final String status; + final double totalAmount; + + const DashboardRecentReservation({ + required this.id, + required this.bookingRef, + required this.customerName, + required this.vehicleName, + required this.startDate, + required this.endDate, + required this.status, + required this.totalAmount, + }); + + factory DashboardRecentReservation.fromJson(Map json) => + DashboardRecentReservation( + id: json['id'] as String, + bookingRef: json['bookingRef'] as String? ?? '', + customerName: json['customerName'] as String? ?? 'Unknown customer', + vehicleName: json['vehicleName'] as String? ?? 'Unknown vehicle', + startDate: DateTime.tryParse(json['startDate'] as String? ?? '') ?? + DateTime.now(), + endDate: DateTime.tryParse(json['endDate'] as String? ?? '') ?? + DateTime.now(), + status: json['status'] as String? ?? 'UNKNOWN', + totalAmount: (json['totalAmount'] as num?)?.toDouble() ?? 0, + ); +} + +class DashboardSourceBreakdown { + final String source; + final int count; + final double revenue; + + const DashboardSourceBreakdown({ + required this.source, + required this.count, + required this.revenue, + }); + + factory DashboardSourceBreakdown.fromJson(Map json) => + DashboardSourceBreakdown( + source: json['source'] as String? ?? 'UNKNOWN', + count: (json['count'] as num?)?.toInt() ?? 0, + revenue: (json['revenue'] as num?)?.toDouble() ?? 0, + ); +} + +class DashboardSubscription { + final String status; + final String? planName; + final DateTime? trialEndsAt; + + const DashboardSubscription({ + required this.status, + required this.planName, + required this.trialEndsAt, + }); + + factory DashboardSubscription.fromJson(Map json) => + DashboardSubscription( + status: json['status'] as String? ?? 'ACTIVE', + planName: json['planName'] as String?, + trialEndsAt: json['trialEndsAt'] == null + ? null + : DateTime.tryParse(json['trialEndsAt'] as String), ); } diff --git a/lib/core/models/customer.dart b/lib/core/models/customer.dart index 8f37f18..177a56a 100644 --- a/lib/core/models/customer.dart +++ b/lib/core/models/customer.dart @@ -58,12 +58,27 @@ class CustomerListResponse { required this.pageSize, }); + factory CustomerListResponse.fromApi(dynamic json) { + if (json is List) { + final customers = json + .map((e) => Customer.fromJson(e as Map)) + .toList(); + return CustomerListResponse( + data: customers, + total: customers.length, + page: 1, + pageSize: customers.length, + ); + } + return CustomerListResponse.fromJson(json as Map); + } + factory CustomerListResponse.fromJson(Map json) => CustomerListResponse( - data: (json['data'] as List) + data: ((json['data'] ?? const []) as List) .map((e) => Customer.fromJson(e as Map)) .toList(), - total: json['total'] as int? ?? 0, + total: json['total'] as int? ?? ((json['data'] as List?)?.length ?? 0), page: json['page'] as int? ?? 1, pageSize: json['pageSize'] as int? ?? 20, ); diff --git a/lib/core/models/reservation.dart b/lib/core/models/reservation.dart index 3581380..90b55d1 100644 --- a/lib/core/models/reservation.dart +++ b/lib/core/models/reservation.dart @@ -131,12 +131,27 @@ class ReservationListResponse { required this.pageSize, }); + factory ReservationListResponse.fromApi(dynamic json) { + if (json is List) { + final reservations = json + .map((e) => Reservation.fromJson(e as Map)) + .toList(); + return ReservationListResponse( + data: reservations, + total: reservations.length, + page: 1, + pageSize: reservations.length, + ); + } + return ReservationListResponse.fromJson(json as Map); + } + factory ReservationListResponse.fromJson(Map json) => ReservationListResponse( - data: (json['data'] as List) + data: ((json['data'] ?? const []) as List) .map((e) => Reservation.fromJson(e as Map)) .toList(), - total: json['total'] as int? ?? 0, + total: json['total'] as int? ?? ((json['data'] as List?)?.length ?? 0), page: json['page'] as int? ?? 1, pageSize: json['pageSize'] as int? ?? 20, ); diff --git a/lib/core/models/vehicle.dart b/lib/core/models/vehicle.dart index 35056c8..a1cffc4 100644 --- a/lib/core/models/vehicle.dart +++ b/lib/core/models/vehicle.dart @@ -72,12 +72,27 @@ class VehicleListResponse { required this.pageSize, }); + factory VehicleListResponse.fromApi(dynamic json) { + if (json is List) { + final vehicles = json + .map((e) => Vehicle.fromJson(e as Map)) + .toList(); + return VehicleListResponse( + data: vehicles, + total: vehicles.length, + page: 1, + pageSize: vehicles.length, + ); + } + return VehicleListResponse.fromJson(json as Map); + } + factory VehicleListResponse.fromJson(Map json) => VehicleListResponse( - data: (json['data'] as List) + data: ((json['data'] ?? const []) as List) .map((e) => Vehicle.fromJson(e as Map)) .toList(), - total: json['total'] as int? ?? 0, + total: json['total'] as int? ?? ((json['data'] as List?)?.length ?? 0), page: json['page'] as int? ?? 1, pageSize: json['pageSize'] as int? ?? 20, ); diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 4e024df..fbd4f07 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -31,7 +31,6 @@ import '../../features/client/screens/booking_screen.dart'; import '../../features/client/screens/booking_confirmation_screen.dart'; import '../../features/client/screens/my_bookings_screen.dart'; import '../../features/client/models/booking_result.dart'; -import '../../features/agency_webview/screens/agency_dashboard_webview_screen.dart'; final routerProvider = Provider((ref) { final router = GoRouter( @@ -43,6 +42,7 @@ final routerProvider = Provider((ref) { final isSplash = loc == '/splash'; final isLanding = loc == '/landing'; final isLogin = loc.startsWith('/login'); + final isAgency = loc == '/agency'; if (status == AuthStatus.unknown) { return isSplash ? null : '/splash'; @@ -57,12 +57,20 @@ final routerProvider = Provider((ref) { return '/landing'; } + if (isAgency) { + if (status == AuthStatus.authenticated) { + return auth.userType == AppConstants.userTypeEmployee + ? '/dashboard' + : '/client'; + } + return '/login/employee'; + } + if (status == AuthStatus.unauthenticated) { if (isLanding || isLogin || loc.startsWith('/client') || - loc == '/role' || - loc == '/agency') { + loc == '/role') { return null; } return '/landing'; @@ -90,6 +98,10 @@ final routerProvider = Provider((ref) { path: '/dashboard/vehicles', builder: (context, _) => const VehiclesScreen(), ), + GoRoute( + path: '/dashboard/fleet', + builder: (context, _) => const VehiclesScreen(), + ), GoRoute( path: '/dashboard/reservations', builder: (context, _) => const ReservationsScreen(), @@ -103,6 +115,11 @@ final routerProvider = Provider((ref) { builder: (_, state) => VehicleDetailScreen(id: state.pathParameters['id']!), ), + GoRoute( + path: '/dashboard/fleet/:id', + builder: (_, state) => + VehicleDetailScreen(id: state.pathParameters['id']!), + ), GoRoute( path: '/dashboard/reservations/:id', builder: (_, state) => @@ -178,7 +195,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: '/agency', - builder: (context, _) => const AgencyDashboardWebViewScreen(), + builder: (context, _) => const EmployeeLoginScreen(), ), GoRoute( path: '/dashboard/reservations/new', @@ -188,12 +205,22 @@ final routerProvider = Provider((ref) { path: '/dashboard/vehicles/new', builder: (context, _) => const VehicleFormScreen(), ), + GoRoute( + path: '/dashboard/fleet/new', + builder: (context, _) => const VehicleFormScreen(), + ), GoRoute( path: '/dashboard/vehicles/:id/edit', builder: (_, state) => VehicleFormScreen( vehicle: state.extra as dynamic, ), ), + GoRoute( + path: '/dashboard/fleet/:id/edit', + builder: (_, state) => VehicleFormScreen( + vehicle: state.extra as dynamic, + ), + ), GoRoute( path: '/dashboard/customers/new', builder: (context, _) => const CustomerFormScreen(), diff --git a/lib/core/services/customer_service.dart b/lib/core/services/customer_service.dart index ec22a39..fb95495 100644 --- a/lib/core/services/customer_service.dart +++ b/lib/core/services/customer_service.dart @@ -15,7 +15,7 @@ class CustomerService { 'pageSize': pageSize, if (search != null && search.isNotEmpty) 'search': search, }); - return CustomerListResponse.fromJson(res.data as Map); + return CustomerListResponse.fromApi(res.data); } Future getCustomer(String id) async { diff --git a/lib/core/services/reservation_service.dart b/lib/core/services/reservation_service.dart index d6a2802..5c8523e 100644 --- a/lib/core/services/reservation_service.dart +++ b/lib/core/services/reservation_service.dart @@ -52,7 +52,7 @@ class ReservationService { if (search != null && search.isNotEmpty) 'search': search, 'customerId': ?customerId, }); - return ReservationListResponse.fromJson(res.data as Map); + return ReservationListResponse.fromApi(res.data); } Future getReservation(String id) async { diff --git a/lib/core/services/vehicle_service.dart b/lib/core/services/vehicle_service.dart index a8301c7..d627ad5 100644 --- a/lib/core/services/vehicle_service.dart +++ b/lib/core/services/vehicle_service.dart @@ -71,15 +71,14 @@ class VehicleService { int page = 1, int pageSize = 20, String? status, - String? search, }) async { - final res = await _client.get('/vehicles', params: { - 'page': page, - 'pageSize': pageSize, + final params = { + if (page != 1) 'page': page, + if (pageSize != 20) 'pageSize': pageSize, 'status': ?status, - if (search != null && search.isNotEmpty) 'search': search, - }); - return VehicleListResponse.fromJson(res.data as Map); + }; + final res = await _client.get('/vehicles', params: params); + return VehicleListResponse.fromApi(res.data); } Future getVehicle(String id) async { diff --git a/lib/features/agency_webview/screens/agency_dashboard_webview_screen.dart b/lib/features/agency_webview/screens/agency_dashboard_webview_screen.dart index 744ed16..69b7f7d 100644 --- a/lib/features/agency_webview/screens/agency_dashboard_webview_screen.dart +++ b/lib/features/agency_webview/screens/agency_dashboard_webview_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../../core/constants/app_constants.dart'; +import '../../../widgets/app_back_button.dart'; class AgencyDashboardWebViewScreen extends StatefulWidget { const AgencyDashboardWebViewScreen({super.key}); @@ -53,7 +54,7 @@ class _AgencyDashboardWebViewScreenState return Scaffold( appBar: AppBar( title: const Text('Agency Dashboard'), - leading: BackButton(onPressed: () => context.go('/landing')), + leading: const AppBackButton(fallbackRoute: '/landing'), actions: [ if (!_isLoading) IconButton( diff --git a/lib/features/auth/screens/employee_login_screen.dart b/lib/features/auth/screens/employee_login_screen.dart index 3a231ca..ab73afb 100644 --- a/lib/features/auth/screens/employee_login_screen.dart +++ b/lib/features/auth/screens/employee_login_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; +import 'package:go_router/go_router.dart'; import '../../../core/providers/auth_provider.dart'; class EmployeeLoginScreen extends ConsumerStatefulWidget { @@ -37,6 +38,9 @@ class _EmployeeLoginScreenState extends ConsumerState { _emailController.text.trim(), _passwordController.text, ); + if (mounted) { + context.go('/dashboard'); + } } on DioException catch (e) { setState(() { _error = _messageForDioError(e); diff --git a/lib/features/auth/screens/landing_screen.dart b/lib/features/auth/screens/landing_screen.dart index 878fd23..2ed7494 100644 --- a/lib/features/auth/screens/landing_screen.dart +++ b/lib/features/auth/screens/landing_screen.dart @@ -60,6 +60,7 @@ class LandingScreen extends StatelessWidget { width: double.infinity, child: ElevatedButton( onPressed: () => context.go('/client'), + //onPressed: () => context.push('/client'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFF6B00), foregroundColor: Colors.white, @@ -86,7 +87,7 @@ class LandingScreen extends StatelessWidget { ), const SizedBox(height: 16), TextButton( - onPressed: () => context.go('/agency'), + onPressed: () => context.push('/login/employee'), style: TextButton.styleFrom( foregroundColor: Colors.white.withValues(alpha: 0.5), ), diff --git a/lib/features/auth/screens/role_screen.dart b/lib/features/auth/screens/role_screen.dart index 5262da6..a692bba 100644 --- a/lib/features/auth/screens/role_screen.dart +++ b/lib/features/auth/screens/role_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../../../widgets/app_back_button.dart'; class RoleScreen extends StatelessWidget { const RoleScreen({super.key}); @@ -13,7 +14,8 @@ class RoleScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 40), + const AppBackButton(fallbackRoute: '/landing'), + const SizedBox(height: 24), Container( width: 60, height: 60, @@ -53,7 +55,7 @@ class RoleScreen extends StatelessWidget { title: 'Browse & Reserve', subtitle: 'Find and book a vehicle', color: const Color(0xFF0E9F6E), - onTap: () => context.go('/client'), + onTap: () => context.push('/client'), ), ], ), diff --git a/lib/features/auth/screens/splash_screen.dart b/lib/features/auth/screens/splash_screen.dart index 2f4b892..8805bf0 100644 --- a/lib/features/auth/screens/splash_screen.dart +++ b/lib/features/auth/screens/splash_screen.dart @@ -34,7 +34,7 @@ class _SplashScreenState extends ConsumerState { ), const SizedBox(height: 20), const Text( - 'RentalDriveGo', + 'KriTomobil', style: TextStyle( color: Colors.white, fontSize: 28, diff --git a/lib/features/client/screens/booking_confirmation_screen.dart b/lib/features/client/screens/booking_confirmation_screen.dart index 14dcae4..628b711 100644 --- a/lib/features/client/screens/booking_confirmation_screen.dart +++ b/lib/features/client/screens/booking_confirmation_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; +import '../../../widgets/app_back_button.dart'; import '../models/booking_result.dart'; const _orange = Color(0xFFFF6B00); @@ -18,6 +19,7 @@ class BookingConfirmationScreen extends StatelessWidget { return Scaffold( appBar: AppBar( + leading: const AppBackButton(fallbackRoute: '/client'), title: const Text('Booking Confirmed'), ), body: SingleChildScrollView( diff --git a/lib/features/client/screens/booking_screen.dart b/lib/features/client/screens/booking_screen.dart index 72ccda9..da24cda 100644 --- a/lib/features/client/screens/booking_screen.dart +++ b/lib/features/client/screens/booking_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../core/models/marketplace.dart'; +import '../../../widgets/app_back_button.dart'; import '../models/booking_result.dart'; import '../providers/client_providers.dart'; @@ -130,7 +131,10 @@ class _BookingScreenState extends ConsumerState { : null; return Scaffold( - appBar: AppBar(title: const Text('Complete Booking')), + appBar: AppBar( + leading: const AppBackButton(fallbackRoute: '/client'), + title: const Text('Complete Booking'), + ), body: Form( key: _formKey, child: ListView( diff --git a/lib/features/client/screens/client_home_screen.dart b/lib/features/client/screens/client_home_screen.dart index c5ae8ae..5832007 100644 --- a/lib/features/client/screens/client_home_screen.dart +++ b/lib/features/client/screens/client_home_screen.dart @@ -6,6 +6,7 @@ import 'package:intl/intl.dart'; import '../providers/client_providers.dart'; import '../../../core/models/marketplace.dart'; import '../../../l10n/app_localizations.dart'; +import '../../../widgets/app_back_button.dart'; import '../../../widgets/preferences_bar.dart'; import 'client_shell.dart'; import 'widgets/marketplace_vehicle_card.dart'; @@ -233,15 +234,21 @@ class _ClientHomeScreenState extends ConsumerState { padding: const EdgeInsets.fromLTRB(20, 16, 12, 0), child: Row( children: [ - Text( - l10n.findCarToRent, - style: TextStyle( - color: cs.onSurface, - fontSize: 20, - fontWeight: FontWeight.bold, + AppBackButton( + fallbackRoute: '/landing', + color: cs.onSurface, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + l10n.findCarToRent, + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), ), - const Spacer(), const PreferencesBar(), ], ), diff --git a/lib/features/client/screens/client_vehicle_detail_screen.dart b/lib/features/client/screens/client_vehicle_detail_screen.dart index ed84eb0..10a9295 100644 --- a/lib/features/client/screens/client_vehicle_detail_screen.dart +++ b/lib/features/client/screens/client_vehicle_detail_screen.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../core/models/marketplace.dart'; import '../../../core/constants/app_constants.dart'; +import '../../../widgets/app_back_button.dart'; import '../providers/client_providers.dart'; class ClientVehicleDetailScreen extends ConsumerWidget { @@ -48,6 +49,7 @@ class _DetailView extends StatelessWidget { SliverAppBar( expandedHeight: 280, pinned: true, + leading: const AppBackButton(fallbackRoute: '/client'), flexibleSpace: FlexibleSpaceBar( background: vehicle.primaryPhoto != null ? CachedNetworkImage( diff --git a/lib/features/client/screens/my_bookings_screen.dart b/lib/features/client/screens/my_bookings_screen.dart index bf0b45c..f043a01 100644 --- a/lib/features/client/screens/my_bookings_screen.dart +++ b/lib/features/client/screens/my_bookings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/providers/auth_provider.dart'; +import '../../../widgets/app_back_button.dart'; import 'client_shell.dart'; class MyBookingsScreen extends ConsumerWidget { @@ -13,7 +14,10 @@ class MyBookingsScreen extends ConsumerWidget { if (auth.status != AuthStatus.authenticated) { return ClientShell( - appBar: AppBar(title: const Text('My Bookings')), + appBar: AppBar( + leading: const AppBackButton(fallbackRoute: '/client'), + title: const Text('My Bookings'), + ), body: Center( child: Padding( padding: const EdgeInsets.all(32), @@ -53,7 +57,10 @@ class MyBookingsScreen extends ConsumerWidget { } return ClientShell( - appBar: AppBar(title: const Text('My Bookings')), + appBar: AppBar( + leading: const AppBackButton(fallbackRoute: '/client'), + title: const Text('My Bookings'), + ), body: const Center( child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/features/dashboard/providers/dashboard_providers.dart b/lib/features/dashboard/providers/dashboard_providers.dart index 544a9c2..56d548f 100644 --- a/lib/features/dashboard/providers/dashboard_providers.dart +++ b/lib/features/dashboard/providers/dashboard_providers.dart @@ -34,24 +34,35 @@ final dashboardMetricsProvider = FutureProvider((ref) async { return ref.read(analyticsServiceProvider).getDashboard(); }); +typedef ReportQuery = ({ + String startDate, + String endDate, + String? status, +}); + final reportProvider = - FutureProvider.family>((ref, params) async { + FutureProvider.family((ref, params) async { return ref.read(analyticsServiceProvider).getReport( - startDate: params['startDate']!, - endDate: params['endDate']!, - status: params['status'], + startDate: params.startDate, + endDate: params.endDate, + status: params.status, ); }); // ─── Vehicles ───────────────────────────────────────────────── -final vehiclesProvider = FutureProvider.family>( +typedef VehicleQuery = ({ + int page, + int pageSize, + String? status, +}); + +final vehiclesProvider = FutureProvider.family( (ref, params) async { return ref.read(vehicleServiceProvider).getVehicles( - page: params['page'] as int? ?? 1, - pageSize: params['pageSize'] as int? ?? 20, - status: params['status'] as String?, - search: params['search'] as String?, + page: params.page, + pageSize: params.pageSize, + status: params.status, ); }, ); @@ -75,16 +86,25 @@ final calendarProvider = // ─── Reservations ───────────────────────────────────────────── +typedef ReservationQuery = ({ + int page, + int pageSize, + String? status, + String? source, + String? search, + String? customerId, +}); + final reservationsProvider = - FutureProvider.family>( + FutureProvider.family( (ref, params) async { return ref.read(reservationServiceProvider).getReservations( - page: params['page'] as int? ?? 1, - pageSize: params['pageSize'] as int? ?? 20, - status: params['status'] as String?, - source: params['source'] as String?, - search: params['search'] as String?, - customerId: params['customerId'] as String?, + page: params.page, + pageSize: params.pageSize, + status: params.status, + source: params.source, + search: params.search, + customerId: params.customerId, ); }, ); @@ -101,13 +121,19 @@ final inspectionsProvider = // ─── Customers ──────────────────────────────────────────────── +typedef CustomerQuery = ({ + int page, + int pageSize, + String? search, +}); + final customersProvider = - FutureProvider.family>( + FutureProvider.family( (ref, params) async { return ref.read(customerServiceProvider).getCustomers( - page: params['page'] as int? ?? 1, - pageSize: params['pageSize'] as int? ?? 20, - search: params['search'] as String?, + page: params.page, + pageSize: params.pageSize, + search: params.search, ); }, ); diff --git a/lib/features/dashboard/screens/contracts_screen.dart b/lib/features/dashboard/screens/contracts_screen.dart index f6b2ced..ec1e9b2 100644 --- a/lib/features/dashboard/screens/contracts_screen.dart +++ b/lib/features/dashboard/screens/contracts_screen.dart @@ -15,6 +15,33 @@ class _ContractsScreenState extends ConsumerState { final _searchCtrl = TextEditingController(); String _search = ''; + List _visibleContracts(List reservations) { + final query = _search.trim().toLowerCase(); + + return reservations.where((reservation) { + if (reservation.contractNumber == null) { + return false; + } + + if (query.isEmpty) { + return true; + } + + final customer = reservation.customer; + final vehicle = reservation.vehicle; + final haystack = [ + customer?.fullName, + customer?.email, + vehicle?.displayName, + vehicle?.licensePlate, + reservation.contractNumber, + reservation.invoiceNumber, + ].whereType().join(' ').toLowerCase(); + + return haystack.contains(query); + }).toList(); + } + @override void dispose() { _searchCtrl.dispose(); @@ -23,12 +50,14 @@ class _ContractsScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final params = { - 'page': 1, - 'pageSize': 100, - 'status': 'CONFIRMED,ACTIVE,COMPLETED,CLOSED', - if (_search.isNotEmpty) 'search': _search, - }; + final ReservationQuery params = ( + page: 1, + pageSize: 100, + status: null, + source: null, + search: null, + customerId: null, + ); final asyncData = ref.watch(reservationsProvider(params)); @@ -69,9 +98,7 @@ class _ContractsScreenState extends ConsumerState { loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Error: $e')), data: (response) { - final contracts = response.data - .where((r) => r.contractNumber != null) - .toList(); + final contracts = _visibleContracts(response.data); if (contracts.isEmpty) { return const Center( diff --git a/lib/features/dashboard/screens/customer_detail_screen.dart b/lib/features/dashboard/screens/customer_detail_screen.dart index bf2b52c..90bdb6e 100644 --- a/lib/features/dashboard/screens/customer_detail_screen.dart +++ b/lib/features/dashboard/screens/customer_detail_screen.dart @@ -17,7 +17,14 @@ class CustomerDetailScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final async = ref.watch(customerDetailProvider(id)); final reservationsAsync = ref.watch( - reservationsProvider({'customerId': id, 'page': 1, 'pageSize': 10}), + reservationsProvider(( + page: 1, + pageSize: 10, + status: null, + source: null, + search: null, + customerId: id, + )), ); return async.when( diff --git a/lib/features/dashboard/screens/customers_screen.dart b/lib/features/dashboard/screens/customers_screen.dart index 45562f8..3e920e4 100644 --- a/lib/features/dashboard/screens/customers_screen.dart +++ b/lib/features/dashboard/screens/customers_screen.dart @@ -25,11 +25,11 @@ class _CustomersScreenState extends ConsumerState { super.dispose(); } - Map get _params => { - 'page': 1, - 'pageSize': 50, - if (_search.isNotEmpty) 'search': _search, - }; + CustomerQuery get _params => ( + page: 1, + pageSize: 50, + search: _search.isNotEmpty ? _search : null, + ); @override Widget build(BuildContext context) { diff --git a/lib/features/dashboard/screens/dashboard_shell.dart b/lib/features/dashboard/screens/dashboard_shell.dart index 911941e..7a15994 100644 --- a/lib/features/dashboard/screens/dashboard_shell.dart +++ b/lib/features/dashboard/screens/dashboard_shell.dart @@ -24,7 +24,10 @@ class DashboardShell extends ConsumerWidget { final employee = ref.watch(authProvider).employee; int currentIndex = 0; - if (location.startsWith('/dashboard/vehicles')) currentIndex = 1; + if (location.startsWith('/dashboard/fleet') || + location.startsWith('/dashboard/vehicles')) { + currentIndex = 1; + } if (location.startsWith('/dashboard/reservations')) currentIndex = 2; if (location.startsWith('/dashboard/customers')) currentIndex = 3; @@ -37,15 +40,14 @@ class DashboardShell extends ConsumerWidget { bottomNavigationBar: NavigationBar( selectedIndex: currentIndex, onDestinationSelected: (i) { - switch (i) { - case 0: - context.go('/dashboard'); - case 1: - context.go('/dashboard/vehicles'); - case 2: - context.go('/dashboard/reservations'); - case 3: - context.go('/dashboard/customers'); + if (i == 0) { + context.go('/dashboard'); + } else if (i == 1) { + context.go('/dashboard/fleet'); + } else if (i == 2) { + context.go('/dashboard/reservations'); + } else if (i == 3) { + context.go('/dashboard/customers'); } }, destinations: const [ @@ -57,7 +59,7 @@ class DashboardShell extends ConsumerWidget { NavigationDestination( icon: Icon(Icons.directions_car_outlined), selectedIcon: Icon(Icons.directions_car), - label: 'Vehicles', + label: 'Fleet', ), NavigationDestination( icon: Icon(Icons.event_note_outlined), @@ -141,10 +143,10 @@ class _Drawer extends ConsumerWidget { ), _NavItem( icon: Icons.directions_car_outlined, - label: 'Vehicles', + label: 'Fleet', onTap: () { Navigator.pop(context); - context.go('/dashboard/vehicles'); + context.go('/dashboard/fleet'); }, ), _NavItem( @@ -166,7 +168,7 @@ class _Drawer extends ConsumerWidget { const Divider(), _NavItem( icon: Icons.public_outlined, - label: 'Online Requests', + label: 'Online Reservations', onTap: () { Navigator.pop(context); context.push('/dashboard/online-reservations'); diff --git a/lib/features/dashboard/screens/home_screen.dart b/lib/features/dashboard/screens/home_screen.dart index f966d46..bc3c1e2 100644 --- a/lib/features/dashboard/screens/home_screen.dart +++ b/lib/features/dashboard/screens/home_screen.dart @@ -1,31 +1,47 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; + +import '../../../core/models/analytics.dart'; +import '../../../core/models/reservation.dart'; import '../../../core/providers/auth_provider.dart'; -import '../providers/dashboard_providers.dart'; -import 'dashboard_shell.dart'; -import '../../../widgets/stat_card.dart'; import '../../../widgets/error_view.dart'; import '../../../widgets/reservation_card.dart'; +import '../../../widgets/stat_card.dart'; +import '../providers/dashboard_providers.dart'; +import 'dashboard_shell.dart'; class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final cs = Theme.of(context).colorScheme; final employee = ref.watch(authProvider).employee; + final canViewRevenue = employee?.role != 'AGENT'; final metricsAsync = ref.watch(dashboardMetricsProvider); - final recentAsync = ref.watch( - reservationsProvider({'page': 1, 'pageSize': 5}), + final fallbackRecentAsync = ref.watch( + reservationsProvider(( + page: 1, + pageSize: 5, + status: null, + source: null, + search: null, + customerId: null, + )), ); final onlineAsync = ref.watch( - reservationsProvider({ - 'status': 'DRAFT', - 'source': 'MARKETPLACE', - 'page': 1, - 'pageSize': 100, - }), + reservationsProvider(( + page: 1, + pageSize: 100, + status: 'DRAFT', + source: 'MARKETPLACE', + search: null, + customerId: null, + )), ); final pendingOnline = onlineAsync.valueOrNull?.data.length ?? 0; final unreadCount = ref.watch(unreadCountProvider).valueOrNull ?? 0; @@ -35,13 +51,13 @@ class HomeScreen extends ConsumerWidget { title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Hello, ${employee?.firstName ?? 'there'}', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + const Text( + 'Dashboard', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), Text( DateFormat('EEEE, MMMM d').format(DateTime.now()), - style: const TextStyle(fontSize: 12, color: Color(0xFF6B7280)), + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), ), ], ), @@ -63,97 +79,290 @@ class HomeScreen extends ConsumerWidget { ref.invalidate(dashboardMetricsProvider); ref.invalidate(reservationsProvider); }, - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - metricsAsync.when( - loading: () => const SizedBox( - height: 200, - child: Center(child: CircularProgressIndicator()), - ), - error: (e, _) => ErrorView( - message: 'Could not load metrics', + child: metricsAsync.when( + loading: () => ListView( + padding: const EdgeInsets.all(16), + children: const [ + SizedBox(height: 240, child: Center(child: CircularProgressIndicator())), + ], + ), + error: (e, _) => ListView( + padding: const EdgeInsets.all(16), + children: [ + ErrorView( + message: 'Could not load dashboard', onRetry: () => ref.invalidate(dashboardMetricsProvider), ), - data: (metrics) => Column( - children: [ - Row( - children: [ - Expanded( - child: StatCard( - title: 'Total Vehicles', - value: '${metrics.totalVehicles}', - icon: Icons.directions_car, - color: const Color(0xFF1A56DB), - subtitle: '${metrics.availableVehicles} available', - ), - ), - const SizedBox(width: 12), - Expanded( - child: StatCard( - title: 'Active Rentals', - value: '${metrics.activeReservations}', - icon: Icons.event_available, - color: const Color(0xFF0E9F6E), - ), - ), - ], + ], + ), + data: (metrics) => ListView( + padding: const EdgeInsets.all(16), + children: [ + if (metrics.subscription != null && + metrics.subscription!.status != 'ACTIVE') + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _SubscriptionBanner( + subscription: metrics.subscription!, ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: StatCard( - title: 'Revenue', - value: - '\$${NumberFormat.compact().format(metrics.totalRevenue / 100)}', - icon: Icons.attach_money, - color: const Color(0xFF5521B5), + ), + _KpiGrid( + kpis: metrics.kpis, + canViewRevenue: canViewRevenue, + ), + if (pendingOnline > 0) ...[ + const SizedBox(height: 16), + InkWell( + onTap: () => context.push('/dashboard/online-reservations'), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + decoration: BoxDecoration( + color: const Color(0xFFFFF3CD), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFFF6B00)), + ), + child: Row( + children: [ + const Icon( + Icons.public, + color: Color(0xFFFF6B00), + size: 22, ), - ), - const SizedBox(width: 12), - Expanded( - child: StatCard( - title: 'Customers', - value: '${metrics.totalCustomers}', - icon: Icons.people, - color: const Color(0xFFB45309), + const SizedBox(width: 12), + Expanded( + child: Text( + '$pendingOnline online reservation${pendingOnline == 1 ? '' : 's'} pending approval', + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Color(0xFF92400E), + ), + ), ), - ), - ], + const Icon(Icons.chevron_right, color: Color(0xFFFF6B00)), + ], + ), ), - const SizedBox(height: 12), - Card( - child: Padding( - padding: const EdgeInsets.all(16), + ), + ], + const SizedBox(height: 16), + _SourceBreakdownSection( + sourceBreakdown: metrics.sourceBreakdown, + canViewRevenue: canViewRevenue, + ), + const SizedBox(height: 16), + _RecentReservationsSection( + analyticsReservations: metrics.recentReservations, + fallbackReservations: fallbackRecentAsync.valueOrNull?.data ?? const [], + ), + ], + ), + ), + ), + ); + } +} + +class _KpiGrid extends StatelessWidget { + final DashboardKpis kpis; + final bool canViewRevenue; + + const _KpiGrid({ + required this.kpis, + required this.canViewRevenue, + }); + + @override + Widget build(BuildContext context) { + final cards = [ + StatCard( + title: 'Total Bookings', + value: NumberFormat.decimalPattern().format(kpis.totalBookings), + icon: Icons.calendar_today_outlined, + color: const Color(0xFF1A56DB), + change: kpis.bookingsChange, + footerLabel: 'vs last month', + ), + StatCard( + title: 'Active Vehicles', + value: NumberFormat.decimalPattern().format(kpis.activeVehicles), + icon: Icons.directions_car_outlined, + color: const Color(0xFF0E9F6E), + change: kpis.vehiclesChange, + footerLabel: 'vs last month', + ), + if (canViewRevenue) + StatCard( + title: 'Monthly Revenue', + value: _formatMoney(kpis.monthlyRevenue), + icon: Icons.attach_money, + color: const Color(0xFFFF6B00), + change: kpis.revenueChange, + footerLabel: 'vs last month', + ), + StatCard( + title: 'Total Customers', + value: NumberFormat.decimalPattern().format(kpis.totalCustomers), + icon: Icons.people_outline, + color: const Color(0xFF7C3AED), + change: kpis.customersChange, + footerLabel: 'vs last month', + ), + ]; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisExtent: 170, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: cards.length, + itemBuilder: (_, i) => cards[i], + ); + } +} + +class _SourceBreakdownSection extends StatelessWidget { + final List sourceBreakdown; + final bool canViewRevenue; + + const _SourceBreakdownSection({ + required this.sourceBreakdown, + required this.canViewRevenue, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final maxCount = sourceBreakdown.isEmpty + ? 1 + : sourceBreakdown.map((e) => e.count).reduce(math.max); + + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Booking Sources', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: cs.onSurface, + ), + ), + const SizedBox(height: 14), + if (sourceBreakdown.isEmpty) + Text( + 'No booking data yet.', + style: TextStyle(color: cs.onSurfaceVariant), + ) + else + ...sourceBreakdown.map( + (item) => Padding( + padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Fleet Occupancy', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Color(0xFF374151), - ), + Row( + children: [ + Expanded( + child: Text( + _sourceLabel(item.source), + style: TextStyle( + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), + ), + ), + Text( + '${item.count} bookings', + style: TextStyle(color: cs.onSurfaceVariant), + ), + ], ), - const SizedBox(height: 12), + const SizedBox(height: 8), LinearProgressIndicator( - value: metrics.occupancyRate / 100, - backgroundColor: const Color(0xFFE5E7EB), + value: maxCount == 0 ? 0 : item.count / maxCount, + minHeight: 8, + borderRadius: BorderRadius.circular(99), + backgroundColor: cs.surfaceContainerHighest, valueColor: const AlwaysStoppedAnimation( Color(0xFF1A56DB), ), - minHeight: 8, - borderRadius: BorderRadius.circular(4), ), - const SizedBox(height: 8), - Text( - '${metrics.occupancyRate.toStringAsFixed(1)}% occupied', - style: const TextStyle( - fontSize: 13, - color: Color(0xFF6B7280), + if (canViewRevenue) ...[ + const SizedBox(height: 6), + Text( + _formatMoney(item.revenue), + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + ), + ], + ), + ), + ), + if (sourceBreakdown.isNotEmpty) ...[ + const SizedBox(height: 12), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Quick Stats', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: cs.onSurface, + ), + ), + const SizedBox(height: 12), + ...sourceBreakdown.map( + (item) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Expanded( + child: Text( + _sourceLabel(item.source), + style: TextStyle(color: cs.onSurfaceVariant), ), ), + Text( + '${item.count}', + style: TextStyle( + fontWeight: FontWeight.w700, + color: cs.onSurface, + ), + ), + if (canViewRevenue) ...[ + const SizedBox(width: 10), + Text( + _formatMoney(item.revenue), + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + ), + ), + ], ], ), ), @@ -161,54 +370,41 @@ class HomeScreen extends ConsumerWidget { ], ), ), - if (pendingOnline > 0) ...[ - const SizedBox(height: 16), - InkWell( - onTap: () => context.push('/dashboard/online-reservations'), - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - decoration: BoxDecoration( - color: const Color(0xFFFFF3CD), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFFF6B00)), - ), - child: Row( - children: [ - const Icon( - Icons.public, - color: Color(0xFFFF6B00), - size: 22, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - '$pendingOnline online request${pendingOnline == 1 ? '' : 's'} pending approval', - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Color(0xFF92400E), - ), - ), - ), - const Icon(Icons.chevron_right, color: Color(0xFFFF6B00)), - ], - ), - ), - ), - ], - const SizedBox(height: 24), + ), + ], + ], + ); + } +} + +class _RecentReservationsSection extends StatelessWidget { + final List analyticsReservations; + final List fallbackReservations; + + const _RecentReservationsSection({ + required this.analyticsReservations, + required this.fallbackReservations, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Recent Reservations', style: TextStyle( fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF111928), + fontWeight: FontWeight.w700, + color: cs.onSurface, ), ), TextButton( @@ -218,37 +414,31 @@ class HomeScreen extends ConsumerWidget { ], ), const SizedBox(height: 8), - recentAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (e, _) => const ErrorView( - message: 'Could not load recent reservations', + if (analyticsReservations.isNotEmpty) + ...analyticsReservations.map( + (reservation) => _DashboardRecentReservationCard( + reservation: reservation, + ), + ) + else if (fallbackReservations.isNotEmpty) + ...fallbackReservations.map( + (reservation) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: ReservationCard( + reservation: reservation, + onTap: () => + context.push('/dashboard/reservations/${reservation.id}'), + ), + ), + ) + else + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + 'No reservations yet', + style: TextStyle(color: cs.onSurfaceVariant), + ), ), - data: (res) => res.data.isEmpty - ? const Center( - child: Padding( - padding: EdgeInsets.all(24), - child: Text( - 'No reservations yet', - style: TextStyle(color: Color(0xFF9CA3AF)), - ), - ), - ) - : Column( - children: res.data - .map( - (r) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: ReservationCard( - reservation: r, - onTap: () => context.push( - '/dashboard/reservations/${r.id}', - ), - ), - ), - ) - .toList(), - ), - ), ], ), ), @@ -256,6 +446,206 @@ class HomeScreen extends ConsumerWidget { } } +class _DashboardRecentReservationCard extends StatelessWidget { + final DashboardRecentReservation reservation; + + const _DashboardRecentReservationCard({required this.reservation}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final dateFmt = DateFormat('MMM d'); + + return InkWell( + onTap: () => context.push('/dashboard/reservations/${reservation.id}'), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.outlineVariant), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + '#${reservation.bookingRef}', + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Color(0xFF1A56DB), + ), + ), + ), + _StatusPill(status: reservation.status), + ], + ), + const SizedBox(height: 8), + Text( + reservation.customerName, + style: TextStyle( + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + reservation.vehicleName, + style: TextStyle(color: cs.onSurfaceVariant), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + '${dateFmt.format(reservation.startDate)} - ${DateFormat('MMM d, yyyy').format(reservation.endDate)}', + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + ), + ), + ), + Text( + _formatMoney(reservation.totalAmount), + style: TextStyle( + fontWeight: FontWeight.w700, + color: cs.onSurface, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class _SubscriptionBanner extends StatelessWidget { + final DashboardSubscription subscription; + + const _SubscriptionBanner({required this.subscription}); + + @override + Widget build(BuildContext context) { + final trialEnds = subscription.trialEndsAt; + late final Color borderColor; + late final Color backgroundColor; + late final Color foregroundColor; + late final IconData icon; + late final String message; + + switch (subscription.status) { + case 'TRIALING': + borderColor = const Color(0xFF93C5FD); + backgroundColor = const Color(0xFFEFF6FF); + foregroundColor = const Color(0xFF1D4ED8); + icon = Icons.schedule_rounded; + message = trialEnds == null + ? 'Trial active' + : 'Trial ends ${DateFormat('MMM d, yyyy').format(trialEnds)}'; + break; + case 'PAST_DUE': + borderColor = const Color(0xFFFBBF24); + backgroundColor = const Color(0xFFFFFBEB); + foregroundColor = const Color(0xFFB45309); + icon = Icons.warning_amber_rounded; + message = 'Subscription payment is past due'; + break; + default: + borderColor = const Color(0xFFFCA5A5); + backgroundColor = const Color(0xFFFEF2F2); + foregroundColor = const Color(0xFFB91C1C); + icon = Icons.block_rounded; + message = 'Subscription is suspended'; + break; + } + + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: borderColor), + ), + child: Row( + children: [ + Icon(icon, color: foregroundColor), + const SizedBox(width: 10), + Expanded( + child: Text( + message, + style: TextStyle( + fontWeight: FontWeight.w600, + color: foregroundColor, + ), + ), + ), + ], + ), + ); + } +} + +class _StatusPill extends StatelessWidget { + final String status; + + const _StatusPill({required this.status}); + + @override + Widget build(BuildContext context) { + final normalized = status.toUpperCase(); + late final Color bg; + late final Color fg; + + switch (normalized) { + case 'CONFIRMED': + bg = const Color(0xFFDBEAFE); + fg = const Color(0xFF1D4ED8); + break; + case 'PENDING': + case 'DRAFT': + bg = const Color(0xFFFEF3C7); + fg = const Color(0xFFB45309); + break; + case 'ACTIVE': + bg = const Color(0xFFDCFCE7); + fg = const Color(0xFF15803D); + break; + case 'CANCELLED': + bg = const Color(0xFFFEE2E2); + fg = const Color(0xFFB91C1C); + break; + default: + bg = const Color(0xFFE5E7EB); + fg = const Color(0xFF374151); + break; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + normalized, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: fg, + ), + ), + ); + } +} + class _AvatarMenu extends ConsumerWidget { final dynamic employee; @@ -263,6 +653,7 @@ class _AvatarMenu extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final cs = Theme.of(context).colorScheme; final initial = employee?.firstName.substring(0, 1).toUpperCase() ?? 'U'; final name = employee?.fullName ?? 'Employee'; final role = employee?.role ?? ''; @@ -309,18 +700,18 @@ class _AvatarMenu extends ConsumerWidget { children: [ Text( name, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, - color: Color(0xFF111928), + color: cs.onSurface, fontSize: 14, ), ), if (role.isNotEmpty) Text( role, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: Color(0xFF6B7280), + color: cs.onSurfaceVariant, ), ), const SizedBox(height: 4), @@ -355,3 +746,24 @@ class _AvatarMenu extends ConsumerWidget { ); } } + +String _formatMoney(double amount) { + return NumberFormat.currency( + name: 'MAD', + symbol: 'MAD ', + decimalDigits: 2, + ).format(amount / 100); +} + +String _sourceLabel(String source) { + switch (source.toUpperCase()) { + case 'MARKETPLACE': + return 'Marketplace'; + case 'WALK_IN': + return 'Walk-in'; + case 'DIRECT': + return 'Direct'; + default: + return source.replaceAll('_', ' '); + } +} diff --git a/lib/features/dashboard/screens/online_reservations_screen.dart b/lib/features/dashboard/screens/online_reservations_screen.dart index 85eb29b..6cbe117 100644 --- a/lib/features/dashboard/screens/online_reservations_screen.dart +++ b/lib/features/dashboard/screens/online_reservations_screen.dart @@ -6,12 +6,14 @@ import '../../../core/models/reservation.dart'; import '../../../widgets/error_view.dart'; import '../../../widgets/status_badge.dart'; -const _params = { - 'status': 'DRAFT', - 'source': 'MARKETPLACE', - 'page': 1, - 'pageSize': 100, -}; +const ReservationQuery _params = ( + status: 'DRAFT', + source: 'MARKETPLACE', + page: 1, + pageSize: 100, + search: null, + customerId: null, +); class OnlineReservationsScreen extends ConsumerWidget { const OnlineReservationsScreen({super.key}); diff --git a/lib/features/dashboard/screens/reports_screen.dart b/lib/features/dashboard/screens/reports_screen.dart index 9f91cfe..0bc8db0 100644 --- a/lib/features/dashboard/screens/reports_screen.dart +++ b/lib/features/dashboard/screens/reports_screen.dart @@ -16,12 +16,12 @@ class _ReportsScreenState extends ConsumerState { DateTime _end = DateTime.now(); String? _statusFilter; - Map get _params => { - 'startDate': + 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, - }; + endDate: '${_end.toIso8601String().substring(0, 10)}T23:59:59.000Z', + status: _statusFilter, + ); Future _pickRange() async { final range = await showDateRangePicker( diff --git a/lib/features/dashboard/screens/reservations_screen.dart b/lib/features/dashboard/screens/reservations_screen.dart index 4a5ad26..0fbf5b3 100644 --- a/lib/features/dashboard/screens/reservations_screen.dart +++ b/lib/features/dashboard/screens/reservations_screen.dart @@ -52,12 +52,14 @@ class _ReservationsScreenState extends ConsumerState super.dispose(); } - Map _params(int tabIndex) => { - 'page': 1, - 'pageSize': 50, - if (_statuses[tabIndex] != null) 'status': _statuses[tabIndex], - if (_search.isNotEmpty) 'search': _search, - }; + ReservationQuery _params(int tabIndex) => ( + page: 1, + pageSize: 50, + status: _statuses[tabIndex], + source: null, + search: _search.isNotEmpty ? _search : null, + customerId: null, + ); @override Widget build(BuildContext context) { @@ -116,7 +118,7 @@ class _ReservationsScreenState extends ConsumerState } class _ReservationTab extends ConsumerWidget { - final Map params; + final ReservationQuery params; final void Function(String id) onTap; const _ReservationTab({required this.params, required this.onTap}); diff --git a/lib/features/dashboard/screens/vehicles_screen.dart b/lib/features/dashboard/screens/vehicles_screen.dart index 1bd9969..7df2edc 100644 --- a/lib/features/dashboard/screens/vehicles_screen.dart +++ b/lib/features/dashboard/screens/vehicles_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:dio/dio.dart'; +import '../../../core/models/vehicle.dart'; import '../providers/dashboard_providers.dart'; import '../../../widgets/vehicle_card.dart'; import '../../../widgets/loading_list.dart'; @@ -26,12 +28,45 @@ class _VehiclesScreenState extends ConsumerState { super.dispose(); } - Map get _params => { - 'page': 1, - 'pageSize': 50, - if (_statusFilter != null) 'status': _statusFilter, - if (_search.isNotEmpty) 'search': _search, - }; + VehicleQuery get _params => ( + page: 1, + pageSize: 100, + status: null, + ); + + List _filterVehicles(List vehicles) { + final query = _search.trim().toLowerCase(); + final status = _statusFilter; + + return vehicles.where((vehicle) { + if (status != null && vehicle.status != status) { + return false; + } + if (query.isEmpty) { + return true; + } + final haystack = [ + vehicle.displayName, + vehicle.licensePlate, + vehicle.status, + vehicle.category, + ].join(' ').toLowerCase(); + return haystack.contains(query); + }).toList(); + } + + String _errorMessage(Object error) { + if (error is DioException) { + final data = error.response?.data; + if (data is Map) { + final message = data['message']; + if (message is String && message.isNotEmpty) { + return message; + } + } + } + return 'Failed to load vehicles'; + } @override Widget build(BuildContext context) { @@ -48,7 +83,7 @@ class _VehiclesScreenState extends ConsumerState { child: const Icon(Icons.add), ), appBar: AppBar( - title: const Text('Vehicles'), + title: const Text('Fleet'), bottom: PreferredSize( preferredSize: const Size.fromHeight(60), child: Padding( @@ -86,34 +121,40 @@ class _VehiclesScreenState extends ConsumerState { body: vehiclesAsync.when( loading: () => const LoadingList(itemHeight: 240), error: (e, _) => ErrorView( - message: 'Failed to load vehicles', + message: _errorMessage(e), onRetry: () => ref.invalidate(vehiclesProvider(_params)), ), - data: (res) => res.data.isEmpty - ? const Center( + data: (res) { + final filteredVehicles = _filterVehicles(res.data); + + return filteredVehicles.isEmpty + ? const Center( child: Text( 'No vehicles found', style: TextStyle(color: Color(0xFF9CA3AF)), ), ) - : RefreshIndicator( - onRefresh: () async => - ref.invalidate(vehiclesProvider(_params)), - child: GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 1, - mainAxisExtent: 240, - mainAxisSpacing: 12, + : RefreshIndicator( + onRefresh: () async => + ref.invalidate(vehiclesProvider(_params)), + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 1, + mainAxisExtent: 240, + mainAxisSpacing: 12, + ), + itemCount: filteredVehicles.length, + itemBuilder: (_, i) => VehicleCard( + vehicle: filteredVehicles[i], + onTap: () => context.push( + '/dashboard/fleet/${filteredVehicles[i].id}', + ), + ), ), - itemCount: res.data.length, - itemBuilder: (_, i) => VehicleCard( - vehicle: res.data[i], - onTap: () => - context.push('/dashboard/vehicles/${res.data[i].id}'), - ), - ), - ), + ); + }, ), ); } diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 0ef463d..f546020 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -1,6 +1,6 @@ { "@@locale": "ar", - "appName": "RentalDriveGo", + "appName": "KriTomobil", "tagline": "أسهل طريقة للعثور\nوحجز سيارتك المثالية", "findYourCar": "ابحث عن سيارتك", "companySignIn": "وكالة السيارات", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f8189cd..a8fbda1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "appName": "RentalDriveGo", + "appName": "KriTomobil", "tagline": "The easiest way to find\nand reserve your perfect car", "findYourCar": "Find your car", "companySignIn": "Space Agency", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index cbf495a..d5dd8b1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,6 +1,6 @@ { "@@locale": "fr", - "appName": "RentalDriveGo", + "appName": "KriTomobil", "tagline": "La façon la plus simple de trouver\net réserver la voiture parfaite", "findYourCar": "Trouver ma voiture", "companySignIn": "Espace Agence", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d48db3c..3fcc97a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -103,7 +103,7 @@ abstract class AppLocalizations { /// No description provided for @appName. /// /// In en, this message translates to: - /// **'RentalDriveGo'** + /// **'KriTomobil'** String get appName; /// No description provided for @tagline. diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index a2f6a45..b865d1b 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -9,7 +9,7 @@ class AppLocalizationsAr extends AppLocalizations { AppLocalizationsAr([String locale = 'ar']) : super(locale); @override - String get appName => 'RentalDriveGo'; + String get appName => 'KriTomobil'; @override String get tagline => 'أسهل طريقة للعثور\nوحجز سيارتك المثالية'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c4b0a38..d94fee4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -9,7 +9,7 @@ class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override - String get appName => 'RentalDriveGo'; + String get appName => 'KriTomobil'; @override String get tagline => 'The easiest way to find\nand reserve your perfect car'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7df8f66..7bb45c1 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -9,7 +9,7 @@ class AppLocalizationsFr extends AppLocalizations { AppLocalizationsFr([String locale = 'fr']) : super(locale); @override - String get appName => 'RentalDriveGo'; + String get appName => 'KriTomobil'; @override String get tagline => diff --git a/lib/widgets/app_back_button.dart b/lib/widgets/app_back_button.dart new file mode 100644 index 0000000..fb57187 --- /dev/null +++ b/lib/widgets/app_back_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class AppBackButton extends StatelessWidget { + final String fallbackRoute; + final Color? color; + + const AppBackButton({ + super.key, + required this.fallbackRoute, + this.color, + }); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(Icons.arrow_back_ios_new_rounded, color: color), + onPressed: () { + if (context.canPop()) { + context.pop(); + return; + } + context.go(fallbackRoute); + }, + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ); + } +} diff --git a/lib/widgets/error_view.dart b/lib/widgets/error_view.dart index 0777124..fd18c59 100644 --- a/lib/widgets/error_view.dart +++ b/lib/widgets/error_view.dart @@ -8,6 +8,8 @@ class ErrorView extends StatelessWidget { @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Center( child: Padding( padding: const EdgeInsets.all(24), @@ -19,7 +21,7 @@ class ErrorView extends StatelessWidget { Text( message, textAlign: TextAlign.center, - style: const TextStyle(color: Color(0xFF6B7280)), + style: TextStyle(color: cs.onSurfaceVariant), ), if (onRetry != null) ...[ const SizedBox(height: 16), diff --git a/lib/widgets/loading_list.dart b/lib/widgets/loading_list.dart index ba51d25..39b86a7 100644 --- a/lib/widgets/loading_list.dart +++ b/lib/widgets/loading_list.dart @@ -9,9 +9,12 @@ class LoadingList extends StatelessWidget { @override Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Shimmer.fromColors( - baseColor: const Color(0xFFE5E7EB), - highlightColor: const Color(0xFFF9FAFB), + baseColor: isDark ? const Color(0xFF2F3145) : const Color(0xFFE5E7EB), + highlightColor: + isDark ? const Color(0xFF3A3C54) : const Color(0xFFF9FAFB), child: ListView.separated( padding: const EdgeInsets.all(16), itemCount: count, @@ -19,7 +22,7 @@ class LoadingList extends StatelessWidget { itemBuilder: (_, _) => Container( height: itemHeight, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF28293A) : Colors.white, borderRadius: BorderRadius.circular(12), ), ), diff --git a/lib/widgets/reservation_card.dart b/lib/widgets/reservation_card.dart index ba9d806..bdccbe9 100644 --- a/lib/widgets/reservation_card.dart +++ b/lib/widgets/reservation_card.dart @@ -12,6 +12,8 @@ class ReservationCard extends StatelessWidget { @override Widget build(BuildContext context) { final fmt = DateFormat('MMM d, yyyy'); + final cs = Theme.of(context).colorScheme; + return Card( child: InkWell( onTap: onTap, @@ -39,34 +41,44 @@ class ReservationCard extends StatelessWidget { const SizedBox(height: 4), Text( reservation.customer!.fullName, - style: const TextStyle( - fontSize: 13, color: Color(0xFF6B7280)), + style: TextStyle( + fontSize: 13, + color: cs.onSurfaceVariant, + ), ), ], const SizedBox(height: 10), Row( children: [ - const Icon(Icons.calendar_today, - size: 14, color: Color(0xFF9CA3AF)), + Icon( + Icons.calendar_today, + size: 14, + color: cs.onSurfaceVariant.withValues(alpha: 0.8), + ), const SizedBox(width: 4), Text( '${fmt.format(reservation.startDate)} → ${fmt.format(reservation.endDate)}', - style: const TextStyle( - fontSize: 13, color: Color(0xFF6B7280)), + style: TextStyle( + fontSize: 13, + color: cs.onSurfaceVariant, + ), ), ], ), const SizedBox(height: 6), Row( children: [ - const Icon(Icons.attach_money, - size: 14, color: Color(0xFF9CA3AF)), + Icon( + Icons.attach_money, + size: 14, + color: cs.onSurfaceVariant.withValues(alpha: 0.8), + ), const SizedBox(width: 4), Text( '\$${(reservation.totalAmount / 100).toStringAsFixed(2)}', - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, - color: Color(0xFF111928), + color: cs.onSurface, ), ), const SizedBox(width: 8), diff --git a/lib/widgets/stat_card.dart b/lib/widgets/stat_card.dart index d1375e4..86f4de8 100644 --- a/lib/widgets/stat_card.dart +++ b/lib/widgets/stat_card.dart @@ -6,6 +6,8 @@ class StatCard extends StatelessWidget { final IconData icon; final Color color; final String? subtitle; + final double? change; + final String? footerLabel; const StatCard({ super.key, @@ -14,10 +16,14 @@ class StatCard extends StatelessWidget { required this.icon, required this.color, this.subtitle, + this.change, + this.footerLabel, }); @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Card( child: Padding( padding: const EdgeInsets.all(16), @@ -40,30 +46,62 @@ class StatCard extends StatelessWidget { const SizedBox(height: 12), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Color(0xFF111928), + color: cs.onSurface, ), ), const SizedBox(height: 4), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 13, - color: Color(0xFF6B7280), + color: cs.onSurfaceVariant, ), ), if (subtitle != null) ...[ const SizedBox(height: 4), Text( subtitle!, - style: const TextStyle( + style: TextStyle( fontSize: 11, - color: Color(0xFF9CA3AF), + color: cs.onSurfaceVariant.withValues(alpha: 0.85), ), ), ], + if (change != null) ...[ + const SizedBox(height: 6), + Row( + children: [ + Text( + '${change! >= 0 ? '+' : ''}${change!.toStringAsFixed(1)}%', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: change! > 0 + ? const Color(0xFF0E9F6E) + : change! < 0 + ? const Color(0xFFE02424) + : cs.onSurfaceVariant, + ), + ), + if (footerLabel != null) ...[ + const SizedBox(width: 6), + Expanded( + child: Text( + footerLabel!, + style: TextStyle( + fontSize: 11, + color: cs.onSurfaceVariant.withValues(alpha: 0.85), + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ], + ), + ], ], ), ), diff --git a/lib/widgets/vehicle_card.dart b/lib/widgets/vehicle_card.dart index d79ed49..bbc5267 100644 --- a/lib/widgets/vehicle_card.dart +++ b/lib/widgets/vehicle_card.dart @@ -12,6 +12,8 @@ class VehicleCard extends StatelessWidget { @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Card( clipBehavior: Clip.antiAlias, child: InkWell( @@ -44,9 +46,9 @@ class VehicleCard extends StatelessWidget { const SizedBox(height: 4), Text( vehicle.licensePlate, - style: const TextStyle( + style: TextStyle( fontSize: 13, - color: Color(0xFF6B7280), + color: cs.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -66,13 +68,15 @@ class VehicleCard extends StatelessWidget { padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( - color: const Color(0xFFF3F4F6), + color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), child: Text( vehicle.category, - style: const TextStyle( - fontSize: 11, color: Color(0xFF6B7280)), + style: TextStyle( + fontSize: 11, + color: cs.onSurfaceVariant, + ), ), ), ],