add 8-tab review page, review controller, updateDriverToActive handles new fields, drivers_cant_register navigates to review
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user