add 8-tab review page, review controller, updateDriverToActive handles new fields, drivers_cant_register navigates to review
This commit is contained in:
@@ -5,30 +5,35 @@ require_once __DIR__ . '/../connect.php'; // يفترض أن يحتوي على
|
||||
|
||||
// --- استقبال البيانات من التطبيق ---
|
||||
$driverId = filterRequest("driverId");
|
||||
$phone = filterRequest("phone"); // استقبال رقم الهاتف مباشرة
|
||||
$phone = filterRequest("phone");
|
||||
$email = filterRequest("email");
|
||||
$status = filterRequest("status") ?: 'actives';
|
||||
|
||||
// --- بيانات جدول السائق (driver) ---
|
||||
$firstName = filterRequest("first_name");
|
||||
$lastName = filterRequest("last_name");
|
||||
$site = filterRequest("site"); // مكان القيد/الولادة
|
||||
$site = filterRequest("site");
|
||||
$address = filterRequest("address") ?: $site;
|
||||
$nationalNumber = filterRequest("national_number");
|
||||
$licenseCategories = filterRequest("license_categories"); // فئة الرخصة
|
||||
$expiryDate = filterRequest("expiry_date"); // تاريخ انتهاء رخصة السائق
|
||||
$licenseCategories = filterRequest("license_categories");
|
||||
$licenseType = filterRequest("license_type");
|
||||
$expiryDate = filterRequest("expiry_date");
|
||||
$licenseIssueDate = filterRequest("license_issue_date");
|
||||
$gender = filterRequest("gender"); // الحقل الجديد
|
||||
$birthdate = filterRequest("birthdate"); // الحقل الجديد
|
||||
$maritalStatus=filterRequest("maritalStatus");
|
||||
$gender = filterRequest("gender");
|
||||
$birthdate = filterRequest("birthdate");
|
||||
$maritalStatus = filterRequest("maritalStatus");
|
||||
|
||||
// --- بيانات جدول السيارة (CarRegistration) ---
|
||||
$owner = filterRequest("owner");
|
||||
$color = filterRequest("color");
|
||||
$colorHex = filterRequest("color_hex");
|
||||
$model = filterRequest("model"); // الموديل
|
||||
$model = filterRequest("model");
|
||||
$carPlate = filterRequest("car_plate");
|
||||
$make = filterRequest("make"); // الصانع
|
||||
$make = filterRequest("make");
|
||||
$fuel = filterRequest("fuel");
|
||||
$year = filterRequest("year");
|
||||
$carExpirationDate = filterRequest("expiration_date"); // تاريخ انتهاء رخصة السيارة
|
||||
$carExpirationDate = filterRequest("expiration_date");
|
||||
$vin = filterRequest("vin");
|
||||
|
||||
// --- بدء المعاملة لضمان سلامة البيانات ---
|
||||
$con->beginTransaction();
|
||||
@@ -36,7 +41,6 @@ $con->beginTransaction();
|
||||
try {
|
||||
// --- 1. معالجة وتشفير البيانات ---
|
||||
$nameArabic = $firstName . ' ' . $lastName;
|
||||
$address = $site;
|
||||
|
||||
// تشفير الحقول الحساسة
|
||||
$encryptedFirstName = $encryptionHelper->encryptData($firstName);
|
||||
@@ -50,6 +54,9 @@ try {
|
||||
$encryptedBirthdate = $encryptionHelper->encryptData($birthdate);
|
||||
$encryptedGender = $encryptionHelper->encryptData($gender);
|
||||
|
||||
$encryptedPhone = !empty($phone) ? $encryptionHelper->encryptData($phone) : null;
|
||||
$encryptedEmail = !empty($email) ? $encryptionHelper->encryptData($email) : null;
|
||||
|
||||
// --- 2. تحديث جدول السائق ---
|
||||
$sqlDriver = "UPDATE `driver` SET
|
||||
`first_name` = :first_name,
|
||||
@@ -58,13 +65,14 @@ try {
|
||||
`address` = :address,
|
||||
`national_number` = :national_number,
|
||||
`license_categories` = :license_categories,
|
||||
`license_type` = :license_type,
|
||||
`expiry_date` = :expiry_date,
|
||||
`issue_date` = :issue_date,
|
||||
`gender` = :gender,
|
||||
`birthdate` = :birthdate,
|
||||
`name_arabic` = :name_arabic,
|
||||
`maritalStatus` = :maritalStatus,
|
||||
`status` = 'actives'
|
||||
`status` = :status
|
||||
WHERE `id` = :driverId";
|
||||
|
||||
$stmtDriver = $con->prepare($sqlDriver);
|
||||
@@ -75,15 +83,27 @@ try {
|
||||
':address' => $encryptedAddress,
|
||||
':national_number' => $encryptedNationalNumber,
|
||||
':license_categories' => $licenseCategories,
|
||||
':license_type' => $licenseType,
|
||||
':expiry_date' => $expiryDate,
|
||||
':issue_date' => $licenseIssueDate,
|
||||
':gender' => $encryptedGender,
|
||||
':birthdate' => $encryptedBirthdate,
|
||||
':name_arabic' => $encryptedNameArabic,
|
||||
':maritalStatus' => $maritalStatus,
|
||||
':status' => $status,
|
||||
':driverId' => $driverId,
|
||||
':maritalStatus' =>$maritalStatus
|
||||
]);
|
||||
|
||||
// --- تحديث الهاتف والايميل إذا وجدا ---
|
||||
if ($encryptedPhone) {
|
||||
$stmtPhone = $con->prepare("UPDATE `driver` SET `phone` = :phone WHERE `id` = :id");
|
||||
$stmtPhone->execute([':phone' => $encryptedPhone, ':id' => $driverId]);
|
||||
}
|
||||
if ($encryptedEmail) {
|
||||
$stmtEmail = $con->prepare("UPDATE `driver` SET `email` = :email WHERE `id` = :id");
|
||||
$stmtEmail->execute([':email' => $encryptedEmail, ':id' => $driverId]);
|
||||
}
|
||||
|
||||
// --- 3. تحديث جدول السيارة ---
|
||||
$sqlCar = "UPDATE `CarRegistration` SET
|
||||
`owner` = :owner,
|
||||
@@ -94,6 +114,7 @@ try {
|
||||
`make` = :make,
|
||||
`fuel` = :fuel,
|
||||
`year` = :year,
|
||||
`vin` = :vin,
|
||||
`expiration_date` = :expiration_date
|
||||
WHERE `driverID` = :driverId";
|
||||
|
||||
@@ -107,6 +128,7 @@ try {
|
||||
':make' => $make,
|
||||
':fuel' => $fuel,
|
||||
':year' => $year,
|
||||
':vin' => $vin ?: '',
|
||||
':expiration_date' => $carExpirationDate,
|
||||
':driverId' => $driverId
|
||||
]);
|
||||
|
||||
@@ -181,6 +181,7 @@ class AppLink {
|
||||
static String getComplaintAllData = "$serviceApp/getComplaintAllData.php";
|
||||
static String getComplaintAllDataForDriver =
|
||||
"$serviceApp/getComplaintAllDataForDriver.php";
|
||||
static String rejectDriver = "$serviceApp/rejectDriver.php";
|
||||
static String addCriminalDocuments = "$authCaptin/addCriminalDocuments.php";
|
||||
static String ride = '$server/ride';
|
||||
static String addRegisrationCar = "$ride/RegisrationCar/add.php";
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:siro_service/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_service/main.dart';
|
||||
import 'package:siro_service/constant/box_name.dart';
|
||||
|
||||
import 'registration_captain_page.dart';
|
||||
import 'review_driver_page.dart';
|
||||
|
||||
class DriversCantRegister extends StatelessWidget {
|
||||
DriversCantRegister({super.key});
|
||||
@@ -151,7 +151,7 @@ class DriversCantRegister extends StatelessWidget {
|
||||
color: AppColor.gold,
|
||||
onPressed: () {
|
||||
Get.to(
|
||||
() => RegisterCaptain(),
|
||||
() => const ReviewDriverPage(),
|
||||
arguments: {
|
||||
"phone": driver['phone_number'],
|
||||
"driverId": driver['driverId'],
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_service/constant/colors.dart';
|
||||
import 'package:siro_service/controller/mainController/review_driver_controller.dart';
|
||||
import 'package:siro_service/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_service/views/widgets/my_scafold.dart';
|
||||
|
||||
class ReviewDriverPage extends StatelessWidget {
|
||||
const ReviewDriverPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(ReviewDriverController());
|
||||
return MyScaffold(
|
||||
title: 'Review Driver'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
_buildTabBar(controller),
|
||||
Expanded(child: _buildTabBarView(controller)),
|
||||
_buildBottomActions(controller, context),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar(ReviewDriverController c) {
|
||||
final keys = c.docUrls.keys.toList();
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final key = keys[index];
|
||||
final isSelected = c.currentTabIndex.value == index;
|
||||
return GestureDetector(
|
||||
onTap: () => c.currentTabIndex.value = index,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColor.primaryColor
|
||||
: AppColor.primaryLight,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
c.tabIcons[key] ?? Icons.image,
|
||||
size: 16,
|
||||
color: isSelected ? Colors.white : AppColor.primaryColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
c.tabLabels[key] ?? key,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color:
|
||||
isSelected ? Colors.white : AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBarView(ReviewDriverController c) {
|
||||
return Obx(() {
|
||||
final index = c.currentTabIndex.value;
|
||||
final keys = c.docUrls.keys.toList();
|
||||
final key = keys[index];
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildDocumentImage(c.docUrls[key]!.value),
|
||||
const SizedBox(height: 16),
|
||||
_buildFormFieldsForTab(c, key),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDocumentImage(String url) {
|
||||
if (url.isEmpty) {
|
||||
return Container(
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.image_not_supported, size: 48, color: Colors.grey[400]),
|
||||
const SizedBox(height: 8),
|
||||
Text('No image available', style: TextStyle(color: Colors.grey[500])),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () => _showImageFullscreen(url),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
url,
|
||||
height: 200,
|
||||
fit: BoxFit.contain,
|
||||
width: double.infinity,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
height: 180,
|
||||
color: Colors.grey[200],
|
||||
child: Center(
|
||||
child: Text('Failed to load image',
|
||||
style: TextStyle(color: Colors.grey[500])),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showImageFullscreen(String url) {
|
||||
Get.dialog(
|
||||
Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
),
|
||||
body: Center(
|
||||
child: InteractiveViewer(
|
||||
child: Image.network(url, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormFieldsForTab(ReviewDriverController c, String tabKey) {
|
||||
switch (tabKey) {
|
||||
case 'id_front':
|
||||
return _buildIdFrontFields(c);
|
||||
case 'id_back':
|
||||
return _buildIdBackFields(c);
|
||||
case 'driver_license':
|
||||
return _buildDriverLicenseFields(c);
|
||||
case 'driver_license_back':
|
||||
return _buildLicenseBackFields(c);
|
||||
case 'car_license_front':
|
||||
return _buildCarFrontFields(c);
|
||||
case 'car_license_back':
|
||||
return _buildCarBackFields(c);
|
||||
case 'criminal_record':
|
||||
return _buildCriminalRecordFields();
|
||||
case 'profile_picture':
|
||||
return _buildProfilePhotoFields(c);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
IconData? icon,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
VoidCallback? onTap,
|
||||
String? hint,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
readOnly: onTap != null,
|
||||
onTap: onTap,
|
||||
decoration: InputDecoration(
|
||||
labelText: label.tr,
|
||||
hintText: hint,
|
||||
prefixIcon: icon != null
|
||||
? Icon(icon, color: AppColor.secondaryColor)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGenderDropdown(ReviewDriverController c) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: c.selectedGender.value.isEmpty
|
||||
? null
|
||||
: c.selectedGender.value,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Gender'.tr,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
),
|
||||
items: ['Male', 'Female'].map((v) {
|
||||
return DropdownMenuItem(value: v, child: Text(v.tr));
|
||||
}).toList(),
|
||||
onChanged: (v) {
|
||||
if (v != null) c.selectedGender.value = v;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorDropdown(ReviewDriverController c) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: c.colorHex.value.isEmpty ? null : c.colorHex.value,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Car Color'.tr,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
),
|
||||
items: ReviewDriverController.kCarColorOptions.map((opt) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: opt['hex'],
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: c.hexToColor(opt['hex']!),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.black12),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(opt['key']!.tr),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (hex) {
|
||||
if (hex != null) c.updateColorSelection(hex);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFuelDropdown(ReviewDriverController c) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: ReviewDriverController.kFuelOptions
|
||||
.contains(c.selectedFuel.value)
|
||||
? c.selectedFuel.value
|
||||
: null,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Fuel Type'.tr,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
),
|
||||
items: ReviewDriverController.kFuelOptions.map((v) {
|
||||
return DropdownMenuItem(value: v, child: Text(v));
|
||||
}).toList(),
|
||||
onChanged: (v) {
|
||||
if (v != null) c.selectedFuel.value = v;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIdFrontFields(ReviewDriverController c) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('ID Card Information (Front)',
|
||||
style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
label: 'First Name',
|
||||
controller: c.firstNameController,
|
||||
icon: Icons.person,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
label: 'Last Name',
|
||||
controller: c.lastNameController,
|
||||
icon: Icons.person,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
label: 'National Number',
|
||||
controller: c.nationalNumberController,
|
||||
icon: Icons.fingerprint,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildGenderDropdown(c)),
|
||||
],
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'Birthdate',
|
||||
controller: c.birthdateController,
|
||||
icon: Icons.cake,
|
||||
onTap: () => c.selectDate(Get.context!, c.birthdateController),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIdBackFields(ReviewDriverController c) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('ID Card Information (Back)',
|
||||
style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
_buildTextField(
|
||||
label: 'Address',
|
||||
controller: c.addressController,
|
||||
icon: Icons.location_on,
|
||||
maxLines: 2,
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'Place of Registration',
|
||||
controller: c.siteController,
|
||||
icon: Icons.location_city,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverLicenseFields(ReviewDriverController c) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Driver License Information',
|
||||
style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
_buildTextField(
|
||||
label: 'License Type',
|
||||
controller: c.licenseTypeController,
|
||||
icon: Icons.card_membership,
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'License Categories',
|
||||
controller: c.licenseCategoriesController,
|
||||
icon: Icons.category,
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'License Issue Date',
|
||||
controller: c.licenseIssueDateController,
|
||||
icon: Icons.event,
|
||||
onTap: () =>
|
||||
c.selectDate(Get.context!, c.licenseIssueDateController),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLicenseBackFields(ReviewDriverController c) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Driver License (Back)', style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
_buildTextField(
|
||||
label: 'Expiry Date',
|
||||
controller: c.expiryDateController,
|
||||
icon: Icons.event_busy,
|
||||
onTap: () => c.selectDate(Get.context!, c.expiryDateController),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCarFrontFields(ReviewDriverController c) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Car Registration (Front)',
|
||||
style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
_buildTextField(
|
||||
label: 'Owner Name',
|
||||
controller: c.ownerController,
|
||||
icon: Icons.person_search,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildColorDropdown(c)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
label: 'Car Plate',
|
||||
controller: c.carPlateController,
|
||||
icon: Icons.confirmation_number,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'License Issue Date',
|
||||
controller: c.licenseIssueDateController,
|
||||
icon: Icons.event,
|
||||
onTap: () =>
|
||||
c.selectDate(Get.context!, c.licenseIssueDateController),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCarBackFields(ReviewDriverController c) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Car Registration (Back)',
|
||||
style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
label: 'Make',
|
||||
controller: c.makeController,
|
||||
icon: Icons.directions_car,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
label: 'Model',
|
||||
controller: c.modelController,
|
||||
icon: Icons.model_training,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
label: 'Year',
|
||||
controller: c.yearController,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildFuelDropdown(c)),
|
||||
],
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'VIN (Chassis Number)',
|
||||
controller: c.vinController,
|
||||
icon: Icons.confirmation_number,
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'Expiration Date',
|
||||
controller: c.carLicenseExpiryDateController,
|
||||
icon: Icons.event_busy,
|
||||
onTap: () => c.selectDate(
|
||||
Get.context!, c.carLicenseExpiryDateController),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCriminalRecordFields() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Criminal Record', style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
Card(
|
||||
color: Colors.green[50],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green[700], size: 32),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Review the criminal record document above. '
|
||||
'Verify the name matches the driver and the record is valid.',
|
||||
style: TextStyle(color: Colors.green[800]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfilePhotoFields(ReviewDriverController c) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Profile Photo', style: Get.textTheme.titleMedium),
|
||||
const Divider(),
|
||||
Card(
|
||||
color: Colors.blue[50],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.face, color: Colors.blue[700], size: 32),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Verify the profile photo matches the person in the ID '
|
||||
'and Driver License photos above.',
|
||||
style: TextStyle(color: Colors.blue[800]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
label: 'Phone Number',
|
||||
controller: c.phoneController,
|
||||
icon: Icons.phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
_buildTextField(
|
||||
label: 'Email',
|
||||
controller: c.emailController,
|
||||
icon: Icons.email,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomActions(ReviewDriverController c, BuildContext context) {
|
||||
return Obx(() {
|
||||
final saving = c.isSaving.value;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
onPressed: () { c.saveChanges(); },
|
||||
kolor: AppColor.blueColor,
|
||||
loading: saving,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: 'Activate'.tr,
|
||||
onPressed: () { c.activateDriver(); },
|
||||
kolor: AppColor.greenColor,
|
||||
loading: saving,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: 'Reject'.tr,
|
||||
onPressed: () { c.showRejectDialog(); },
|
||||
kolor: AppColor.redColor,
|
||||
loading: saving,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:siro_service/constant/box_name.dart';
|
||||
import 'package:siro_service/constant/links.dart';
|
||||
import 'package:siro_service/controller/functions/crud.dart';
|
||||
import 'package:siro_service/main.dart';
|
||||
|
||||
class ReviewDriverController extends GetxController {
|
||||
var isLoading = true.obs;
|
||||
var isSaving = false.obs;
|
||||
var driverId = ''.obs;
|
||||
var phone = ''.obs;
|
||||
var serverData = <String, dynamic>{}.obs;
|
||||
var currentTabIndex = 0.obs;
|
||||
|
||||
final Map<String, RxString> docUrls = {
|
||||
'id_front': ''.obs,
|
||||
'id_back': ''.obs,
|
||||
'driver_license': ''.obs,
|
||||
'driver_license_back': ''.obs,
|
||||
'car_license_front': ''.obs,
|
||||
'car_license_back': ''.obs,
|
||||
'criminal_record': ''.obs,
|
||||
'profile_picture': ''.obs,
|
||||
};
|
||||
|
||||
final Map<String, String> tabLabels = {
|
||||
'id_front': 'ID Front',
|
||||
'id_back': 'ID Back',
|
||||
'driver_license': 'Driver License',
|
||||
'driver_license_back': 'License Back',
|
||||
'car_license_front': 'Car Reg Front',
|
||||
'car_license_back': 'Car Reg Back',
|
||||
'criminal_record': 'Criminal Record',
|
||||
'profile_picture': 'Profile Photo',
|
||||
};
|
||||
|
||||
final Map<String, IconData> tabIcons = {
|
||||
'id_front': Icons.badge,
|
||||
'id_back': Icons.badge_outlined,
|
||||
'driver_license': Icons.drive_eta,
|
||||
'driver_license_back': Icons.drive_eta_outlined,
|
||||
'car_license_front': Icons.description,
|
||||
'car_license_back': Icons.description_outlined,
|
||||
'criminal_record': Icons.gavel,
|
||||
'profile_picture': Icons.person,
|
||||
};
|
||||
|
||||
late TextEditingController firstNameController;
|
||||
late TextEditingController lastNameController;
|
||||
late TextEditingController phoneController;
|
||||
late TextEditingController emailController;
|
||||
late TextEditingController siteController;
|
||||
late TextEditingController nationalNumberController;
|
||||
late TextEditingController birthdateController;
|
||||
late TextEditingController addressController;
|
||||
late TextEditingController licenseCategoriesController;
|
||||
late TextEditingController licenseTypeController;
|
||||
late TextEditingController expiryDateController;
|
||||
late TextEditingController licenseIssueDateController;
|
||||
late TextEditingController ownerController;
|
||||
late TextEditingController carPlateController;
|
||||
late TextEditingController vinController;
|
||||
late TextEditingController carLicenseExpiryDateController;
|
||||
late TextEditingController makeController;
|
||||
late TextEditingController modelController;
|
||||
late TextEditingController yearController;
|
||||
|
||||
var selectedGender = ''.obs;
|
||||
var colorHex = ''.obs;
|
||||
var colorName = ''.obs;
|
||||
var selectedFuel = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initializeTextControllers();
|
||||
final args = Get.arguments;
|
||||
if (args != null) {
|
||||
driverId.value = args['driverId'] ?? '';
|
||||
phone.value = args['phone'] ?? '';
|
||||
}
|
||||
fetchData();
|
||||
}
|
||||
|
||||
void _initializeTextControllers() {
|
||||
firstNameController = TextEditingController();
|
||||
lastNameController = TextEditingController();
|
||||
phoneController = TextEditingController();
|
||||
emailController = TextEditingController();
|
||||
siteController = TextEditingController();
|
||||
nationalNumberController = TextEditingController();
|
||||
birthdateController = TextEditingController();
|
||||
addressController = TextEditingController();
|
||||
licenseCategoriesController = TextEditingController();
|
||||
licenseTypeController = TextEditingController();
|
||||
expiryDateController = TextEditingController();
|
||||
licenseIssueDateController = TextEditingController();
|
||||
ownerController = TextEditingController();
|
||||
carPlateController = TextEditingController();
|
||||
vinController = TextEditingController();
|
||||
carLicenseExpiryDateController = TextEditingController();
|
||||
makeController = TextEditingController();
|
||||
modelController = TextEditingController();
|
||||
yearController = TextEditingController();
|
||||
}
|
||||
|
||||
Future<void> fetchData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.getDriverDetailsForActivate,
|
||||
payload: {'driverId': driverId.value},
|
||||
);
|
||||
if (response != 'failure' &&
|
||||
response['status'] == 'success' &&
|
||||
(response['message'] as List).isNotEmpty) {
|
||||
var raw = response['message'][0] as Map<String, dynamic>;
|
||||
serverData.value =
|
||||
raw.map((key, value) => MapEntry(key, value?.toString() ?? ''));
|
||||
_populateControllers();
|
||||
_populateDocUrls(raw['documents']);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error', 'Failed to load data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _populateDocUrls(dynamic docs) {
|
||||
if (docs is List) {
|
||||
for (var doc in docs) {
|
||||
if (doc is Map && doc['doc_type'] != null && doc['link'] != null) {
|
||||
String type = doc['doc_type'].toString();
|
||||
if (docUrls.containsKey(type)) {
|
||||
docUrls[type]!.value = doc['link'].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _populateControllers() {
|
||||
firstNameController.text = serverData['first_name'] ?? '';
|
||||
lastNameController.text = serverData['last_name'] ?? '';
|
||||
phoneController.text = serverData['phone'] ?? '';
|
||||
emailController.text = serverData['email'] ?? '';
|
||||
siteController.text = serverData['site'] ?? '';
|
||||
nationalNumberController.text = serverData['national_number'] ?? '';
|
||||
birthdateController.text = serverData['birthdate'] ?? '';
|
||||
addressController.text = serverData['address'] ?? '';
|
||||
licenseCategoriesController.text = serverData['license_categories'] ?? '';
|
||||
licenseTypeController.text = serverData['license_type'] ?? '';
|
||||
expiryDateController.text = serverData['expiry_date'] ?? '';
|
||||
licenseIssueDateController.text =
|
||||
serverData['licenseIssueDate'] ?? serverData['issue_date'] ?? '';
|
||||
selectedGender.value = serverData['gender'] ?? '';
|
||||
|
||||
ownerController.text = serverData['owner'] ?? '';
|
||||
carPlateController.text = serverData['car_plate'] ?? '';
|
||||
vinController.text = serverData['vin'] ?? '';
|
||||
carLicenseExpiryDateController.text =
|
||||
serverData['expiration_date'] ?? '';
|
||||
makeController.text = serverData['make'] ?? '';
|
||||
modelController.text = serverData['model'] ?? '';
|
||||
yearController.text = serverData['year'] ?? '';
|
||||
selectedFuel.value = serverData['fuel'] ?? '';
|
||||
|
||||
final serverColorName = serverData['color'] ?? '';
|
||||
final colorOption = kCarColorOptions.firstWhere(
|
||||
(opt) => opt['key'] == serverColorName,
|
||||
orElse: () => {'key': serverColorName, 'hex': '#000000'},
|
||||
);
|
||||
colorName.value = colorOption['key']!;
|
||||
colorHex.value = colorOption['hex']!;
|
||||
|
||||
phone.value = serverData['phone'] ?? '';
|
||||
}
|
||||
|
||||
Future<void> selectDate(
|
||||
BuildContext context, TextEditingController controller) async {
|
||||
DateTime initialDate = DateTime.now();
|
||||
try {
|
||||
if (controller.text.isNotEmpty) {
|
||||
initialDate = DateFormat('yyyy-MM-dd').parse(controller.text);
|
||||
}
|
||||
} catch (_) {}
|
||||
DateTime? pickedDate = initialDate;
|
||||
await showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
height: 280,
|
||||
color: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CupertinoDatePicker(
|
||||
initialDateTime: initialDate,
|
||||
minimumDate: DateTime(1950),
|
||||
maximumDate: DateTime(2050),
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
pickedDate = newDate;
|
||||
},
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
child: const Text('Done'),
|
||||
onPressed: () {
|
||||
if (pickedDate != null) {
|
||||
controller.text =
|
||||
DateFormat('yyyy-MM-dd').format(pickedDate!);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildPayload() {
|
||||
return {
|
||||
'driverId': driverId.value,
|
||||
'phone': phoneController.text,
|
||||
'first_name': firstNameController.text,
|
||||
'last_name': lastNameController.text,
|
||||
'site': siteController.text,
|
||||
'national_number': nationalNumberController.text,
|
||||
'gender': selectedGender.value,
|
||||
'birthdate': birthdateController.text,
|
||||
'address': addressController.text,
|
||||
'email': emailController.text,
|
||||
'license_type': licenseTypeController.text,
|
||||
'license_categories': licenseCategoriesController.text,
|
||||
'expiry_date': expiryDateController.text,
|
||||
'license_issue_date': licenseIssueDateController.text,
|
||||
'maritalStatus': box.read(BoxName.employeename) ?? 'unknown',
|
||||
'owner': ownerController.text,
|
||||
'color': colorName.value,
|
||||
'color_hex': colorHex.value,
|
||||
'car_plate': carPlateController.text,
|
||||
'expiration_date': carLicenseExpiryDateController.text,
|
||||
'make': makeController.text,
|
||||
'model': modelController.text,
|
||||
'fuel': selectedFuel.value,
|
||||
'year': yearController.text,
|
||||
'vin': vinController.text,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> saveChanges() async {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
var payload = _buildPayload();
|
||||
payload['status'] = 'pending_review';
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.updateDriverToActive,
|
||||
payload: payload,
|
||||
);
|
||||
if (response != 'failure' && response['status'] == 'success') {
|
||||
Get.snackbar('Success', 'Data saved successfully');
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to save changes');
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error', 'Error: $e');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> activateDriver() async {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
var payload = _buildPayload();
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.updateDriverToActive,
|
||||
payload: payload,
|
||||
);
|
||||
if (response != 'failure' && response['status'] == 'success') {
|
||||
Get.snackbar('Success', 'Driver activated successfully!');
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Get.back();
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to activate driver');
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error', 'Error: $e');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> rejectDriver(String reason) async {
|
||||
if (reason.trim().isEmpty) {
|
||||
Get.snackbar('Error', 'Please enter a rejection reason');
|
||||
return;
|
||||
}
|
||||
isSaving.value = true;
|
||||
try {
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.rejectDriver,
|
||||
payload: {
|
||||
'driverId': driverId.value,
|
||||
'reason': reason.trim(),
|
||||
},
|
||||
);
|
||||
if (response != 'failure' && response['status'] == 'success') {
|
||||
Get.snackbar('Success', 'Driver rejected');
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Get.back();
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to reject driver');
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error', 'Error: $e');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void showRejectDialog() {
|
||||
final reasonController = TextEditingController();
|
||||
Get.defaultDialog(
|
||||
title: 'Reject Driver'.tr,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: reasonController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter rejection reason...'.tr,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () => rejectDriver(reasonController.text),
|
||||
child: Text('Reject'.tr, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
cancel: ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Cancel'.tr),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static const List<Map<String, String>> kCarColorOptions = [
|
||||
{'key': 'white', 'hex': '#FFFFFF'},
|
||||
{'key': 'black', 'hex': '#000000'},
|
||||
{'key': 'silver', 'hex': '#C0C0C0'},
|
||||
{'key': 'gray', 'hex': '#808080'},
|
||||
{'key': 'gunmetal', 'hex': '#2A3439'},
|
||||
{'key': 'red', 'hex': '#C62828'},
|
||||
{'key': 'blue', 'hex': '#1565C0'},
|
||||
{'key': 'navy', 'hex': '#0D47A1'},
|
||||
{'key': 'green', 'hex': '#2E7D32'},
|
||||
{'key': 'darkGreen', 'hex': '#1B5E20'},
|
||||
{'key': 'beige', 'hex': '#D7CCC8'},
|
||||
{'key': 'brown', 'hex': '#5D4037'},
|
||||
{'key': 'maroon', 'hex': '#800000'},
|
||||
{'key': 'burgundy', 'hex': '#800020'},
|
||||
{'key': 'yellow', 'hex': '#F9A825'},
|
||||
{'key': 'orange', 'hex': '#EF6C00'},
|
||||
{'key': 'gold', 'hex': '#D4AF37'},
|
||||
{'key': 'bronze', 'hex': '#CD7F32'},
|
||||
{'key': 'champagne', 'hex': '#EFE1C6'},
|
||||
{'key': 'purple', 'hex': '#6A1B9A'},
|
||||
];
|
||||
|
||||
static const List<String> kFuelOptions = [
|
||||
'بنزين',
|
||||
'ديزل',
|
||||
'هايبرد',
|
||||
'كهربائي',
|
||||
];
|
||||
|
||||
Color hexToColor(String code) =>
|
||||
Color(int.parse(code.substring(1, 7), radix: 16) + 0xFF000000);
|
||||
|
||||
void updateColorSelection(String hex) {
|
||||
colorHex.value = hex;
|
||||
colorName.value =
|
||||
kCarColorOptions.firstWhere((o) => o['hex'] == hex)['key']!;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
firstNameController.dispose();
|
||||
lastNameController.dispose();
|
||||
phoneController.dispose();
|
||||
emailController.dispose();
|
||||
siteController.dispose();
|
||||
nationalNumberController.dispose();
|
||||
birthdateController.dispose();
|
||||
addressController.dispose();
|
||||
licenseCategoriesController.dispose();
|
||||
licenseTypeController.dispose();
|
||||
expiryDateController.dispose();
|
||||
licenseIssueDateController.dispose();
|
||||
ownerController.dispose();
|
||||
carPlateController.dispose();
|
||||
vinController.dispose();
|
||||
carLicenseExpiryDateController.dispose();
|
||||
makeController.dispose();
|
||||
modelController.dispose();
|
||||
yearController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user