feat: Gemini AI (doc OCR + face match) + role-assignment endpoints
- GeminiService (integrations/gemini, @Global): extractDocument (OCR fields) + faceMatch; graceful if no GEMINI_API_KEY - documents: face-match now real via Gemini; POST /admin/documents/:id/ocr extracts fields for CS auto-fill - roles: PATCH /admin/users/:id/role (admin) + POST /platform/users/role (bootstrap via x-platform-secret) - config: gemini + platform.secret; .env.example entries Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+308
@@ -0,0 +1,308 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../auth/captin/ai_eg_controller.dart';
|
||||
import '../../auth/captin/driver_car_controller.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
class CarsInsertingPage extends StatelessWidget {
|
||||
CarsInsertingPage({super.key});
|
||||
|
||||
// حقن الكنترولرز
|
||||
final driverCarController = Get.put(DriverCarController());
|
||||
final aiController = Get.put(AI());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Insert New Car'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Container(
|
||||
color: AppColor.accentColor.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
// 1. الوجه الأمامي (الذكاء الاصطناعي)
|
||||
egyptCarLicenceFront(),
|
||||
|
||||
// 2. الوجه الخلفي (الذكاء الاصطناعي)
|
||||
egyptCarLicenceBack(),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// تعليمات
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Please make sure to read the license carefully.'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'If your car license has the new design, upload the front side with two images.'
|
||||
.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 3. زر الحفظ النهائي
|
||||
GetBuilder<AI>(builder: (controller) {
|
||||
// التحقق من أن الصور تم رفعها
|
||||
bool isFrontReady =
|
||||
controller.responseIdCardDriverEgyptFront.isNotEmpty;
|
||||
bool isBackReady =
|
||||
controller.responseIdCardDriverEgyptBack.isNotEmpty;
|
||||
|
||||
return MyElevatedButton(
|
||||
title: 'Save New Car'.tr,
|
||||
// تعطيل الزر إذا لم تكتمل البيانات
|
||||
onPressed: _handleSaveCar,
|
||||
kolor: (isFrontReady && isBackReady)
|
||||
? AppColor.primaryColor
|
||||
: Colors.grey,
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// منطق الحفظ (يربط الـ AI بالـ DriverCarController)
|
||||
void _handleSaveCar() {
|
||||
final aiFront = Get.find<AI>().responseIdCardDriverEgyptFront;
|
||||
final aiBack = Get.find<AI>().responseIdCardDriverEgyptBack;
|
||||
|
||||
// استدعاء دالة الإضافة في كنترولر السيارات
|
||||
Get.find<DriverCarController>().addNewCarToAccount(
|
||||
vin: aiBack['chassis'].toString(),
|
||||
plate: aiFront['car_plate'].toString(), // اللوحة من الأمام
|
||||
make: aiBack['make'].toString(),
|
||||
model: aiBack['model'].toString(),
|
||||
year: aiBack['year'].toString(),
|
||||
expiryDate: aiFront['LicenseExpirationDate'].toString(),
|
||||
color: aiBack['color'].toString(),
|
||||
colorHex: aiBack['color_hex'].toString(),
|
||||
owner: aiFront['owner'].toString(),
|
||||
address: aiFront['address'].toString(),
|
||||
inspectionDate: aiBack['inspection_date'].toString(), // تاريخ الفحص
|
||||
displacement: aiBack['displacement'].toString(), // السعة اللترية
|
||||
fuel: aiBack['fuel'].toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Widget الوجه الأمامي ---
|
||||
GetBuilder<AI> egyptCarLicenceFront() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
|
||||
final licenseExpiryDate = DateTime.tryParse(
|
||||
ai.responseIdCardDriverEgyptFront['LicenseExpirationDate'] ??
|
||||
'') ??
|
||||
DateTime.now();
|
||||
final isLicenseExpired = licenseExpiryDate.isBefore(DateTime.now());
|
||||
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Vehicle Details Front'.tr,
|
||||
style: AppStyle.headTitle2),
|
||||
IconButton(
|
||||
// إعادة الرفع
|
||||
onPressed: () => ai.allMethodForAI('car_front'),
|
||||
icon: const Icon(Icons.refresh,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: AppColor.accentColor),
|
||||
_infoRow('Plate Number'.tr,
|
||||
ai.responseIdCardDriverEgyptFront['car_plate']),
|
||||
_infoRow('Owner Name'.tr,
|
||||
ai.responseIdCardDriverEgyptFront['owner']),
|
||||
_infoRow(
|
||||
'Address'.tr, ai.responseIdCardDriverEgyptFront['address']),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('License Expiry Date'.tr),
|
||||
Text(
|
||||
licenseExpiryDate.toString().split(' ')[0],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isLicenseExpired ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// حالة الانتظار (زر الرفع)
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () => ai.allMethodForAI('car_front'), // ✅ هنا التفعيل
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/3.png',
|
||||
height: Get.height * .20,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('Capture Car License (Front)'.tr,
|
||||
style: AppStyle.title),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget الوجه الخلفي ---
|
||||
GetBuilder<AI> egyptCarLicenceBack() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
|
||||
// منطق تواريخ الفحص والضريبة
|
||||
final inspectionDateStr =
|
||||
ai.responseIdCardDriverEgyptBack['inspection_date'] ?? '';
|
||||
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Vehicle Details Back'.tr, style: AppStyle.headTitle2),
|
||||
IconButton(
|
||||
// إعادة الرفع
|
||||
onPressed: () => ai.allMethodForAI('car_back'),
|
||||
icon: const Icon(Icons.refresh,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: AppColor.accentColor),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _infoRow('Make'.tr,
|
||||
ai.responseIdCardDriverEgyptBack['make'])),
|
||||
Expanded(
|
||||
child: _infoRow('Model'.tr,
|
||||
ai.responseIdCardDriverEgyptBack['model'])),
|
||||
],
|
||||
),
|
||||
_infoRow('Year'.tr, ai.responseIdCardDriverEgyptBack['year']),
|
||||
_infoRow(
|
||||
'Chassis'.tr, ai.responseIdCardDriverEgyptBack['chassis']),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _infoRow('Color'.tr,
|
||||
ai.responseIdCardDriverEgyptBack['color'])),
|
||||
// عرض دائرة اللون
|
||||
if (ai.responseIdCardDriverEgyptBack['color_hex'] != null)
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(ai
|
||||
.responseIdCardDriverEgyptBack['color_hex']
|
||||
.toString()
|
||||
.replaceFirst('#', '0xff'))),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.grey)),
|
||||
)
|
||||
],
|
||||
),
|
||||
_infoRow('Displacement'.tr,
|
||||
"${ai.responseIdCardDriverEgyptBack['displacement']} cc"),
|
||||
_infoRow('Fuel'.tr, ai.responseIdCardDriverEgyptBack['fuel']),
|
||||
_infoRow('Inspection Date'.tr, inspectionDateStr),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// حالة الانتظار
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () => ai.allMethodForAI('car_back'), // ✅ هنا التفعيل
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/4.png',
|
||||
height: Get.height * .20,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('Capture Car License (Back)'.tr,
|
||||
style: AppStyle.title),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Helper Widget
|
||||
Widget _infoRow(String label, String? value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("$label:", style: const TextStyle(color: Colors.grey)),
|
||||
Expanded(
|
||||
child: Text(value ?? "---",
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user