- 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>
217 lines
7.9 KiB
Dart
Executable File
217 lines
7.9 KiB
Dart
Executable File
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();
|
||
}
|
||
}
|