diff --git a/.gitignore b/.gitignore
index 3820a95..ed76329 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Environment configs (contain URLs — do not commit)
+env/
+
# Miscellaneous
*.class
*.log
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 399f698..d08cc9c 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -4,4 +4,6 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
+
+
diff --git a/build_prod_andr.md b/build_prod_andr.md
new file mode 100644
index 0000000..cc4e006
--- /dev/null
+++ b/build_prod_andr.md
@@ -0,0 +1,17 @@
+Run it from the project root:
+
+ ./build_prod_apk.sh
+
+ The APK lands at build/outputs/rentaldrivego-release.apk.
+
+ One thing to sort out before distributing — the build.gradle.kts currently signs with the debug
+ key in release mode:
+ signingConfig = signingConfigs.getByName("debug")
+ That's fine for internal testing, but app stores and most MDM systems require a proper release
+ keystore. When you're ready to publish:
+
+ 1. Generate a keystore once:
+ keytool -genkey -v -keystore android/rentaldrivego.jks \
+ -alias rentaldrivego -keyalg RSA -keysize 2048 -validity 10000
+ 2. Add the signing config to android/app/build.gradle.kts pointing at that keystore
+ 3. Add android/rentaldrivego.jks to .gitignore
\ No newline at end of file
diff --git a/build_prod_apk.sh b/build_prod_apk.sh
new file mode 100755
index 0000000..a78c64f
--- /dev/null
+++ b/build_prod_apk.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# ── Config ────────────────────────────────────────────────────────────────────
+ENV_FILE="env/prod.json"
+OUTPUT_DIR="build/outputs"
+APK_SRC="build/app/outputs/flutter-apk/app-release.apk"
+APK_DEST="$OUTPUT_DIR/rentaldrivego-release.apk"
+
+# ── Checks ────────────────────────────────────────────────────────────────────
+cd "$(dirname "$0")"
+
+if ! command -v flutter &>/dev/null; then
+ echo "❌ flutter not found in PATH"
+ exit 1
+fi
+
+if [[ ! -f "$ENV_FILE" ]]; then
+ echo "❌ $ENV_FILE not found — create it with production API URLs"
+ exit 1
+fi
+
+# ── Build ─────────────────────────────────────────────────────────────────────
+echo "▶ Cleaning previous build..."
+flutter clean
+
+echo "▶ Fetching dependencies..."
+flutter pub get
+
+echo "▶ Building release APK (production)..."
+flutter build apk --release --dart-define-from-file="$ENV_FILE"
+
+# ── Copy to output ────────────────────────────────────────────────────────────
+mkdir -p "$OUTPUT_DIR"
+cp "$APK_SRC" "$APK_DEST"
+
+echo ""
+echo "✅ Build complete!"
+echo " APK → $APK_DEST"
+echo " Size: $(du -sh "$APK_DEST" | cut -f1)"
diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart
index 0455d99..25dc943 100644
--- a/lib/core/constants/app_constants.dart
+++ b/lib/core/constants/app_constants.dart
@@ -3,21 +3,39 @@ import 'dart:io';
class AppConstants {
static const String apiBaseUrlOverride =
String.fromEnvironment('API_BASE_URL');
- static const String developmentAndroidApiUrl = 'http://10.0.2.2:4000/api/v1';
- static const String developmentLocalApiUrl = 'http://localhost:4000/api/v1';
+ static const String dashboardUrlOverride =
+ String.fromEnvironment('DASHBOARD_URL');
+
+ // Production
+ static const String productionApiUrl = 'https://api.rentaldrivego.ma/api/v1';
+ static const String productionDashboardUrl = 'https://rentaldrivego.ma/dashboard';
+
+ // Development
+ static const String devAndroidApiUrl = 'http://10.0.2.2:4000/api/v1';
+ static const String devLocalApiUrl = 'http://localhost:4000/api/v1';
+ static const String devAndroidDashboardUrl = 'http://10.0.2.2:3001';
+ static const String devLocalDashboardUrl = 'http://localhost:3001';
- // Dev builds stay local by default. Real phones still need a LAN-reachable
- // override because their localhost is the phone itself, not the dev machine.
static String get baseUrl {
- if (apiBaseUrlOverride.isNotEmpty) {
- return apiBaseUrlOverride;
- }
+ if (apiBaseUrlOverride.isNotEmpty) return apiBaseUrlOverride;
+ if (Platform.isAndroid) return devAndroidApiUrl;
+ return devLocalApiUrl;
+ }
- if (Platform.isAndroid) {
- return developmentAndroidApiUrl;
- }
+ static String get dashboardBaseUrl {
+ if (dashboardUrlOverride.isNotEmpty) return dashboardUrlOverride;
+ if (Platform.isAndroid) return devAndroidDashboardUrl;
+ return devLocalDashboardUrl;
+ }
- return developmentLocalApiUrl;
+ static String resolveMediaUrl(String url) {
+ if (!url.startsWith('http')) {
+ return '${baseUrl.replaceAll('/api/v1', '')}$url';
+ }
+ if (Platform.isAndroid && url.contains('localhost')) {
+ return url.replaceFirst('localhost', '10.0.2.2');
+ }
+ return url;
}
static const String tokenKey = 'auth_token';
diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart
index 93a8956..4e024df 100644
--- a/lib/core/router/app_router.dart
+++ b/lib/core/router/app_router.dart
@@ -10,10 +10,13 @@ import '../../features/auth/screens/employee_login_screen.dart';
import '../../features/dashboard/screens/home_screen.dart';
import '../../features/dashboard/screens/vehicles_screen.dart';
import '../../features/dashboard/screens/vehicle_detail_screen.dart';
+import '../../features/dashboard/screens/vehicle_form_screen.dart';
import '../../features/dashboard/screens/reservations_screen.dart';
import '../../features/dashboard/screens/reservation_detail_screen.dart';
+import '../../features/dashboard/screens/new_reservation_screen.dart';
import '../../features/dashboard/screens/customers_screen.dart';
import '../../features/dashboard/screens/customer_detail_screen.dart';
+import '../../features/dashboard/screens/customer_form_screen.dart';
import '../../features/dashboard/screens/notifications_screen.dart';
import '../../features/dashboard/screens/online_reservations_screen.dart';
import '../../features/dashboard/screens/contract_screen.dart';
@@ -25,7 +28,10 @@ import '../../features/dashboard/screens/team_screen.dart';
import '../../features/client/screens/client_home_screen.dart';
import '../../features/client/screens/client_vehicle_detail_screen.dart';
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(
@@ -52,7 +58,13 @@ final routerProvider = Provider((ref) {
}
if (status == AuthStatus.unauthenticated) {
- if (isLanding || isLogin || loc.startsWith('/client')) return null;
+ if (isLanding ||
+ isLogin ||
+ loc.startsWith('/client') ||
+ loc == '/role' ||
+ loc == '/agency') {
+ return null;
+ }
return '/landing';
}
@@ -156,6 +168,42 @@ final routerProvider = Provider((ref) {
vehicle: state.extra as MarketplaceVehicle?,
),
),
+ GoRoute(
+ path: '/booking-confirmation',
+ builder: (_, state) {
+ final result = state.extra as BookingResult?;
+ if (result == null) return const ClientHomeScreen();
+ return BookingConfirmationScreen(result: result);
+ },
+ ),
+ GoRoute(
+ path: '/agency',
+ builder: (context, _) => const AgencyDashboardWebViewScreen(),
+ ),
+ GoRoute(
+ path: '/dashboard/reservations/new',
+ builder: (context, _) => const NewReservationScreen(),
+ ),
+ GoRoute(
+ path: '/dashboard/vehicles/new',
+ builder: (context, _) => const VehicleFormScreen(),
+ ),
+ GoRoute(
+ path: '/dashboard/vehicles/:id/edit',
+ builder: (_, state) => VehicleFormScreen(
+ vehicle: state.extra as dynamic,
+ ),
+ ),
+ GoRoute(
+ path: '/dashboard/customers/new',
+ builder: (context, _) => const CustomerFormScreen(),
+ ),
+ GoRoute(
+ path: '/dashboard/customers/:id/edit',
+ builder: (_, state) => CustomerFormScreen(
+ customer: state.extra as dynamic,
+ ),
+ ),
],
);
diff --git a/lib/core/services/api_client.dart b/lib/core/services/api_client.dart
index 6037de2..a3ff994 100644
--- a/lib/core/services/api_client.dart
+++ b/lib/core/services/api_client.dart
@@ -34,6 +34,12 @@ class ApiClient {
handler.next(response);
},
onError: (e, handler) {
+ // Log API errors to help diagnose issues during development.
+ final resp = e.response;
+ if (resp != null) {
+ // ignore: avoid_print
+ print('[API] ${e.requestOptions.method} ${e.requestOptions.path} → ${resp.statusCode}: ${resp.data}');
+ }
handler.next(e);
},
));
diff --git a/lib/core/services/marketplace_service.dart b/lib/core/services/marketplace_service.dart
index c1f5e5f..11e449d 100644
--- a/lib/core/services/marketplace_service.dart
+++ b/lib/core/services/marketplace_service.dart
@@ -74,7 +74,8 @@ class MarketplaceService {
required String firstName,
required String lastName,
required String email,
- String? phone,
+ required String phone,
+ required String driverLicense,
required DateTime startDate,
required DateTime endDate,
String? notes,
@@ -86,7 +87,8 @@ class MarketplaceService {
'firstName': firstName,
'lastName': lastName,
'email': email,
- if (phone != null && phone.isNotEmpty) 'phone': phone,
+ 'phone': phone,
+ 'driverLicense': driverLicense,
'startDate': '${startDate.toIso8601String().substring(0, 10)}T00:00:00.000Z',
'endDate': '${endDate.toIso8601String().substring(0, 10)}T00:00:00.000Z',
if (notes != null && notes.isNotEmpty) 'notes': notes,
diff --git a/lib/features/agency_webview/screens/agency_dashboard_webview_screen.dart b/lib/features/agency_webview/screens/agency_dashboard_webview_screen.dart
new file mode 100644
index 0000000..744ed16
--- /dev/null
+++ b/lib/features/agency_webview/screens/agency_dashboard_webview_screen.dart
@@ -0,0 +1,149 @@
+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';
+
+class AgencyDashboardWebViewScreen extends StatefulWidget {
+ const AgencyDashboardWebViewScreen({super.key});
+
+ @override
+ State createState() =>
+ _AgencyDashboardWebViewScreenState();
+}
+
+class _AgencyDashboardWebViewScreenState
+ extends State {
+ late final WebViewController _controller;
+ bool _isLoading = true;
+ bool _hasError = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setNavigationDelegate(
+ NavigationDelegate(
+ onPageStarted: (_) => setState(() {
+ _isLoading = true;
+ _hasError = false;
+ }),
+ onPageFinished: (_) => setState(() => _isLoading = false),
+ onWebResourceError: (_) => setState(() {
+ _isLoading = false;
+ _hasError = true;
+ }),
+ ),
+ )
+ ..loadRequest(
+ Uri.parse('${AppConstants.dashboardBaseUrl}/dashboard'),
+ );
+ }
+
+ void _reload() {
+ setState(() {
+ _hasError = false;
+ _isLoading = true;
+ });
+ _controller.reload();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Agency Dashboard'),
+ leading: BackButton(onPressed: () => context.go('/landing')),
+ actions: [
+ if (!_isLoading)
+ IconButton(
+ icon: const Icon(Icons.refresh_rounded),
+ onPressed: _reload,
+ tooltip: 'Reload',
+ ),
+ PopupMenuButton(
+ onSelected: (value) {
+ if (value == 'employee_login') {
+ context.push('/login/employee');
+ }
+ },
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ itemBuilder: (_) => [
+ const PopupMenuItem(
+ value: 'employee_login',
+ child: Row(
+ children: [
+ Icon(Icons.login_rounded, size: 18),
+ SizedBox(width: 10),
+ Text('Employee App Login'),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ body: Stack(
+ children: [
+ WebViewWidget(controller: _controller),
+ if (_isLoading)
+ const LinearProgressIndicator(minHeight: 3),
+ if (_hasError && !_isLoading)
+ _ErrorOverlay(onRetry: _reload),
+ ],
+ ),
+ );
+ }
+}
+
+class _ErrorOverlay extends StatelessWidget {
+ final VoidCallback onRetry;
+
+ const _ErrorOverlay({required this.onRetry});
+
+ @override
+ Widget build(BuildContext context) {
+ final cs = Theme.of(context).colorScheme;
+ return ColoredBox(
+ color: cs.surface,
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.cloud_off_rounded,
+ size: 56,
+ color: cs.onSurfaceVariant,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Could not load dashboard',
+ style: TextStyle(
+ fontSize: 17,
+ fontWeight: FontWeight.w600,
+ color: cs.onSurface,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Make sure the agency dashboard server is running.',
+ style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 24),
+ OutlinedButton.icon(
+ onPressed: onRetry,
+ icon: const Icon(Icons.refresh_rounded),
+ label: const Text('Try Again'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/auth/screens/landing_screen.dart b/lib/features/auth/screens/landing_screen.dart
index 15076e4..878fd23 100644
--- a/lib/features/auth/screens/landing_screen.dart
+++ b/lib/features/auth/screens/landing_screen.dart
@@ -86,7 +86,7 @@ class LandingScreen extends StatelessWidget {
),
const SizedBox(height: 16),
TextButton(
- onPressed: () => context.push('/login/employee'),
+ onPressed: () => context.go('/agency'),
style: TextButton.styleFrom(
foregroundColor: Colors.white.withValues(alpha: 0.5),
),
diff --git a/lib/features/client/models/booking_result.dart b/lib/features/client/models/booking_result.dart
new file mode 100644
index 0000000..762f933
--- /dev/null
+++ b/lib/features/client/models/booking_result.dart
@@ -0,0 +1,21 @@
+import '../../../core/models/marketplace.dart';
+
+class BookingResult {
+ final String reservationId;
+ final MarketplaceVehicle vehicle;
+ final DateTime startDate;
+ final DateTime endDate;
+
+ const BookingResult({
+ required this.reservationId,
+ required this.vehicle,
+ required this.startDate,
+ required this.endDate,
+ });
+
+ int get days => endDate.difference(startDate).inDays.clamp(1, 9999);
+ double get total => vehicle.dailyRateCents * days / 100;
+ String get shortRef => reservationId.length >= 8
+ ? reservationId.substring(0, 8).toUpperCase()
+ : reservationId.toUpperCase();
+}
diff --git a/lib/features/client/screens/booking_confirmation_screen.dart b/lib/features/client/screens/booking_confirmation_screen.dart
new file mode 100644
index 0000000..14dcae4
--- /dev/null
+++ b/lib/features/client/screens/booking_confirmation_screen.dart
@@ -0,0 +1,223 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:intl/intl.dart';
+import '../models/booking_result.dart';
+
+const _orange = Color(0xFFFF6B00);
+const _blue = Color(0xFF1A56DB);
+
+class BookingConfirmationScreen extends StatelessWidget {
+ final BookingResult result;
+
+ const BookingConfirmationScreen({super.key, required this.result});
+
+ @override
+ Widget build(BuildContext context) {
+ final fmt = DateFormat('MMM d, yyyy');
+ final cs = Theme.of(context).colorScheme;
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Booking Confirmed'),
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.fromLTRB(24, 32, 24, 40),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ // Success badge
+ Container(
+ padding: const EdgeInsets.all(28),
+ decoration: const BoxDecoration(
+ color: Color(0xFFDEF7EC),
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(
+ Icons.check_circle_outline_rounded,
+ color: Color(0xFF057A55),
+ size: 60,
+ ),
+ ),
+ const SizedBox(height: 24),
+ const Text(
+ 'Booking Confirmed!',
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ result.vehicle.displayName,
+ style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16),
+ textAlign: TextAlign.center,
+ ),
+ Text(
+ result.vehicle.company.brand.displayName,
+ style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
+ ),
+ const SizedBox(height: 32),
+
+ // Booking summary card
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: cs.surfaceContainerHigh,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: cs.outlineVariant),
+ ),
+ child: Column(
+ children: [
+ // Reference number
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 10,
+ ),
+ decoration: BoxDecoration(
+ color: _blue.withValues(alpha: 0.08),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'Booking Reference',
+ style: TextStyle(
+ color: cs.onSurfaceVariant,
+ fontSize: 13,
+ ),
+ ),
+ Text(
+ result.shortRef,
+ style: const TextStyle(
+ color: _blue,
+ fontWeight: FontWeight.bold,
+ fontSize: 17,
+ letterSpacing: 2,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ _InfoRow(
+ label: 'Pick-up date',
+ value: fmt.format(result.startDate),
+ ),
+ const SizedBox(height: 10),
+ _InfoRow(
+ label: 'Drop-off date',
+ value: fmt.format(result.endDate),
+ ),
+ const SizedBox(height: 10),
+ _InfoRow(
+ label: 'Duration',
+ value:
+ '${result.days} day${result.days == 1 ? '' : 's'}',
+ ),
+ const Divider(height: 24),
+ _InfoRow(
+ label: 'Daily rate',
+ value:
+ '${result.vehicle.dailyRate.toStringAsFixed(2)} MAD/day',
+ ),
+ const SizedBox(height: 10),
+ _InfoRow(
+ label: 'Total',
+ value: '${result.total.toStringAsFixed(2)} MAD',
+ valueStyle: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 17,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Email notice
+ Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF0F9FF),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: const Color(0xFFBAE6FD)),
+ ),
+ child: const Row(
+ children: [
+ Icon(
+ Icons.email_outlined,
+ color: Color(0xFF0369A1),
+ size: 18,
+ ),
+ SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ 'A confirmation has been sent to your email.',
+ style: TextStyle(
+ color: Color(0xFF0369A1),
+ fontSize: 13,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 32),
+
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () => context.go('/client'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _orange,
+ foregroundColor: Colors.white,
+ minimumSize: const Size.fromHeight(52),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(14),
+ ),
+ elevation: 0,
+ ),
+ child: const Text(
+ 'Search More Cars',
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _InfoRow extends StatelessWidget {
+ final String label;
+ final String value;
+ final TextStyle? valueStyle;
+
+ const _InfoRow({required this.label, required this.value, this.valueStyle});
+
+ @override
+ Widget build(BuildContext context) {
+ final cs = Theme.of(context).colorScheme;
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ label,
+ style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
+ ),
+ Text(
+ value,
+ style: valueStyle ??
+ TextStyle(
+ color: cs.onSurface,
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/features/client/screens/booking_screen.dart b/lib/features/client/screens/booking_screen.dart
index 77801fb..72ccda9 100644
--- a/lib/features/client/screens/booking_screen.dart
+++ b/lib/features/client/screens/booking_screen.dart
@@ -3,8 +3,12 @@ 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 '../models/booking_result.dart';
import '../providers/client_providers.dart';
+const _orange = Color(0xFFFF6B00);
+const _blue = Color(0xFF1A56DB);
+
class BookingScreen extends ConsumerStatefulWidget {
final String vehicleId;
final MarketplaceVehicle? vehicle;
@@ -25,6 +29,7 @@ class _BookingScreenState extends ConsumerState {
final _lastNameCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _phoneCtrl = TextEditingController();
+ final _licenseCtrl = TextEditingController();
final _notesCtrl = TextEditingController();
bool _loading = false;
@@ -36,6 +41,7 @@ class _BookingScreenState extends ConsumerState {
_lastNameCtrl.dispose();
_emailCtrl.dispose();
_phoneCtrl.dispose();
+ _licenseCtrl.dispose();
_notesCtrl.dispose();
super.dispose();
}
@@ -45,13 +51,13 @@ class _BookingScreenState extends ConsumerState {
final params = ref.read(searchParamsProvider);
if (!params.hasDateRange) {
- setState(() => _error = 'Please select dates before booking.');
+ setState(() => _error = 'Please select rental dates before booking.');
return;
}
final vehicle = widget.vehicle;
if (vehicle == null) {
- setState(() => _error = 'Vehicle information missing.');
+ setState(() => _error = 'Vehicle information missing. Please go back and try again.');
return;
}
@@ -61,6 +67,9 @@ class _BookingScreenState extends ConsumerState {
});
try {
+ final locale = Localizations.localeOf(context).languageCode;
+ final lang = ['en', 'fr', 'ar'].contains(locale) ? locale : 'en';
+
final reservationId =
await ref.read(marketplaceServiceProvider).createReservation(
vehicleId: vehicle.id,
@@ -68,110 +77,43 @@ class _BookingScreenState extends ConsumerState {
firstName: _firstNameCtrl.text.trim(),
lastName: _lastNameCtrl.text.trim(),
email: _emailCtrl.text.trim(),
- phone: _phoneCtrl.text.trim().isEmpty
- ? null
- : _phoneCtrl.text.trim(),
+ phone: _phoneCtrl.text.trim(),
+ driverLicense: _licenseCtrl.text.trim(),
startDate: params.startDate!,
endDate: params.endDate!,
notes: _notesCtrl.text.trim().isEmpty
? null
: _notesCtrl.text.trim(),
+ language: lang,
);
- if (mounted) _showSuccess(reservationId, vehicle, params);
+ if (mounted) {
+ context.pushReplacement(
+ '/booking-confirmation',
+ extra: BookingResult(
+ reservationId: reservationId,
+ vehicle: vehicle,
+ startDate: params.startDate!,
+ endDate: params.endDate!,
+ ),
+ );
+ }
} catch (e) {
- setState(() =>
- _error = 'Booking failed. Please check your details and try again.');
+ setState(() => _error = _parseError(e));
} finally {
if (mounted) setState(() => _loading = false);
}
}
- void _showSuccess(
- String reservationId, MarketplaceVehicle v, SearchParams params) {
- final fmt = DateFormat('MMM d, yyyy');
- final days = params.endDate!.difference(params.startDate!).inDays;
- final total = v.dailyRateCents * days / 100;
-
- showDialog(
- context: context,
- barrierDismissible: false,
- builder: (_) => AlertDialog(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- const SizedBox(height: 8),
- Container(
- padding: const EdgeInsets.all(16),
- decoration: const BoxDecoration(
- color: Color(0xFFDEF7EC),
- shape: BoxShape.circle,
- ),
- child: const Icon(Icons.check,
- color: Color(0xFF057A55), size: 32),
- ),
- const SizedBox(height: 16),
- const Text('Booking Confirmed!',
- style: TextStyle(
- fontSize: 18, fontWeight: FontWeight.bold)),
- const SizedBox(height: 8),
- Text(
- v.displayName,
- style: const TextStyle(
- color: Color(0xFF6B7280), fontSize: 14),
- ),
- const SizedBox(height: 12),
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: const Color(0xFFF9FAFB),
- borderRadius: BorderRadius.circular(10),
- ),
- child: Column(
- children: [
- _ConfirmRow(
- label: 'Dates',
- value:
- '${fmt.format(params.startDate!)} → ${fmt.format(params.endDate!)}'),
- const SizedBox(height: 6),
- _ConfirmRow(
- label: 'Duration', value: '$days days'),
- const SizedBox(height: 6),
- _ConfirmRow(
- label: 'Total',
- value: '\$${total.toStringAsFixed(2)}'),
- const SizedBox(height: 6),
- _ConfirmRow(
- label: 'Reference',
- value: reservationId.substring(0, 8).toUpperCase(),
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- const Text(
- 'A confirmation has been sent\nto your email.',
- textAlign: TextAlign.center,
- style: TextStyle(
- fontSize: 12, color: Color(0xFF9CA3AF)),
- ),
- ],
- ),
- actions: [
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: () {
- Navigator.pop(context);
- context.go('/client');
- },
- child: const Text('Back to Search'),
- ),
- ),
- ],
- ),
- );
+ String _parseError(Object e) {
+ final s = e.toString().toLowerCase();
+ if (s.contains('unavailable') || s.contains('409')) {
+ return 'This vehicle is not available for the selected dates.';
+ }
+ if (s.contains('socket') || s.contains('connection')) {
+ return 'Could not reach the server. Check your connection and try again.';
+ }
+ return 'Booking failed. Please check your details and try again.';
}
@override
@@ -180,7 +122,7 @@ class _BookingScreenState extends ConsumerState {
final params = ref.watch(searchParamsProvider);
final hasDates = params.hasDateRange;
final days = hasDates
- ? params.endDate!.difference(params.startDate!).inDays
+ ? params.endDate!.difference(params.startDate!).inDays.clamp(1, 9999)
: 0;
final fmt = DateFormat('MMM d, yyyy');
final total = vehicle != null && hasDates
@@ -192,52 +134,55 @@ class _BookingScreenState extends ConsumerState {
body: Form(
key: _formKey,
child: ListView(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 40),
children: [
- // Vehicle summary
- if (vehicle != null)
+ // ── Vehicle summary ───────────────────────────────────────────
+ if (vehicle != null) ...[
+ const SizedBox(height: 16),
Card(
- margin: const EdgeInsets.only(bottom: 16),
+ margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
- const Icon(Icons.directions_car,
- color: Color(0xFF1A56DB), size: 28),
+ const Icon(Icons.directions_car, color: _blue, size: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(vehicle.displayName,
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 15)),
- Text(vehicle.company.brand.displayName,
- style: const TextStyle(
- color: Color(0xFF6B7280),
- fontSize: 12)),
+ Text(
+ vehicle.displayName,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold, fontSize: 15),
+ ),
+ Text(
+ vehicle.company.brand.displayName,
+ style: const TextStyle(
+ color: Color(0xFF6B7280), fontSize: 12),
+ ),
],
),
),
if (total != null)
Text(
- '\$${total.toStringAsFixed(0)}',
+ '${total.toStringAsFixed(0)} MAD',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
- color: Color(0xFF1A56DB),
+ color: _blue,
),
),
],
),
),
),
+ ],
- // Date summary
- if (hasDates)
+ // ── Date summary ──────────────────────────────────────────────
+ if (hasDates) ...[
+ const SizedBox(height: 10),
Container(
- margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFEBF5FF),
@@ -246,23 +191,24 @@ class _BookingScreenState extends ConsumerState {
),
child: Row(
children: [
- const Icon(Icons.calendar_today,
- color: Color(0xFF1A56DB), size: 16),
+ const Icon(Icons.calendar_today, color: _blue, size: 16),
const SizedBox(width: 10),
- Text(
- '${fmt.format(params.startDate!)} → ${fmt.format(params.endDate!)} · $days day${days == 1 ? '' : 's'}',
- style: const TextStyle(
- color: Color(0xFF1A56DB),
- fontWeight: FontWeight.w600,
- fontSize: 13,
+ Expanded(
+ child: Text(
+ '${fmt.format(params.startDate!)} → ${fmt.format(params.endDate!)} · $days day${days == 1 ? '' : 's'}',
+ style: const TextStyle(
+ color: _blue,
+ fontWeight: FontWeight.w600,
+ fontSize: 13,
+ ),
),
),
],
),
- )
- else
+ ),
+ ] else ...[
+ const SizedBox(height: 10),
Container(
- margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFDF6B2),
@@ -273,19 +219,23 @@ class _BookingScreenState extends ConsumerState {
Icon(Icons.warning_amber,
color: Color(0xFFB45309), size: 16),
SizedBox(width: 8),
- Text(
- 'No dates selected — go back and pick dates',
- style: TextStyle(
- color: Color(0xFF92400E), fontSize: 13),
+ Expanded(
+ child: Text(
+ 'No dates selected — go back and pick dates',
+ style: TextStyle(
+ color: Color(0xFF92400E), fontSize: 13),
+ ),
),
],
),
),
+ ],
+ // ── Error ─────────────────────────────────────────────────────
if (_error != null) ...[
+ const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
- margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: const Color(0xFFFDE8E8),
borderRadius: BorderRadius.circular(10),
@@ -295,8 +245,10 @@ class _BookingScreenState extends ConsumerState {
),
],
- // Personal info
+ // ── Your information ──────────────────────────────────────────
+ const SizedBox(height: 24),
const _SectionLabel('Your Information'),
+ const SizedBox(height: 14),
Row(
children: [
Expanded(
@@ -306,8 +258,7 @@ class _BookingScreenState extends ConsumerState {
textInputAction: TextInputAction.next,
decoration:
const InputDecoration(labelText: 'First Name'),
- validator: (v) =>
- v == null || v.trim().isEmpty ? 'Required' : null,
+ validator: _required,
),
),
const SizedBox(width: 12),
@@ -318,8 +269,7 @@ class _BookingScreenState extends ConsumerState {
textInputAction: TextInputAction.next,
decoration:
const InputDecoration(labelText: 'Last Name'),
- validator: (v) =>
- v == null || v.trim().isEmpty ? 'Required' : null,
+ validator: _required,
),
),
],
@@ -333,9 +283,8 @@ class _BookingScreenState extends ConsumerState {
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
),
- validator: (v) => (v == null || !v.contains('@'))
- ? 'Enter a valid email'
- : null,
+ validator: (v) =>
+ (v == null || !v.contains('@')) ? 'Enter a valid email' : null,
),
const SizedBox(height: 14),
TextFormField(
@@ -343,14 +292,26 @@ class _BookingScreenState extends ConsumerState {
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
- labelText: 'Phone (optional)',
+ labelText: 'Phone',
prefixIcon: Icon(Icons.phone_outlined),
),
+ validator: _required,
+ ),
+ const SizedBox(height: 14),
+ TextFormField(
+ controller: _licenseCtrl,
+ textCapitalization: TextCapitalization.characters,
+ textInputAction: TextInputAction.next,
+ decoration: const InputDecoration(
+ labelText: "Driver's License Number",
+ prefixIcon: Icon(Icons.credit_card_outlined),
+ ),
+ validator: _required,
),
const SizedBox(height: 14),
TextFormField(
controller: _notesCtrl,
- maxLines: 2,
+ maxLines: 3,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
labelText: 'Notes (optional)',
@@ -358,29 +319,69 @@ class _BookingScreenState extends ConsumerState {
alignLabelWithHint: true,
),
),
+
+ // ── Info note ─────────────────────────────────────────────────
+ const SizedBox(height: 16),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF0F9FF),
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(color: const Color(0xFFBAE6FD)),
+ ),
+ child: const Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(Icons.info_outline, size: 16, color: Color(0xFF0369A1)),
+ SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ 'Full identity and license details will be collected when you pick up the vehicle.',
+ style: TextStyle(
+ color: Color(0xFF0369A1), fontSize: 12),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // ── Submit ────────────────────────────────────────────────────
const SizedBox(height: 28),
ElevatedButton(
onPressed: (_loading || !hasDates) ? null : _submit,
style: ElevatedButton.styleFrom(
- minimumSize: const Size.fromHeight(52)),
+ backgroundColor: _orange,
+ foregroundColor: Colors.white,
+ minimumSize: const Size.fromHeight(52),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(14),
+ ),
+ elevation: 0,
+ ),
child: _loading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
- strokeWidth: 2,
- valueColor:
- AlwaysStoppedAnimation(Colors.white)),
+ strokeWidth: 2,
+ valueColor:
+ AlwaysStoppedAnimation(Colors.white),
+ ),
)
- : const Text('Confirm Booking',
- style: TextStyle(fontSize: 16)),
+ : const Text(
+ 'Confirm Booking',
+ style: TextStyle(
+ fontSize: 16, fontWeight: FontWeight.bold),
+ ),
),
- const SizedBox(height: 24),
],
),
),
);
}
+
+ static String? _required(String? v) =>
+ v == null || v.trim().isEmpty ? 'Required' : null;
}
class _SectionLabel extends StatelessWidget {
@@ -388,31 +389,12 @@ class _SectionLabel extends StatelessWidget {
const _SectionLabel(this.text);
@override
- Widget build(BuildContext context) => Padding(
- padding: const EdgeInsets.only(bottom: 12),
- child: Text(text,
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 15,
- color: Color(0xFF374151))),
- );
-}
-
-class _ConfirmRow extends StatelessWidget {
- final String label;
- final String value;
- const _ConfirmRow({required this.label, required this.value});
-
- @override
- Widget build(BuildContext context) => Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(label,
- style: const TextStyle(
- fontSize: 13, color: Color(0xFF6B7280))),
- Text(value,
- style: const TextStyle(
- fontSize: 13, fontWeight: FontWeight.w600)),
- ],
+ Widget build(BuildContext context) => Text(
+ text,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 15,
+ color: Color(0xFF374151),
+ ),
);
}
diff --git a/lib/features/client/screens/client_home_screen.dart b/lib/features/client/screens/client_home_screen.dart
index ca5604c..c5ae8ae 100644
--- a/lib/features/client/screens/client_home_screen.dart
+++ b/lib/features/client/screens/client_home_screen.dart
@@ -214,7 +214,7 @@ class _ClientHomeScreenState extends ConsumerState {
: '${_selectedTypes.length} types';
final priceLabel = _priceFilterActive
- ? '\$${(_minPriceCents / 100).toStringAsFixed(0)} – \$${(_maxPriceCents / 100).toStringAsFixed(0)}/day'
+ ? '${(_minPriceCents / 100).toStringAsFixed(0)} – ${(_maxPriceCents / 100).toStringAsFixed(0)} MAD/day'
: l10n.anyPrice;
return ClientShell(
@@ -995,8 +995,8 @@ class _PriceSheetState extends State<_PriceSheet> {
}
String _fmt(double v) => v >= _PriceSheet._cap
- ? '\$${v.toStringAsFixed(0)}+'
- : '\$${v.toStringAsFixed(0)}';
+ ? '${v.toStringAsFixed(0)}+ MAD'
+ : '${v.toStringAsFixed(0)} MAD';
@override
Widget build(BuildContext context) {
diff --git a/lib/features/client/screens/client_vehicle_detail_screen.dart b/lib/features/client/screens/client_vehicle_detail_screen.dart
index 9b69c1a..ed84eb0 100644
--- a/lib/features/client/screens/client_vehicle_detail_screen.dart
+++ b/lib/features/client/screens/client_vehicle_detail_screen.dart
@@ -42,8 +42,6 @@ class _DetailView extends StatelessWidget {
? searchParams.endDate!.difference(searchParams.startDate!).inDays
: null;
final fmt = DateFormat('MMM d, yyyy');
- final baseUrl = AppConstants.baseUrl.replaceAll('/api/v1', '');
-
return Scaffold(
body: CustomScrollView(
slivers: [
@@ -53,9 +51,8 @@ class _DetailView extends StatelessWidget {
flexibleSpace: FlexibleSpaceBar(
background: vehicle.primaryPhoto != null
? CachedNetworkImage(
- imageUrl: vehicle.primaryPhoto!.startsWith('http')
- ? vehicle.primaryPhoto!
- : '$baseUrl${vehicle.primaryPhoto!}',
+ imageUrl:
+ AppConstants.resolveMediaUrl(vehicle.primaryPhoto!),
fit: BoxFit.cover,
errorWidget: (_, _, _) => _placeholder(),
)
@@ -135,7 +132,7 @@ class _DetailView extends StatelessWidget {
fontSize: 13)),
const Spacer(),
Text(
- '\$${vehicle.dailyRate.toStringAsFixed(2)}/day',
+ '${vehicle.dailyRate.toStringAsFixed(2)} MAD/day',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -173,7 +170,7 @@ class _DetailView extends StatelessWidget {
fontWeight: FontWeight.w600)),
const Spacer(),
Text(
- '\$${(vehicle.dailyRateCents * days / 100).toStringAsFixed(2)}',
+ '${(vehicle.dailyRateCents * days / 100).toStringAsFixed(2)} MAD',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
diff --git a/lib/features/client/screens/widgets/marketplace_vehicle_card.dart b/lib/features/client/screens/widgets/marketplace_vehicle_card.dart
index 1dfcbaf..e0bea93 100644
--- a/lib/features/client/screens/widgets/marketplace_vehicle_card.dart
+++ b/lib/features/client/screens/widgets/marketplace_vehicle_card.dart
@@ -1,5 +1,3 @@
-import 'dart:io';
-
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../../../core/models/marketplace.dart';
@@ -142,7 +140,7 @@ class MarketplaceVehicleCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
- '\$${vehicle.dailyRate.toStringAsFixed(0)}',
+ '${vehicle.dailyRate.toStringAsFixed(0)} MAD',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
@@ -166,7 +164,7 @@ class MarketplaceVehicleCard extends StatelessWidget {
color: cs.onSurfaceVariant),
),
Text(
- '\$${(totalCents / 100).toStringAsFixed(0)} total',
+ '${(totalCents / 100).toStringAsFixed(0)} MAD total',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
@@ -211,15 +209,7 @@ class _VehicleImage extends StatelessWidget {
);
}
- String _resolveUrl(String photo) {
- if (!photo.startsWith('http')) {
- return '${AppConstants.baseUrl.replaceAll('/api/v1', '')}$photo';
- }
- if (Platform.isAndroid) {
- return photo.replaceFirst('localhost', '10.0.2.2');
- }
- return photo;
- }
+ String _resolveUrl(String photo) => AppConstants.resolveMediaUrl(photo);
Widget _placeholder() => Container(
height: 180,
diff --git a/pubspec.lock b/pubspec.lock
index 0bfdd2d..6d6fca7 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -850,6 +850,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
+ webview_flutter:
+ dependency: "direct main"
+ description:
+ name: webview_flutter
+ sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.13.1"
+ webview_flutter_android:
+ dependency: transitive
+ description:
+ name: webview_flutter_android
+ sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.12.0"
+ webview_flutter_platform_interface:
+ dependency: transitive
+ description:
+ name: webview_flutter_platform_interface
+ sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.15.1"
+ webview_flutter_wkwebview:
+ dependency: transitive
+ description:
+ name: webview_flutter_wkwebview
+ sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.25.1"
win32:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 468b9ba..0c5e165 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -21,6 +21,7 @@ dependencies:
shimmer: ^3.0.0
fl_chart: ^0.69.0
image_picker: ^1.1.2
+ webview_flutter: ^4.0.0
dev_dependencies:
flutter_test:
diff --git a/rental_car_flutter_app_plan.md b/rental_car_flutter_app_plan.md
new file mode 100644
index 0000000..38d5624
--- /dev/null
+++ b/rental_car_flutter_app_plan.md
@@ -0,0 +1,1113 @@
+# Rental Car Flutter App Development Plan
+
+## 1. Project Overview
+
+The goal is to build a cross-platform rental car application using Flutter. The app will allow customers to search for rental cars, view available vehicles, and submit booking requests.
+
+The agency dashboard already exists as a web application. For the first mobile version, the existing web dashboard will be reused inside the Flutter app through a WebView instead of rebuilding the entire dashboard natively.
+
+The app should work across:
+
+- Android
+- iOS
+- Web
+- Desktop, if needed later
+
+Flutter will be used for the frontend because it supports multiple platforms from one codebase.
+
+---
+
+## 2. Main User Types
+
+## 2.1 Customer
+
+Customers are normal users who want to rent a car.
+
+Customers should be able to:
+
+- Open the app
+- View the company logo on the home page
+- Click the **Search your Car** button
+- Enter pick-up location
+- Enter drop-off location
+- Select pick-up date and time
+- Select drop-off date and time
+- Select car type
+- View available cars
+- View car details
+- Submit a booking request
+
+## 2.2 Agency
+
+Agencies are rental companies that already use the existing web dashboard.
+
+Agencies should be able to:
+
+- Click the **Agency** space on the home page
+- Open the existing web dashboard inside the mobile app
+- Sign in using existing agency credentials
+- Manage cars
+- Manage bookings
+- Manage pricing
+- Manage company information
+
+---
+
+## 3. MVP Strategy
+
+The first version should not rebuild the agency dashboard in Flutter.
+
+Instead, the Flutter app should:
+
+- Build a native customer-facing rental search experience
+- Use WebView to display the existing agency dashboard
+- Keep customer and agency flows separate
+- Avoid duplicating dashboard code too early
+
+This keeps development faster, cheaper, and less likely to collapse under its own heroic ambition.
+
+---
+
+## 4. Home Page
+
+## 4.1 Purpose
+
+The home page should be simple, clean, and direct.
+
+It should immediately give the user two choices:
+
+1. Search for a rental car
+2. Access the agency area
+
+## 4.2 Home Page Elements
+
+The home page should include:
+
+- Company logo
+- Main button labeled **Search your Car**
+- Small clickable area labeled **Agency**
+- Optional slogan under the logo
+- Clean background
+- Mobile-friendly layout
+
+## 4.3 Home Page Layout
+
+```text
+------------------------------------------------
+| |
+| Company Logo |
+| |
+| Find your rental car |
+| |
+| [ Search your Car ] |
+| |
+| |
+| Agency |
+| |
+------------------------------------------------
+```
+
+## 4.4 Home Page Actions
+
+### Search your Car
+
+When the user clicks **Search your Car**, the app opens the customer search screen.
+
+### Agency
+
+When the user clicks **Agency**, the app opens the existing web dashboard inside a Flutter WebView.
+
+---
+
+## 5. Customer Search Flow
+
+## 5.1 Search Screen
+
+After clicking **Search your Car**, the user should see a search form.
+
+## 5.2 Search Fields
+
+The search form should include:
+
+- Pick-up location
+- Drop-off location
+- Pick-up date
+- Pick-up time
+- Drop-off date
+- Drop-off time
+- Car type selector
+
+## 5.3 Car Types
+
+Example car types:
+
+- Economy
+- Compact
+- Sedan
+- SUV
+- Luxury
+- Van
+- Pickup Truck
+- Electric
+- Hybrid
+
+## 5.4 Search Form Layout
+
+```text
+------------------------------------------------
+| Search Your Car |
+| |
+| Pick-up Location |
+| [ Enter location ] |
+| |
+| Drop-off Location |
+| [ Enter location ] |
+| |
+| Pick-up Date & Time |
+| [ Select date ] [ Select time ] |
+| |
+| Drop-off Date & Time |
+| [ Select date ] [ Select time ] |
+| |
+| Car Type |
+| [ Select car type ] |
+| |
+| [ Search Cars ] |
+------------------------------------------------
+```
+
+## 5.5 Validation Rules
+
+The app should validate:
+
+- Pick-up location is required
+- Drop-off location is required
+- Pick-up date is required
+- Pick-up time is required
+- Drop-off date is required
+- Drop-off time is required
+- Drop-off date/time must be after pick-up date/time
+- Car type is required or defaults to **Any**
+- Empty location strings should not be accepted
+
+---
+
+## 6. Search Results Page
+
+## 6.1 Purpose
+
+The search results page displays available cars based on the customer’s search criteria.
+
+## 6.2 Car Card Information
+
+Each car card should show:
+
+- Car image
+- Car name
+- Car type
+- Transmission type
+- Fuel type
+- Number of seats
+- Price per day
+- Agency name
+- Availability status
+- Button: **View Details**
+
+## 6.3 Example Car Card
+
+```text
+------------------------------------------------
+| [Car Image] |
+| Toyota Corolla |
+| Sedan | Automatic | Gasoline |
+| 5 Seats |
+| $45/day |
+| Agency: Fast Rent Cars |
+| [ View Details ] |
+------------------------------------------------
+```
+
+---
+
+## 7. Car Details Page
+
+## 7.1 Purpose
+
+The car details page shows full information about a selected vehicle.
+
+## 7.2 Details to Display
+
+The page should include:
+
+- Car images
+- Car name
+- Car type
+- Brand
+- Model
+- Year
+- Transmission
+- Fuel type
+- Number of seats
+- Luggage capacity
+- Price per day
+- Deposit amount
+- Mileage policy
+- Insurance options
+- Agency name
+- Pick-up and drop-off policy
+- Button: **Book Now**
+
+---
+
+## 8. Booking Flow
+
+## 8.1 Booking Request
+
+When the customer clicks **Book Now**, the app should open a booking form.
+
+## 8.2 Booking Fields
+
+The booking should include:
+
+- Customer name
+- Customer phone
+- Customer email
+- Selected car
+- Pick-up location
+- Drop-off location
+- Pick-up date/time
+- Drop-off date/time
+- Total price
+- Booking status
+
+## 8.3 Booking Status Options
+
+```text
+Pending
+Confirmed
+Rejected
+Cancelled
+Completed
+```
+
+## 8.4 Payment Strategy
+
+For the first version, online payment should be skipped unless it already exists in the current system.
+
+Recommended MVP payment approach:
+
+- Customer submits booking request
+- Agency reviews booking
+- Agency confirms availability
+- Customer pays manually or through an external payment link
+
+Online payment can be added later using:
+
+- Stripe
+- PayPal
+- Square
+- Local payment provider
+
+Do not add payment processing too early. Payment systems look simple until refunds, disputes, taxes, and failed transactions arrive with knives.
+
+---
+
+## 9. Agency Dashboard Integration
+
+## 9.1 Strategy
+
+The agency dashboard already exists as a web application.
+
+For version 1, the Flutter app should open the existing dashboard inside a WebView.
+
+This avoids rebuilding the dashboard in Flutter and allows agencies to continue using the current system.
+
+## 9.2 Agency Flow
+
+```text
+Home Page
+→ Agency
+→ WebView opens existing dashboard login page
+→ Agency signs in
+→ Existing company dashboard opens
+→ Agency manages cars, bookings, pricing, and profile
+```
+
+## 9.3 Recommended MVP Flow
+
+The simplest first version is:
+
+1. User clicks **Agency**
+2. Flutter opens `AgencyDashboardWebViewScreen`
+3. WebView loads the existing web dashboard login page
+4. Agency signs in using existing credentials
+5. Agency uses the current dashboard inside the mobile app
+
+## 9.4 Why This Is Better for MVP
+
+Using WebView allows the app to:
+
+- Reuse the existing dashboard
+- Save development time
+- Avoid duplicating business logic
+- Avoid rebuilding agency management screens
+- Keep agency operations consistent across web and mobile
+- Launch faster
+
+## 9.5 Risks
+
+The existing dashboard must be tested carefully on mobile.
+
+Potential risks:
+
+- Dashboard may not be fully responsive
+- Some buttons or tables may be hard to use on small screens
+- Image upload may require WebView permissions
+- File picker behavior may differ across Android and iOS
+- Login sessions may expire unexpectedly
+- Push notifications may not work through the web dashboard
+- App store review may object if the app feels like only a wrapped website
+
+## 9.6 MVP Decision
+
+For version 1:
+
+- Use WebView for the agency dashboard
+- Do not rebuild the dashboard natively
+- Test the dashboard on Android and iPhone
+- Only rebuild dashboard parts in Flutter if WebView creates serious usability problems
+
+---
+
+## 10. Suggested Flutter Package for WebView
+
+Use:
+
+```yaml
+webview_flutter: ^4.0.0
+```
+
+Example screen:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+class AgencyDashboardWebViewScreen extends StatefulWidget {
+ const AgencyDashboardWebViewScreen({super.key});
+
+ @override
+ State createState() =>
+ _AgencyDashboardWebViewScreenState();
+}
+
+class _AgencyDashboardWebViewScreenState
+ extends State {
+ late final WebViewController controller;
+
+ @override
+ void initState() {
+ super.initState();
+
+ controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..loadRequest(
+ Uri.parse('https://your-dashboard-url.com'),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Agency Dashboard'),
+ ),
+ body: WebViewWidget(controller: controller),
+ );
+ }
+}
+```
+
+Replace:
+
+```text
+https://your-dashboard-url.com
+```
+
+with the real dashboard URL.
+
+---
+
+## 11. Login Strategy
+
+## 11.1 Recommended First Version
+
+For MVP, let the existing dashboard handle login.
+
+Flow:
+
+```text
+Agency clicks Agency
+→ WebView opens dashboard login page
+→ Agency logs in using existing credentials
+→ Dashboard opens inside app
+```
+
+This avoids complicated token handoff between Flutter and the web dashboard.
+
+## 11.2 More Advanced Future Version
+
+Later, the Flutter app can have its own agency login screen.
+
+Flow:
+
+```text
+Agency clicks Agency
+→ Flutter sign-in screen
+→ Authenticate agency
+→ Generate secure session
+→ Open dashboard already logged in
+```
+
+This requires secure authentication handling.
+
+Do not pass permanent tokens in the URL.
+
+Avoid:
+
+```text
+https://dashboard.com?token=abc123
+```
+
+Better options:
+
+- Secure cookies
+- OAuth redirect
+- Backend-generated session
+- Short-lived one-time login token
+- Firebase custom token flow
+- Supabase session handoff
+
+---
+
+## 12. Updated Screen List
+
+## 12.1 Customer Screens
+
+```text
+HomeScreen
+SearchCarScreen
+SearchResultsScreen
+CarDetailsScreen
+BookingScreen
+BookingConfirmationScreen
+```
+
+## 12.2 Agency Screens
+
+```text
+AgencyDashboardWebViewScreen
+```
+
+## 12.3 Optional Future Agency Screens
+
+```text
+NativeAgencySignInScreen
+NativeAgencyDashboardScreen
+ManageCarsScreen
+AddCarScreen
+EditCarScreen
+ManageBookingsScreen
+BookingDetailsScreen
+AgencyProfileScreen
+```
+
+---
+
+## 13. Suggested Flutter Project Structure
+
+```text
+lib/
+│
+├── main.dart
+├── app.dart
+│
+├── core/
+│ ├── constants/
+│ ├── theme/
+│ ├── routing/
+│ ├── utils/
+│ └── widgets/
+│
+├── features/
+│ ├── home/
+│ ├── car_search/
+│ ├── car_details/
+│ ├── booking/
+│ └── agency_webview/
+│
+├── models/
+│ ├── car_model.dart
+│ ├── booking_model.dart
+│ └── agency_model.dart
+│
+├── services/
+│ ├── car_service.dart
+│ ├── booking_service.dart
+│ └── agency_service.dart
+│
+└── providers/
+ ├── car_provider.dart
+ └── booking_provider.dart
+```
+
+---
+
+## 14. Routing Plan
+
+Suggested routes:
+
+```text
+/
+ HomeScreen
+
+/search
+ SearchCarScreen
+
+/results
+ SearchResultsScreen
+
+/car/:carId
+ CarDetailsScreen
+
+/booking/:carId
+ BookingScreen
+
+/booking-confirmation
+ BookingConfirmationScreen
+
+/agency
+ AgencyDashboardWebViewScreen
+```
+
+Future native agency routes:
+
+```text
+/agency/login
+ NativeAgencySignInScreen
+
+/agency/dashboard
+ NativeAgencyDashboardScreen
+
+/agency/cars
+ ManageCarsScreen
+
+/agency/bookings
+ ManageBookingsScreen
+```
+
+---
+
+## 15. Recommended Flutter Packages
+
+## 15.1 State Management
+
+Recommended:
+
+```yaml
+flutter_riverpod
+```
+
+Alternative options:
+
+```yaml
+provider
+bloc
+```
+
+## 15.2 Navigation
+
+Recommended:
+
+```yaml
+go_router
+```
+
+## 15.3 Forms and Validation
+
+```yaml
+flutter_form_builder
+form_builder_validators
+```
+
+## 15.4 Date and Time
+
+```yaml
+intl
+```
+
+Flutter also includes built-in date and time pickers.
+
+## 15.5 WebView
+
+```yaml
+webview_flutter
+```
+
+## 15.6 Images and UI
+
+```yaml
+cached_network_image
+flutter_svg
+google_fonts
+```
+
+---
+
+## 16. Backend Recommendation
+
+If the current web dashboard already has a backend, the Flutter app should connect to the same backend.
+
+The customer search and booking flow should use the same data source as the existing dashboard.
+
+This prevents duplicate data, mismatched bookings, and the kind of confusion that makes support teams age in dog years.
+
+If no backend API is available yet, create one before building too much Flutter UI.
+
+Recommended backend options:
+
+- Existing dashboard backend
+- Firebase
+- Supabase
+- Node.js API
+- Laravel API
+
+---
+
+## 17. Database Design
+
+If the current system already has these tables or collections, reuse them.
+
+## 17.1 Agencies
+
+```json
+{
+ "agencyId": "string",
+ "companyName": "string",
+ "email": "string",
+ "phone": "string",
+ "address": "string",
+ "logoUrl": "string",
+ "createdAt": "timestamp",
+ "isActive": true
+}
+```
+
+## 17.2 Cars
+
+```json
+{
+ "carId": "string",
+ "agencyId": "string",
+ "name": "Toyota Corolla",
+ "brand": "Toyota",
+ "model": "Corolla",
+ "year": 2023,
+ "type": "Sedan",
+ "transmission": "Automatic",
+ "fuelType": "Gasoline",
+ "seats": 5,
+ "doors": 4,
+ "luggageCapacity": 2,
+ "pricePerDay": 45,
+ "depositAmount": 200,
+ "description": "Clean and fuel-efficient sedan.",
+ "imageUrls": [],
+ "isAvailable": true,
+ "createdAt": "timestamp"
+}
+```
+
+## 17.3 Bookings
+
+```json
+{
+ "bookingId": "string",
+ "carId": "string",
+ "agencyId": "string",
+ "customerName": "string",
+ "customerEmail": "string",
+ "customerPhone": "string",
+ "pickupLocation": "string",
+ "dropoffLocation": "string",
+ "pickupDateTime": "timestamp",
+ "dropoffDateTime": "timestamp",
+ "carType": "Sedan",
+ "totalPrice": 180,
+ "status": "Pending",
+ "createdAt": "timestamp"
+}
+```
+
+---
+
+## 18. Security Requirements
+
+Security rules must enforce:
+
+- Agencies can only manage their own data
+- Customers cannot access agency management features
+- Public users can search available cars
+- Booking creation must validate required fields
+- Booking status changes must be restricted to agency users
+- Dashboard sessions must be protected
+- API keys must not be exposed inside the Flutter app
+
+The WebView dashboard must use HTTPS.
+
+Never load the dashboard over plain HTTP.
+
+---
+
+## 19. Rental Business Logic
+
+## 19.1 Rental Duration
+
+The app should calculate rental duration using pick-up and drop-off date/time.
+
+Rules:
+
+- Minimum rental duration is 1 day
+- If the rental is less than 24 hours, charge 1 day
+- If the rental includes partial days, round up to the next full day
+- Drop-off date/time must be after pick-up date/time
+
+Example:
+
+```text
+Pick-up: June 1, 10:00 AM
+Drop-off: June 3, 2:00 PM
+
+Rental duration = 3 days
+```
+
+## 19.2 Total Price
+
+Basic formula:
+
+```text
+Total Price = Rental Days × Price Per Day
+```
+
+Future additions:
+
+- Taxes
+- Deposit
+- Insurance
+- Discount
+- Late return fee
+- Extra driver fee
+- Delivery fee
+
+---
+
+## 20. MVP Feature List
+
+## 20.1 Customer MVP
+
+The customer side should include:
+
+- Home page
+- Search form
+- Search results page
+- Car details page
+- Booking request form
+- Booking confirmation page
+
+## 20.2 Agency MVP
+
+The agency side should include:
+
+- Agency link on home page
+- WebView dashboard screen
+- Existing dashboard login
+- Existing dashboard management tools
+
+## 20.3 Backend MVP
+
+The backend should support:
+
+- Car listing retrieval
+- Search/filtering
+- Booking creation
+- Agency dashboard data
+- Secure agency access
+
+---
+
+## 21. Features to Avoid in Version 1
+
+Do not include these in the first version unless they already exist and work:
+
+- Native Flutter agency dashboard
+- Online payment
+- Coupons
+- Loyalty points
+- Live chat
+- GPS tracking
+- AI recommendations
+- Complex analytics
+- Multi-language support
+- Push notifications
+
+These features can come later. Early-stage apps usually fail because someone keeps adding “just one more thing” until the roadmap looks like a hostage note.
+
+---
+
+## 22. Development Phases
+
+## Phase 1: Planning and Setup
+
+- Confirm app name
+- Prepare company logo
+- Confirm existing dashboard URL
+- Confirm backend/API access
+- Create Flutter project
+- Set up routing
+- Set up theme
+- Add WebView package
+
+## Phase 2: Home Page
+
+- Build home screen
+- Add company logo
+- Add **Search your Car** button
+- Add small **Agency** space
+- Connect buttons to routes
+
+## Phase 3: Agency WebView
+
+- Create `AgencyDashboardWebViewScreen`
+- Load dashboard URL
+- Test dashboard login
+- Test dashboard responsiveness
+- Test image upload
+- Test Android behavior
+- Test iOS behavior
+
+## Phase 4: Customer Search UI
+
+- Build search form
+- Add pick-up location field
+- Add drop-off location field
+- Add date pickers
+- Add time pickers
+- Add car type dropdown
+- Add validation
+
+## Phase 5: Search Results
+
+- Create car model
+- Use fake data first
+- Build car cards
+- Build search results page
+- Add filtering by car type and location
+
+## Phase 6: Car Details
+
+- Build car details page
+- Display car image and details
+- Add price information
+- Add **Book Now** button
+
+## Phase 7: Booking Flow
+
+- Build booking form
+- Calculate rental days
+- Calculate total price
+- Submit booking request
+- Show booking confirmation
+
+## Phase 8: Backend Integration
+
+- Connect to existing backend or API
+- Fetch real car data
+- Submit real booking requests
+- Confirm bookings appear in agency dashboard
+- Test data consistency
+
+## Phase 9: Testing
+
+Test:
+
+- Home page navigation
+- Search form validation
+- Search results filtering
+- Car details page
+- Booking creation
+- WebView dashboard login
+- Dashboard mobile responsiveness
+- Dashboard image upload
+- Android build
+- iOS build
+- Web build, if needed
+
+## Phase 10: Deployment
+
+- Prepare Android release
+- Prepare iOS release
+- Configure production backend
+- Configure dashboard production URL
+- Test final builds
+- Prepare app store assets
+- Submit to app stores
+
+---
+
+## 23. WebView Testing Checklist
+
+Before accepting WebView as the final agency solution, test:
+
+- Dashboard opens correctly
+- Login works
+- Logout works
+- Session stays active properly
+- Dashboard layout fits mobile screens
+- Menu/navigation works on touch devices
+- Tables are readable
+- Forms are usable
+- Add car works
+- Edit car works
+- Delete/archive car works
+- Booking management works
+- Image upload works
+- File picker works
+- Camera/gallery permissions work
+- Keyboard does not cover form fields
+- Back button behavior works on Android
+- Dashboard uses HTTPS
+- No sensitive token is exposed in the URL
+
+---
+
+## 24. Risks and Decisions
+
+## Risk 1: Dashboard Not Mobile Responsive
+
+If the existing dashboard does not work well on phone screens, the WebView approach may feel poor.
+
+Decision:
+
+Test first. Rebuild only the broken dashboard parts natively if needed.
+
+## Risk 2: Authentication Complexity
+
+Passing login sessions between Flutter and the web dashboard can become complicated.
+
+Decision:
+
+For MVP, let the web dashboard handle login inside WebView.
+
+## Risk 3: Duplicate Data
+
+If the mobile app uses a different backend from the dashboard, bookings and cars may become inconsistent.
+
+Decision:
+
+Use the same backend/API as the existing dashboard.
+
+## Risk 4: App Store Review
+
+If the app is only a website wrapper, app stores may reject it.
+
+Decision:
+
+Make the customer search and booking experience native Flutter. Use WebView only for the agency dashboard.
+
+## Risk 5: Scope Creep
+
+Trying to build customer app, native dashboard, payments, notifications, and analytics all at once will slow the launch.
+
+Decision:
+
+Build only the MVP first.
+
+---
+
+## 25. MVP Success Criteria
+
+The MVP is successful when:
+
+- Customer can open the app
+- Customer can search for a car
+- Customer can view car details
+- Customer can submit a booking request
+- Agency can open the dashboard inside the app
+- Agency can sign in
+- Agency can manage cars and bookings using the existing dashboard
+- The same data appears correctly between the customer app and dashboard
+- The app works on Android and iOS
+- The WebView dashboard is usable on mobile
+
+---
+
+## 26. Recommended First Build Order
+
+Build in this order:
+
+1. Create Flutter project
+2. Build home page
+3. Add company logo
+4. Add **Search your Car** button
+5. Add **Agency** entry point
+6. Add WebView dashboard screen
+7. Test existing dashboard inside the app
+8. Build search form
+9. Build fake search results
+10. Build car details page
+11. Build booking form
+12. Connect backend/API
+13. Test booking flow with dashboard
+14. Polish UI
+15. Test Android and iOS
+16. Deploy
+
+---
+
+## 27. Final MVP Scope
+
+## Customer
+
+- Home page
+- Search form
+- Search results
+- Car details
+- Booking request
+- Booking confirmation
+
+## Agency
+
+- Agency button/link
+- WebView dashboard
+- Existing dashboard login
+- Existing dashboard management
+
+## Backend
+
+- Existing dashboard backend if available
+- Car data
+- Booking data
+- Agency data
+- Secure dashboard access
+
+---
+
+## 28. Final Recommendation
+
+Use Flutter for the customer-facing mobile app.
+
+Use WebView for the existing agency dashboard in version 1.
+
+Do not rebuild the agency dashboard natively until there is clear evidence that the WebView version is not good enough.
+
+This approach reduces development time, protects the existing dashboard investment, and gets the app closer to launch without turning the project into a feature swamp.