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:
@@ -0,0 +1,216 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/mydialoug.dart';
|
||||
import 'ai_eg_controller.dart';
|
||||
|
||||
class CriminalDocumemtPage extends StatelessWidget {
|
||||
const CriminalDocumemtPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(AI()); // التأكد من وجود الكنترولر
|
||||
|
||||
return MyScafolld(
|
||||
title: "صحيفة الحالة الجنائية".tr, // تعريب العنوان
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<AI>(builder: (controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// رسالة توضيحية للسائق
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: Colors.blue),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"هذه الوثيقة تساعد في توثيق حسابك وزيادة فرص قبول الرحلات."
|
||||
.tr,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// بطاقة رفع الفيش
|
||||
_buildCriminalRecordCard(controller),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// زر الحفظ (يظهر فقط إذا تم الرفع والتحقق)
|
||||
if (controller.responseCriminalRecordEgypt.isNotEmpty)
|
||||
MyElevatedButton(
|
||||
title: 'تأكيد وحفظ'.tr,
|
||||
// width: double.infinity,
|
||||
onPressed: () => _handleSave(controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget بطاقة الرفع والعرض ---
|
||||
Widget _buildCriminalRecordCard(AI ai) {
|
||||
bool hasData = ai.responseCriminalRecordEgypt.isNotEmpty;
|
||||
|
||||
if (hasData) {
|
||||
var data = ai.responseCriminalRecordEgypt;
|
||||
|
||||
// جلب اسم السائق المسجل حالياً في التطبيق (من الـ Box أو السيرفر)
|
||||
// ملاحظة: تأكد أنك تحفظ الاسم العربي في BoxName.nameArabic عند تسجيل الدخول
|
||||
String currentDriverName = box.read(BoxName.nameArabic) ?? '';
|
||||
|
||||
// مقارنة تقريبية (إزالة المسافات)
|
||||
bool isNameMatch = _compareNames(data['FullName'], currentDriverName);
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("البيانات المستخرجة".tr, style: AppStyle.headTitle2),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, color: AppColor.primaryColor),
|
||||
onPressed: () => ai.allMethodForAI('criminal_record'),
|
||||
)
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
_infoRow("الاسم في الفيش".tr, data['FullName'],
|
||||
isError: !isNameMatch),
|
||||
if (!isNameMatch)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"الاسم لا يطابق اسمك المسجل: $currentDriverName".tr,
|
||||
style: const TextStyle(color: Colors.red, fontSize: 10),
|
||||
),
|
||||
),
|
||||
_infoRow("الرقم القومي".tr, data['NationalID']),
|
||||
_infoRow("النتيجة".tr, data['InspectionResult'],
|
||||
isSuccess:
|
||||
data['InspectionResult'].toString().contains("لا يوجد")),
|
||||
_infoRow("تاريخ الإصدار".tr, data['IssueDate']),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// حالة الانتظار (زر الرفع)
|
||||
return GestureDetector(
|
||||
onTap: () => ai.allMethodForAI('criminal_record'),
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!, width: 2, style: BorderStyle.solid),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('assets/images/6.png',
|
||||
height: 80, color: Colors.grey), // أيقونة بصمة أو مستند
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
"اضغط لرفع صورة الفيش".tr,
|
||||
style: AppStyle.title.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String label, String? value,
|
||||
{bool isError = false, bool isSuccess = false}) {
|
||||
Color textColor = Colors.black87;
|
||||
if (isError) textColor = Colors.red;
|
||||
if (isSuccess) textColor = Colors.green;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Colors.grey)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value ?? "---",
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: textColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- منطق المقارنة والحفظ ---
|
||||
|
||||
bool _compareNames(String? extractedName, String storedName) {
|
||||
if (extractedName == null) return false;
|
||||
// تنظيف الأسماء (إزالة مسافات، توحيد الهمزات) للمقارنة الدقيقة
|
||||
String clean1 = extractedName
|
||||
.replaceAll(' ', '')
|
||||
.replaceAll('أ', 'ا')
|
||||
.replaceAll('إ', 'ا');
|
||||
String clean2 = storedName
|
||||
.replaceAll(' ', '')
|
||||
.replaceAll('أ', 'ا')
|
||||
.replaceAll('إ', 'ا');
|
||||
// نكتفي بمطابقة جزئية (مثلاً 50% من الأحرف) أو مطابقة كاملة حسب صرامة النظام
|
||||
return clean1.contains(clean2) || clean2.contains(clean1);
|
||||
}
|
||||
|
||||
void _handleSave(AI controller) async {
|
||||
String currentDriverName = box.read(BoxName.nameArabic) ?? '';
|
||||
String extractedName =
|
||||
controller.responseCriminalRecordEgypt['FullName'] ?? '';
|
||||
|
||||
if (!_compareNames(extractedName, currentDriverName)) {
|
||||
Get.defaultDialog(
|
||||
title: "خطأ في التطابق".tr,
|
||||
middleText:
|
||||
"اسمك في الفيش لا يطابق الاسم المسجل في حسابك.\nيرجى التأكد من الوثيقة."
|
||||
.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () => Get.back(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// إذا كان التطابق صحيحاً، نقوم برفع البيانات للسيرفر
|
||||
// هنا نفترض وجود دالة في الكنترولر لحفظ الفيش فقط
|
||||
await controller.saveCriminalRecordOnly();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user