Files
tripz-llc/apps/driver/lib/views/auth/syria/registration_view.dart
T
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:14:13 +03:00

596 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../controller/auth/syria/registration_controller.dart';
import '../../../main.dart';
class RegistrationView extends StatelessWidget {
const RegistrationView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final RegistrationController controller = Get.put(RegistrationController());
return Scaffold(
appBar: AppBar(
title: Text('Driver Registration'.tr),
centerTitle: true,
),
body: Column(
children: [
SizedBox(
height: 90,
child: Obx(
() => Stepper(
currentStep: controller.currentPage.value,
type: StepperType.horizontal,
controlsBuilder: (_, __) => const SizedBox.shrink(),
steps: [
Step(
title: Text('Driver'.tr),
content: const SizedBox.shrink()),
Step(
title: Text('Vehicle'.tr),
content: const SizedBox.shrink()),
Step(
title: Text('Docs'.tr), content: const SizedBox.shrink()),
],
),
),
),
Expanded(
child: PageView(
controller: controller.pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (i) => controller.currentPage.value = i,
children: [
_buildDriverInfoStep(context, controller),
_buildCarInfoStep(context, controller),
_buildDocumentUploadStep(context, controller),
],
),
),
],
),
bottomNavigationBar: _buildBottomNavBar(controller),
);
}
// STEP 1
Widget _buildDriverInfoStep(BuildContext ctx, RegistrationController c) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: c.driverInfoFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Driver's Personal Information".tr,
style:
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
TextFormField(
controller: c.firstNameController,
decoration: InputDecoration(
labelText: 'First Name'.tr,
border: const OutlineInputBorder(),
),
validator: (v) {
if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
if (v.length < 2) {
return 'Name must be at least 2 characters'.tr;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: c.lastNameController,
decoration: InputDecoration(
labelText: 'Last Name'.tr,
border: const OutlineInputBorder(),
),
validator: (v) {
if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
if (v.length < 2) {
return 'Name must be at least 2 characters'.tr;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: c.nationalIdController,
decoration: InputDecoration(
labelText: 'National ID Number'.tr,
border: const OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (v) {
if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
final country = box.read(BoxName.countryCode) ?? 'Jordan';
int expected;
if (country == 'Syria') {
expected = 11;
} else if (country == 'Egypt') {
expected = 12;
} else {
expected = 10; // Jordan
}
if (v.length != expected) {
return 'National ID must be $expected digits'.tr;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: c.bithdateController,
decoration: InputDecoration(
labelText: 'سنة الميلاد'.tr,
hintText: '1999'.tr,
border: const OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (v) {
if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
if (v.length != 4) {
return 'Birth year must be 4 digits'.tr;
}
// Optional: check if it’s a valid number
if (int.tryParse(v) == null) {
return 'Enter a valid year'.tr;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: c.siteController,
decoration: InputDecoration(
labelText: 'City / Governorate'.tr,
hintText: 'e.g. Amman'.tr,
border: const OutlineInputBorder(),
),
validator: (v) {
if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: c.driverLicenseExpiryController,
decoration: InputDecoration(
labelText: 'License Expiry Date'.tr,
hintText: 'YYYY-MM-DD'.tr,
border: const OutlineInputBorder()),
readOnly: true,
onTap: () async {
DateTime? d = await showDatePicker(
context: ctx,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (d != null) {
c.driverLicenseExpiryDate = d;
c.driverLicenseExpiryController.text =
d.toLocal().toString().split(' ')[0];
}
},
validator: (v) =>
(v?.isEmpty ?? true) ? 'Please select a date'.tr : null,
),
],
),
),
);
}
// STEP 2
Widget _buildCarInfoStep(BuildContext ctx, RegistrationController c) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: c.carInfoFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Vehicle Information'.tr,
style:
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
// ============================================================
// 1. القائمة المنسدلة لتصنيف المركبة (سيارة، دراجة، فان)
// ============================================================
DropdownButtonFormField<int>(
value: c.selectedVehicleCategoryId,
decoration: InputDecoration(
labelText: 'Vehicle Category'.tr, // ترجمة: تصنيف المركبة
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.directions_car),
),
items: c.vehicleCategoryOptions.map((item) {
return DropdownMenuItem<int>(
value: item['id'],
child: Text(item['name']),
);
}).toList(),
onChanged: (val) {
c.selectedVehicleCategoryId = val;
c.update(); // تحديث الواجهة إذا لزم الأمر
},
validator: (v) => (v == null) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
// ============================================================
// 2. القائمة المنسدلة لنوع الوقود (بنزين، كهرباء...)
// ============================================================
DropdownButtonFormField<int>(
value: c.selectedFuelTypeId,
decoration: InputDecoration(
labelText: 'Fuel Type'.tr, // ترجمة: نوع الوقود
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.local_gas_station),
),
items: c.fuelTypeOptions.map((item) {
return DropdownMenuItem<int>(
value: item['id'],
child: Text(item['name']),
);
}).toList(),
onChanged: (val) {
c.selectedFuelTypeId = val;
c.update();
},
validator: (v) => (v == null) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
TextFormField(
controller: c.carPlateController,
decoration: InputDecoration(
labelText: 'Car Plate Number'.tr,
border: const OutlineInputBorder()),
validator: (v) =>
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
TextFormField(
controller: c.carMakeController,
decoration: InputDecoration(
labelText: 'Car Make (e.g., Toyota)'.tr,
border: const OutlineInputBorder()),
validator: (v) =>
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
TextFormField(
controller: c.carModelController,
decoration: InputDecoration(
labelText: 'Car Model (e.g., Corolla)'.tr,
border: const OutlineInputBorder()),
validator: (v) =>
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
TextFormField(
controller: c.carYearController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Year of Manufacture'.tr,
border: const OutlineInputBorder()),
validator: (v) =>
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
// حقل اسم اللون (يبقى اختياري أو نملؤه تلقائيًا عند اختيار الهكس)
// TextFormField(
// controller: c.carColorController,
// decoration: InputDecoration(
// labelText: 'Car Color (Name)'.tr,
// border: const OutlineInputBorder(),
// ),
// validator: (v) =>
// (v?.isEmpty ?? true) ? 'Required field'.tr : null,
// ),
// const SizedBox(height: 16),
// الدروب داون للهكس + دائرة اللون
GetBuilder<RegistrationController>(
id: 'carColor', // اختياري لتحديث انتقائي
builder: (c) {
return DropdownButtonFormField<String>(
value: (c.colorHex != null && c.colorHex!.isNotEmpty)
? c.colorHex
: null,
isExpanded: true,
decoration: InputDecoration(
labelText: 'Car Color (Hex)'.tr,
border: const OutlineInputBorder(),
// prefixIcon: Padding(
// padding:
// const EdgeInsetsDirectional.only(start: 12, end: 8),
// child: Container(
// width: 18,
// height: 18,
// decoration: BoxDecoration(
// color: (c.colorHex?.isNotEmpty ?? false)
// ? c.hexToColor(c.colorHex!)
// : Colors.transparent,
// shape: BoxShape.circle,
// border: Border.all(color: Colors.black26),
// ),
// ),
// ),
),
items: RegistrationController.kCarColorOptions.map((opt) {
final hex = opt['hex']!;
final key = opt['key']!;
return DropdownMenuItem<String>(
value: hex,
child: Row(
children: [
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: c.hexToColor(hex),
shape: BoxShape.circle,
border: Border.all(color: Colors.black12),
),
),
const SizedBox(width: 8),
Expanded(child: Text(key.tr)),
],
),
);
}).toList(),
onChanged: (hex) {
c.colorHex = hex; // خزّن الهكس
final key = RegistrationController.kCarColorOptions
.firstWhere((o) => o['hex'] == hex)['key']!;
c.carColorController.text = key.tr;
c.update([
'carColor'
]); // <-- مهم: يعيد بناء الودجت ويحدّث الدائرة
},
validator: (v) =>
(v == null || v.isEmpty) ? 'Required field'.tr : null,
);
},
)
],
),
),
);
}
// STEP 3
Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) {
final String primaryLink =
'${AppLink.server}/auth/driver/uploadImage.php';
return GetBuilder<RegistrationController>(
builder: (ctrl) => SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Upload Documents'.tr,
style:
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
_buildImagePickerBox(
'National ID (Front)'.tr,
ctrl.docUrls['id_front'],
() async =>
await ctrl.choosImage(primaryLink, 'id_front'),
),
_buildImagePickerBox(
'National ID (Back)'.tr,
ctrl.docUrls['id_back'],
() async =>
await ctrl.choosImage(primaryLink, 'id_back'),
),
_buildImagePickerBox(
'Driver License (Front)'.tr,
ctrl.docUrls['driver_license'],
() async =>
await ctrl.choosImage(primaryLink, 'driver_license'),
),
if (box.read(BoxName.countryCode) == 'Syria')
_buildImagePickerBox(
'Driver License (Back)'.tr,
ctrl.docUrls['driver_license_back'],
() async =>
await ctrl.choosImage(primaryLink, 'driver_license_back'),
),
_buildImagePickerBox(
'Profile Picture'.tr,
ctrl.docUrls['profile_picture'],
() async =>
await ctrl.choosImage(primaryLink, 'profile_picture'),
),
_buildImagePickerBox(
ctrl.getCriminalRecordTitle(),
ctrl.docUrls['criminal_record'],
() async =>
await ctrl.choosImage(primaryLink, 'criminal_record'),
),
_buildImagePickerBox(
'Car Registration (Front)'.tr,
ctrl.docUrls['car_license_front'],
() async =>
await ctrl.choosImage(primaryLink, 'car_license_front'),
),
_buildImagePickerBox(
'Car Registration (Back)'.tr,
ctrl.docUrls['car_license_back'],
() async =>
await ctrl.choosImage(primaryLink, 'car_license_back'),
),
],
),
),
);
}
Widget signedImageWithAuth(String fileUrl, String bearerToken) {
return Image.network(
fileUrl,
headers: {'Authorization': 'Bearer $bearerToken'},
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Text('Image expired or unauthorized'),
);
}
Widget _buildImagePickerBox(String title, String? img, VoidCallback onTap) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: onTap,
child: SizedBox(
height: 150,
width: double.infinity,
child: Stack(
children: [
// Card template background
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.grey[300]!,
width: 1.5,
strokeAlign: BorderSide.strokeAlignInside,
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.credit_card, size: 48, color: Colors.grey[300]),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
color: Colors.grey[400],
fontSize: 13,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
),
// Uploaded image with 50% opacity
if (img != null && img.isNotEmpty)
Positioned.fill(
child: Opacity(
opacity: 0.5,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.network(
img,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.broken_image, size: 40, color: Colors.red),
const SizedBox(height: 8),
Text('Image not available',
style: TextStyle(color: Colors.red[700])),
],
),
);
},
),
),
),
),
// Empty state with camera icon
if (img == null || img.isEmpty)
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.camera_alt_outlined,
size: 40, color: Colors.grey[600]),
const SizedBox(height: 8),
Text('Tap to upload'.tr,
style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
),
),
],
),
),
),
);
}
Widget _buildBottomNavBar(RegistrationController c) {
return Obx(() => Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (c.currentPage.value > 0)
TextButton(
onPressed: c.goToPreviousStep,
child: Text('<< BACK'.tr),
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 40, vertical: 12),
backgroundColor: c.currentPage.value == 2
? Colors.green
: Theme.of(Get.context!).primaryColor,
),
onPressed: c.isLoading.value
? null
: () {
if (c.currentPage.value == 2) {
c.submitRegistration();
} else {
c.goToNextStep();
}
},
child: c.isLoading.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: Text(
c.currentPage.value == 2 ? 'SUBMIT'.tr : 'NEXT >>'.tr,
style:
const TextStyle(fontSize: 16, color: Colors.white),
),
),
],
),
));
}
}