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,
+ ),
),
),
],