Update: 2026-05-06 01:38:39
This commit is contained in:
892
newplan.md
Normal file
892
newplan.md
Normal file
@@ -0,0 +1,892 @@
|
||||
# خطة تنفيذ مُصادَق — Flutter Mobile App
|
||||
## خارطة الطريق الشاملة بالأولويات والمكتبات والتفاصيل
|
||||
|
||||
---
|
||||
|
||||
## أولاً: قرارات المكتبات النهائية
|
||||
|
||||
### هيكل `pubspec.yaml` الكامل
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# ─── State Management ───────────────────────────────
|
||||
get: ^4.6.6 # GetX — state + routing + DI
|
||||
|
||||
# ─── Networking ─────────────────────────────────────
|
||||
dio: ^5.4.0 # HTTP client
|
||||
flutter_secure_storage: ^9.0.0 # تخزين JWT + secrets آمن
|
||||
|
||||
# ─── Local Database (Isar > Hive للمشروع هذا) ──────
|
||||
isar: ^3.1.0+1 # قاعدة بيانات محلية — أسرع من Hive
|
||||
isar_flutter_libs: ^3.1.0+1 # Flutter bindings
|
||||
path_provider: ^2.1.2 # مسارات الملفات
|
||||
|
||||
# ─── Authentication & Security ──────────────────────
|
||||
local_auth: ^2.1.8 # Fingerprint + FaceID
|
||||
device_info_plus: ^10.1.0 # Device fingerprinting
|
||||
crypto: ^3.0.3 # HMAC-SHA256
|
||||
|
||||
# ─── Camera & Scanning ──────────────────────────────
|
||||
camerawesome: ^2.0.0 # كاميرا متقدمة مع تحكم كامل
|
||||
cunning_document_scanner: ^0.2.0 # Edge detection + Auto-crop (iOS/Android)
|
||||
image_picker: ^1.0.7 # Gallery fallback
|
||||
|
||||
# ─── Image Processing ───────────────────────────────
|
||||
image: ^4.1.7 # معالجة صور Dart-native (offline)
|
||||
opencv_dart: ^1.3.2 # Adaptive thresholding + advanced ops
|
||||
flutter_image_compress: ^2.1.0 # ضغط JPEG بجودة قابلة للضبط
|
||||
|
||||
# ─── PDF Generation ─────────────────────────────────
|
||||
pdf: ^3.10.8 # إنشاء PDF بدعم العربية
|
||||
printing: ^5.12.0 # مشاركة + طباعة + preview
|
||||
|
||||
# ─── Voice & Audio ──────────────────────────────────
|
||||
record: ^5.1.0 # تسجيل صوتي (OGG/WAV) — أخف من flutter_sound
|
||||
permission_handler: ^11.3.0 # صلاحيات Mic + Camera + Storage
|
||||
|
||||
# ─── Connectivity & Background ──────────────────────
|
||||
connectivity_plus: ^6.0.3 # كشف الإنترنت
|
||||
workmanager: ^0.5.2 # Background sync jobs
|
||||
|
||||
# ─── UI & UX ────────────────────────────────────────
|
||||
cached_network_image: ^3.3.1 # صور من الشبكة
|
||||
shimmer: ^3.0.0 # Loading skeleton
|
||||
lottie: ^3.1.0 # Animations
|
||||
|
||||
# ─── Utilities ──────────────────────────────────────
|
||||
uuid: ^4.3.3 # Batch IDs
|
||||
intl: ^0.19.0 # تنسيق التواريخ والأرقام العربية
|
||||
package_info_plus: ^8.0.0 # App version
|
||||
|
||||
dev_dependencies:
|
||||
isar_generator: ^3.1.0+1
|
||||
build_runner: ^2.4.8
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
```
|
||||
|
||||
### لماذا Isar وليس Hive؟
|
||||
|
||||
| المعيار | Hive | Isar |
|
||||
|---------|------|------|
|
||||
| السرعة | جيد | أسرع 10x على الـ queries |
|
||||
| Type Safety | يدوي (TypeAdapters) | تلقائي عبر code generation |
|
||||
| Queries | محدود | Full query engine مع indexes |
|
||||
| الحجم | خفيف | أكبر قليلاً |
|
||||
| الاستخدام | بسيط جداً | يحتاج build_runner مرة واحدة |
|
||||
| الاختيار | MVP بسيط | **مُصادَق** — لأننا نحتاج queries معقدة على الفواتير المحلية |
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 1 — أساس المشروع والأمان (الأسبوع 1-3)
|
||||
|
||||
### 1.1 هيكل مجلدات Flutter
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart
|
||||
├── app/
|
||||
│ ├── bindings/ # GetX bindings
|
||||
│ ├── routes/ # AppPages + AppRoutes
|
||||
│ └── theme/ # AppColors, AppStyles
|
||||
├── core/
|
||||
│ ├── network/
|
||||
│ │ ├── dio_client.dart # Dio instance
|
||||
│ │ └── hmac_interceptor.dart # HMAC signing
|
||||
│ ├── storage/
|
||||
│ │ ├── isar_service.dart # Isar instance singleton
|
||||
│ │ └── secure_storage.dart # flutter_secure_storage wrapper
|
||||
│ ├── services/
|
||||
│ │ ├── auth_service.dart
|
||||
│ │ ├── biometric_service.dart
|
||||
│ │ └── device_service.dart # fingerprint + hardware check
|
||||
│ └── constants/
|
||||
│ ├── app_link.dart # API endpoints
|
||||
│ ├── app_color.dart
|
||||
│ └── box_name.dart # Isar collection names
|
||||
├── features/
|
||||
│ ├── auth/ # Login + OTP + Biometric
|
||||
│ ├── scanner/ # Batch camera + image processing
|
||||
│ ├── invoices/ # Invoice list + detail
|
||||
│ ├── voice/ # Voice assistant
|
||||
│ └── dashboard/ # Main dashboard
|
||||
└── shared/
|
||||
├── widgets/
|
||||
└── models/
|
||||
```
|
||||
|
||||
### 1.2 HMAC Interceptor (أمان الـ API)
|
||||
|
||||
```dart
|
||||
// core/network/hmac_interceptor.dart
|
||||
class HmacInterceptor extends Interceptor {
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final body = options.data != null ? jsonEncode(options.data) : '';
|
||||
final apiSecret = SecureStorage.read(BoxName.apiSecret);
|
||||
|
||||
// HMAC-SHA256: timestamp + method + path + body
|
||||
final message = '$timestamp|${options.method}|${options.path}|$body';
|
||||
final hmac = Hmac(sha256, utf8.encode(apiSecret ?? ''));
|
||||
final signature = hmac.convert(utf8.encode(message)).toString();
|
||||
|
||||
options.headers['X-Timestamp'] = timestamp;
|
||||
options.headers['X-Signature'] = signature;
|
||||
options.headers['Authorization'] = 'Bearer ${SecureStorage.read(BoxName.jwt)}';
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 نظام المصادقة — SMS OTP + Biometric
|
||||
|
||||
**التدفق الكامل:**
|
||||
|
||||
```
|
||||
[المدير يُضيف محاسب + رقم هاتفه في Web Dashboard]
|
||||
↓
|
||||
[المحاسب يفتح التطبيق → يدخل رقم الهاتف]
|
||||
↓
|
||||
[Backend يرسل OTP عبر SMS (Twilio أو منصة محلية أردنية)]
|
||||
↓
|
||||
[المحاسب يدخل OTP → Backend يتحقق → يرجع JWT]
|
||||
↓
|
||||
[التطبيق يحفظ JWT + يطلب تفعيل البصمة/FaceID]
|
||||
↓
|
||||
[عمليات الدخول التالية: بصمة فقط (أو PIN كـ fallback)]
|
||||
```
|
||||
|
||||
```dart
|
||||
// features/auth/controllers/auth_controller.dart
|
||||
class AuthController extends GetxController {
|
||||
final _biometricService = Get.find<BiometricService>();
|
||||
final _authService = Get.find<AuthService>();
|
||||
final _deviceService = Get.find<DeviceService>();
|
||||
|
||||
Future<void> requestOtp(String phone) async {
|
||||
final deviceFingerprint = await _deviceService.getFingerprint();
|
||||
// إرسال OTP مع fingerprint للتحقق منه في Backend
|
||||
await _authService.requestOtp(phone: phone, deviceId: deviceFingerprint);
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(String phone, String otp) async {
|
||||
final deviceFingerprint = await _deviceService.getFingerprint();
|
||||
final result = await _authService.verifyOtp(
|
||||
phone: phone,
|
||||
otp: otp,
|
||||
deviceId: deviceFingerprint,
|
||||
);
|
||||
// حفظ JWT في secure storage
|
||||
await SecureStorage.write(BoxName.jwt, result.token);
|
||||
await SecureStorage.write(BoxName.apiSecret, result.apiSecret);
|
||||
|
||||
// عرض شاشة تفعيل البصمة
|
||||
if (await _biometricService.isAvailable()) {
|
||||
Get.toNamed(AppRoutes.biometricSetup);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loginWithBiometric() async {
|
||||
final authenticated = await _biometricService.authenticate();
|
||||
if (authenticated) {
|
||||
// التحقق من JWT مخزن + تجديده إذا انتهت صلاحيته
|
||||
final token = await SecureStorage.read(BoxName.jwt);
|
||||
if (token != null) Get.offAllNamed(AppRoutes.dashboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// core/services/device_service.dart
|
||||
class DeviceService extends GetxService {
|
||||
Future<String> getFingerprint() async {
|
||||
final info = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
final android = await info.androidInfo;
|
||||
// Combination of stable device identifiers
|
||||
final raw = '${android.brand}|${android.model}|${android.id}';
|
||||
return sha256.convert(utf8.encode(raw)).toString();
|
||||
} else {
|
||||
final ios = await info.iosInfo;
|
||||
final raw = '${ios.model}|${ios.identifierForVendor}';
|
||||
return sha256.convert(utf8.encode(raw)).toString();
|
||||
}
|
||||
}
|
||||
|
||||
Future<DeviceCapability> checkCapability() async {
|
||||
final info = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
final android = await info.androidInfo;
|
||||
final sdkInt = android.version.sdkInt;
|
||||
// Android 10+ (API 29) = متوافق
|
||||
if (sdkInt >= 29) return DeviceCapability.full;
|
||||
if (sdkInt >= 26) return DeviceCapability.limited;
|
||||
return DeviceCapability.unsupported;
|
||||
}
|
||||
// iOS 14+ = متوافق
|
||||
return DeviceCapability.full;
|
||||
}
|
||||
}
|
||||
|
||||
enum DeviceCapability { full, limited, unsupported }
|
||||
```
|
||||
|
||||
**Backend PHP — التحقق من OTP + Device:**
|
||||
|
||||
```php
|
||||
// api/auth/verify_otp.php
|
||||
function verifyOtpAndIssueToken($phone, $otp, $deviceId) {
|
||||
// 1. تحقق من OTP
|
||||
$storedOtp = Redis::get("otp:$phone");
|
||||
if ($storedOtp !== $otp) return ['error' => 'OTP غير صحيح'];
|
||||
|
||||
// 2. جلب المستخدم
|
||||
$user = User::where('phone', $phone)->first();
|
||||
if (!$user) return ['error' => 'المستخدم غير موجود'];
|
||||
|
||||
// 3. تسجيل/تحديث الجهاز
|
||||
UserDevice::updateOrCreate(
|
||||
['user_id' => $user->id, 'device_fingerprint' => $deviceId],
|
||||
['last_seen' => now(), 'is_trusted' => true]
|
||||
);
|
||||
|
||||
// 4. توليد JWT مع device_id مضمّن
|
||||
$payload = [
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $user->tenant_id,
|
||||
'device_id' => $deviceId,
|
||||
'exp' => time() + (30 * 24 * 3600) // 30 يوم
|
||||
];
|
||||
$token = JWT::encode($payload, config('jwt_secret'), 'HS256');
|
||||
|
||||
// 5. توليد API Secret خاص بهذا الجهاز للـ HMAC
|
||||
$apiSecret = hash('sha256', $user->id . $deviceId . config('app_key'));
|
||||
|
||||
Redis::del("otp:$phone");
|
||||
return ['token' => $token, 'api_secret' => $apiSecret];
|
||||
}
|
||||
```
|
||||
|
||||
**جدول `user_devices` المطلوب إضافته:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_devices (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
device_fingerprint VARCHAR(64) NOT NULL,
|
||||
device_name VARCHAR(100),
|
||||
is_trusted TINYINT(1) DEFAULT 0,
|
||||
last_seen TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
UNIQUE KEY unique_device (user_id, device_fingerprint)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 2 — الماسح الذكي ومعالجة الصور (الأسبوع 3-7)
|
||||
|
||||
### 2.1 وضع الدفعة (Batch Scan Mode)
|
||||
|
||||
```dart
|
||||
// features/scanner/controllers/batch_scanner_controller.dart
|
||||
class BatchScannerController extends GetxController {
|
||||
final images = <File>[].obs;
|
||||
final isProcessing = false.obs;
|
||||
final processedCount = 0.obs;
|
||||
|
||||
// Pipeline: Scan → Process → Queue → Upload
|
||||
Future<void> addAndProcessImage(File rawImage) async {
|
||||
final capability = await DeviceService().checkCapability();
|
||||
|
||||
File processed;
|
||||
if (capability == DeviceCapability.full) {
|
||||
// معالجة كاملة على الجهاز
|
||||
processed = await ImageProcessor.processLocally(rawImage);
|
||||
} else {
|
||||
// ضغط فقط — المعالجة على السيرفر
|
||||
processed = await ImageProcessor.compressOnly(rawImage);
|
||||
}
|
||||
|
||||
images.add(processed);
|
||||
processedCount.value = images.length;
|
||||
}
|
||||
|
||||
Future<void> generateAndUpload(String companyId) async {
|
||||
isProcessing.value = true;
|
||||
|
||||
// 1. دمج الصور في PDF
|
||||
final pdf = await PdfGenerator.fromImages(images, companyName: companyId);
|
||||
|
||||
// 2. حفظ في Isar كـ pending batch
|
||||
final batch = InvoiceBatch(
|
||||
id: Uuid().v4(),
|
||||
companyId: companyId,
|
||||
pdfPath: pdf.path,
|
||||
status: BatchStatus.pending,
|
||||
imageCount: images.length,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
await IsarService.instance.writeTxn(() async {
|
||||
await IsarService.instance.invoiceBatchs.put(batch);
|
||||
});
|
||||
|
||||
// 3. محاولة رفع فورية إذا في إنترنت
|
||||
final hasConnection = await ConnectivityService.isConnected();
|
||||
if (hasConnection) {
|
||||
await _uploadBatch(batch);
|
||||
}
|
||||
// وإلا WorkManager سيتولى الرفع لاحقاً
|
||||
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 معالجة الصور — Pipeline كامل
|
||||
|
||||
```dart
|
||||
// core/services/image_processor.dart
|
||||
class ImageProcessor {
|
||||
/// Full pipeline: Edge → Perspective → Grayscale → Threshold → Compress
|
||||
static Future<File> processLocally(File input) async {
|
||||
// 1. تحميل الصورة
|
||||
final bytes = await input.readAsBytes();
|
||||
img.Image image = img.decodeImage(bytes)!;
|
||||
|
||||
// 2. تدرج الرمادي
|
||||
image = img.grayscale(image);
|
||||
|
||||
// 3. زيادة التباين (يساعد OCR)
|
||||
image = img.adjustColor(image, contrast: 1.5);
|
||||
|
||||
// 4. تحسين الحدة
|
||||
image = img.sharpen(image, amount: 1.0);
|
||||
|
||||
// 5. Binarization بسيط عبر image package
|
||||
image = _applyThreshold(image, threshold: 128);
|
||||
|
||||
// ملاحظة: لـ Adaptive Thresholding الحقيقي → استخدم opencv_dart
|
||||
// final mat = await opencv.imread(input.path);
|
||||
// final processed = await opencv.adaptiveThreshold(mat, 255,
|
||||
// opencv.ADAPTIVE_THRESH_GAUSSIAN_C, opencv.THRESH_BINARY, 11, 2);
|
||||
|
||||
// 6. ضغط JPEG بجودة 85%
|
||||
final compressed = img.encodeJpg(image, quality: 85);
|
||||
|
||||
// حجم متوقع: 4MB → ~200KB
|
||||
final output = File('${input.parent.path}/processed_${input.path.split('/').last}');
|
||||
await output.writeAsBytes(compressed);
|
||||
return output;
|
||||
}
|
||||
|
||||
static img.Image _applyThreshold(img.Image src, {int threshold = 128}) {
|
||||
return img.adjustColor(src,
|
||||
hueRotation: 0,
|
||||
saturation: 0,
|
||||
exposure: 0,
|
||||
gamma: threshold / 128.0,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<File> compressOnly(File input) async {
|
||||
final compressed = await FlutterImageCompress.compressAndGetFile(
|
||||
input.path,
|
||||
'${input.parent.path}/compressed_${input.path.split('/').last}',
|
||||
quality: 70,
|
||||
minWidth: 1200,
|
||||
minHeight: 1600,
|
||||
);
|
||||
return compressed ?? input;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 توليد PDF من الصور
|
||||
|
||||
```dart
|
||||
// core/services/pdf_generator.dart
|
||||
class PdfGenerator {
|
||||
static Future<File> fromImages(
|
||||
List<File> images, {
|
||||
required String companyName,
|
||||
}) async {
|
||||
final pdf = pw.Document();
|
||||
|
||||
for (final imageFile in images) {
|
||||
final imageBytes = await imageFile.readAsBytes();
|
||||
final pdfImage = pw.MemoryImage(imageBytes);
|
||||
|
||||
pdf.addPage(pw.Page(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.all(0),
|
||||
build: (ctx) => pw.Image(pdfImage, fit: pw.BoxFit.contain),
|
||||
));
|
||||
}
|
||||
|
||||
final date = DateTime.now().toString().split(' ')[0];
|
||||
final filename = 'فواتير_${companyName}_$date.pdf';
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final file = File('${dir.path}/$filename');
|
||||
await file.writeAsBytes(await pdf.save());
|
||||
return file;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Isar Schema للـ Offline Queue
|
||||
|
||||
```dart
|
||||
// shared/models/invoice_batch.dart
|
||||
@collection
|
||||
class InvoiceBatch {
|
||||
Id get isarId => fastHash(id);
|
||||
|
||||
late String id; // UUID
|
||||
late String companyId;
|
||||
late String pdfPath;
|
||||
late int imageCount;
|
||||
|
||||
@enumerated
|
||||
late BatchStatus status; // pending | uploading | done | failed
|
||||
|
||||
late DateTime createdAt;
|
||||
DateTime? uploadedAt;
|
||||
int retryCount = 0;
|
||||
String? errorMessage;
|
||||
}
|
||||
|
||||
@collection
|
||||
class LocalInvoice {
|
||||
Id get isarId => fastHash(id);
|
||||
|
||||
late String id;
|
||||
late String batchId;
|
||||
late String imagePath;
|
||||
late String companyId;
|
||||
|
||||
@enumerated
|
||||
late InvoiceStatus status; // pending | extracted | validated | submitted
|
||||
|
||||
// بيانات مستخرجة من AI
|
||||
String? invoiceNumber;
|
||||
double? totalAmount;
|
||||
double? taxAmount;
|
||||
String? supplierName;
|
||||
|
||||
late DateTime createdAt;
|
||||
bool isSynced = false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 3 — المساعد الصوتي (الأسبوع 7-10)
|
||||
|
||||
### 3.1 معمارية كاملة: Record → Groq → Gemini → Command
|
||||
|
||||
```dart
|
||||
// features/voice/controllers/voice_controller.dart
|
||||
class VoiceController extends GetxController {
|
||||
final _recorder = AudioRecorder();
|
||||
final isRecording = false.obs;
|
||||
final lastCommand = ''.obs;
|
||||
final commandResult = Rxn<VoiceCommandResult>();
|
||||
|
||||
static const maxDurationSeconds = 15;
|
||||
Timer? _autoStopTimer;
|
||||
|
||||
Future<void> startRecording() async {
|
||||
if (!await _recorder.hasPermission()) return;
|
||||
|
||||
final dir = await getTemporaryDirectory();
|
||||
final path = '${dir.path}/voice_${DateTime.now().millisecondsSinceEpoch}.m4a';
|
||||
|
||||
await _recorder.start(
|
||||
const RecordConfig(encoder: AudioEncoder.aacLc, sampleRate: 16000),
|
||||
path: path,
|
||||
);
|
||||
isRecording.value = true;
|
||||
|
||||
// إيقاف تلقائي بعد 15 ثانية
|
||||
_autoStopTimer = Timer(Duration(seconds: maxDurationSeconds), stopAndProcess);
|
||||
}
|
||||
|
||||
Future<void> stopAndProcess() async {
|
||||
_autoStopTimer?.cancel();
|
||||
if (!isRecording.value) return;
|
||||
|
||||
final path = await _recorder.stop();
|
||||
isRecording.value = false;
|
||||
|
||||
if (path == null) return;
|
||||
|
||||
final file = File(path);
|
||||
final result = await VoiceService.processCommand(file);
|
||||
commandResult.value = result;
|
||||
|
||||
await _executeCommand(result);
|
||||
}
|
||||
|
||||
Future<void> _executeCommand(VoiceCommandResult result) async {
|
||||
switch (result.action) {
|
||||
case VoiceAction.listInvoices:
|
||||
Get.toNamed(AppRoutes.invoices, arguments: result.params);
|
||||
break;
|
||||
case VoiceAction.checkQuota:
|
||||
Get.toNamed(AppRoutes.subscription);
|
||||
break;
|
||||
case VoiceAction.openScanner:
|
||||
Get.toNamed(AppRoutes.scanner, arguments: result.params);
|
||||
break;
|
||||
// ... باقي الأوامر
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Voice Service — Groq STT + Gemini Intent
|
||||
|
||||
```dart
|
||||
// features/voice/services/voice_service.dart
|
||||
class VoiceService {
|
||||
static Future<VoiceCommandResult> processCommand(File audioFile) async {
|
||||
// Step 1: STT via Groq Whisper
|
||||
final text = await _groqTranscribe(audioFile);
|
||||
if (text.isEmpty) return VoiceCommandResult.failed('لم أفهم الأمر');
|
||||
|
||||
// Step 2: Intent parsing via Gemini Flash Lite Latest
|
||||
final intent = await _parseIntent(text);
|
||||
return intent;
|
||||
}
|
||||
|
||||
static Future<String> _groqTranscribe(File audio) async {
|
||||
final dio = DioClient.instance; // بدون HMAC (Groq خارجي)
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(audio.path, filename: 'voice.m4a'),
|
||||
'model': 'whisper-large-v3-turbo',
|
||||
'language': 'ar',
|
||||
'response_format': 'text',
|
||||
});
|
||||
|
||||
final response = await Dio().post(
|
||||
'https://api.groq.com/openai/v1/audio/transcriptions',
|
||||
data: formData,
|
||||
options: Options(headers: {
|
||||
'Authorization': 'Bearer ${AppConfig.groqApiKey}',
|
||||
}),
|
||||
);
|
||||
|
||||
return response.data.toString().trim();
|
||||
}
|
||||
|
||||
static Future<VoiceCommandResult> _parseIntent(String text) async {
|
||||
const systemPrompt = '''
|
||||
أنت محلل أوامر لنظام مُصادَق للفوترة الأردني.
|
||||
استخرج النية والمعاملات من النص وأرجع JSON فقط.
|
||||
|
||||
الأوامر المتاحة:
|
||||
- list_invoices: { company?: string, from?: date, to?: date, status?: string }
|
||||
- check_quota: {}
|
||||
- open_scanner: { company?: string }
|
||||
- search_invoice: { amount?: number, company?: string, number?: string }
|
||||
- get_report: { type: "tax"|"monthly", period?: string }
|
||||
- check_status: { invoice_id?: string, company?: string }
|
||||
- export_pdf: { invoice_id?: string, company?: string }
|
||||
- navigate: { screen: string }
|
||||
|
||||
أرجع: { "action": "...", "params": {...}, "confirmation": "نص قصير تأكيد" }
|
||||
''';
|
||||
|
||||
final response = await Dio().post(
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite-latest:generateContent',
|
||||
queryParameters: {'key': AppConfig.geminiApiKey},
|
||||
data: {
|
||||
'contents': [{'parts': [{'text': text}]}],
|
||||
'systemInstruction': {'parts': [{'text': systemPrompt}]},
|
||||
'generationConfig': {
|
||||
'responseMimeType': 'application/json',
|
||||
'maxOutputTokens': 200,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
final json = jsonDecode(
|
||||
response.data['candidates'][0]['content']['parts'][0]['text']
|
||||
);
|
||||
|
||||
return VoiceCommandResult(
|
||||
action: VoiceAction.fromString(json['action']),
|
||||
params: json['params'] ?? {},
|
||||
confirmation: json['confirmation'] ?? '',
|
||||
rawText: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 الأوامر الصوتية الـ 10
|
||||
|
||||
```dart
|
||||
enum VoiceAction {
|
||||
listInvoices, // "فواتير شركة X لشهر كذا"
|
||||
checkQuota, // "كم باقي من رصيدي"
|
||||
openScanner, // "صور فاتورة لشركة X"
|
||||
searchInvoice, // "ابحث عن فاتورة 150 دينار"
|
||||
getReport, // "تقرير ضريبة الشهر الماضي"
|
||||
checkStatus, // "حالة فاتورة شركة X"
|
||||
exportPdf, // "حوّل فاتورة 300 لـ PDF"
|
||||
listRejected, // "الفواتير المرفوضة هذا الأسبوع"
|
||||
subscriptionInfo,// "حالة اشتراكي"
|
||||
navigate, // "افتح لوحة التحكم"
|
||||
unknown,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 4 — AI Pre-Audit الضريبي (الأسبوع 10-13)
|
||||
|
||||
### 4.1 تحديث Backend — نظام Queue للفواتير
|
||||
|
||||
**المطلوب إضافته في الـ schema:**
|
||||
|
||||
```sql
|
||||
-- جدول جديد: invoice_processing_queue
|
||||
CREATE TABLE invoice_processing_queue (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
batch_id VARCHAR(36) NOT NULL,
|
||||
invoice_id INT, -- بعد الاستخراج
|
||||
tenant_id INT NOT NULL,
|
||||
image_path VARCHAR(500) NOT NULL,
|
||||
status ENUM('pending','processing','done','failed') DEFAULT 'pending',
|
||||
attempts INT DEFAULT 0,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at TIMESTAMP NULL,
|
||||
INDEX idx_status_tenant (status, tenant_id),
|
||||
INDEX idx_batch (batch_id)
|
||||
);
|
||||
|
||||
-- جدول جديد: invoice_batches
|
||||
CREATE TABLE invoice_batches (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
tenant_id INT NOT NULL,
|
||||
company_id INT NOT NULL,
|
||||
pdf_path VARCHAR(500),
|
||||
total_images INT,
|
||||
processed_images INT DEFAULT 0,
|
||||
status ENUM('uploading','processing','done','partial_fail') DEFAULT 'uploading',
|
||||
created_by INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 تحديث InvoiceExtractionService.php
|
||||
|
||||
```php
|
||||
// services/InvoiceExtractionService.php
|
||||
class InvoiceExtractionService {
|
||||
// قواعد الضريبة الأردنية
|
||||
private array $taxRules = [
|
||||
// معفيات صفر
|
||||
'zero_rated' => ['أدوية', 'دواء', 'خبز', 'طحين', 'سكر', 'أرز'],
|
||||
// ضريبة خاصة
|
||||
'special' => ['سيارات' => 0.15, 'سجائر' => 0.32, 'كحول' => 0.30],
|
||||
// معدل عام
|
||||
'standard_rate' => 0.16, // 16% ضريبة مبيعات أردن
|
||||
];
|
||||
|
||||
public function extractAndAudit(string $imagePath, int $tenantId): array {
|
||||
// 1. استخراج البيانات من Gemini Vision
|
||||
$extracted = $this->extractWithVision($imagePath);
|
||||
|
||||
// 2. AI Pre-Audit
|
||||
$audit = $this->performPreAudit($extracted);
|
||||
|
||||
// 3. حساب Hash لمنع التكرار
|
||||
$hash = $this->calculateInvoiceHash($extracted);
|
||||
|
||||
// 4. التحقق من التكرار
|
||||
$duplicate = Invoice::where('invoice_hash', $hash)
|
||||
->where('tenant_id', $tenantId)->first();
|
||||
|
||||
if ($duplicate) {
|
||||
$audit['warnings'][] = [
|
||||
'code' => 'DUPLICATE_INVOICE',
|
||||
'message' => 'هذه الفاتورة مرفوعة مسبقاً (رقم ' . $duplicate->id . ')',
|
||||
'severity' => 'critical'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'extracted' => $extracted,
|
||||
'audit' => $audit,
|
||||
'hash' => $hash,
|
||||
'jofotara_readiness' => $this->assessJoFotaraReadiness($extracted, $audit),
|
||||
];
|
||||
}
|
||||
|
||||
private function performPreAudit(array $data): array {
|
||||
$warnings = [];
|
||||
$errors = [];
|
||||
|
||||
// فحص الضريبة
|
||||
if (isset($data['items'])) {
|
||||
foreach ($data['items'] as $item) {
|
||||
$expectedTax = $this->calculateExpectedTax($item['name'], $item['amount']);
|
||||
if (abs($item['tax'] - $expectedTax) > 0.01) {
|
||||
$warnings[] = [
|
||||
'code' => 'TAX_MISMATCH',
|
||||
'message' => "ضريبة غير صحيحة لـ '{$item['name']}': المتوقع {$expectedTax} الموجود {$item['tax']}",
|
||||
'severity' => 'high'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// فحص اكتمال البيانات الإلزامية
|
||||
$required = ['supplier_name', 'invoice_number', 'invoice_date', 'total_amount'];
|
||||
foreach ($required as $field) {
|
||||
if (empty($data[$field])) {
|
||||
$errors[] = ['code' => 'MISSING_FIELD', 'field' => $field, 'severity' => 'critical'];
|
||||
}
|
||||
}
|
||||
|
||||
return ['warnings' => $warnings, 'errors' => $errors];
|
||||
}
|
||||
|
||||
private function assessJoFotaraReadiness(array $data, array $audit): array {
|
||||
$errorCount = count($audit['errors']);
|
||||
$highWarnings = array_filter($audit['warnings'], fn($w) => $w['severity'] === 'high');
|
||||
|
||||
$score = 100 - ($errorCount * 30) - (count($highWarnings) * 15);
|
||||
return [
|
||||
'score' => max(0, $score),
|
||||
'ready' => $score >= 70,
|
||||
'message' => $score >= 70 ? 'جاهزة للإرسال' : 'تحتاج مراجعة قبل الإرسال'
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 5 — فحص الجهاز + Offline Architecture (الأسبوع 12-14)
|
||||
|
||||
### 5.1 Device Capability Check عند أول تشغيل
|
||||
|
||||
```dart
|
||||
// features/auth/views/device_check_screen.dart
|
||||
class DeviceCheckScreen extends GetView<DeviceCheckController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isChecking.value) {
|
||||
return _buildCheckingUI();
|
||||
}
|
||||
return _buildResultUI(controller.capability.value);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultUI(DeviceCapability cap) {
|
||||
return switch (cap) {
|
||||
DeviceCapability.full => _buildFullModeUI(),
|
||||
DeviceCapability.limited => _buildLimitedModeUI(),
|
||||
DeviceCapability.unsupported => _buildUnsupportedUI(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceCheckController extends GetxController {
|
||||
final isChecking = true.obs;
|
||||
final capability = DeviceCapability.full.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_runCheck();
|
||||
}
|
||||
|
||||
Future<void> _runCheck() async {
|
||||
final result = await DeviceService().checkCapability();
|
||||
|
||||
// حفظ النتيجة محلياً
|
||||
GetStorage().write(BoxName.deviceCapability, result.name);
|
||||
|
||||
capability.value = result;
|
||||
isChecking.value = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 WorkManager للـ Background Sync
|
||||
|
||||
```dart
|
||||
// main.dart
|
||||
void callbackDispatcher() {
|
||||
Workmanager().executeTask((task, inputData) async {
|
||||
switch (task) {
|
||||
case 'syncPendingBatches':
|
||||
await _syncPendingBatches();
|
||||
break;
|
||||
case 'retryFailedUploads':
|
||||
await _retryFailedUploads();
|
||||
break;
|
||||
}
|
||||
return Future.value(true);
|
||||
});
|
||||
}
|
||||
|
||||
// في main():
|
||||
Workmanager().initialize(callbackDispatcher);
|
||||
Workmanager().registerPeriodicTask(
|
||||
'syncTask',
|
||||
'syncPendingBatches',
|
||||
frequency: const Duration(minutes: 15),
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## الملخص التنفيذي بالأسابيع
|
||||
|
||||
| الأسبوع | التركيز | المخرجات |
|
||||
|---------|---------|----------|
|
||||
| 1-2 | هيكل المشروع + HMAC + Isar setup | بنية مجلدات نظيفة، DioClient محمي |
|
||||
| 2-3 | SMS OTP + Biometric Auth | تسجيل دخول آمن كامل |
|
||||
| 3-5 | Batch Scanner + camerawesome | تصوير متسلسل يعمل |
|
||||
| 5-7 | Image Processing (image + opencv) | صور مضغوطة ومحسّنة |
|
||||
| 7-8 | PDF Generator + Offline Queue | دفعات محفوظة محلياً |
|
||||
| 8-10 | Voice: Record → Groq → Gemini | 10 أوامر صوتية تعمل |
|
||||
| 10-12 | AI Pre-Audit في Backend | تحذيرات ضريبية قبل JoFotara |
|
||||
| 12-13 | Device Check + WorkManager Sync | مزامنة خلفية موثوقة |
|
||||
| 13-14 | Testing + Beta (10 مكاتب عمان) | إطلاق تجريبي |
|
||||
|
||||
---
|
||||
|
||||
## قرارات نهائية سريعة
|
||||
|
||||
| القرار | الاختيار | السبب |
|
||||
|--------|---------|-------|
|
||||
| STT | Groq Whisper | الأرخص 9x + الأسرع |
|
||||
| Intent LLM | Gemini 2.0 Flash Lite Latest | ما ينوقف، مجاني بحد معقول |
|
||||
| Local DB | Isar | Queries معقدة على الفواتير |
|
||||
| Camera | camerawesome | تحكم أفضل في Batch Mode |
|
||||
| Image Processing | image (Dart) + opencv_dart | مرونة: Dart للبسيط، OpenCV للمعقد |
|
||||
| PDF | pdf (pub.dev) | دعم RTL + عربية |
|
||||
| Recording | record | أخف من flutter_sound، كافٍ للمهمة |
|
||||
| Background Sync | WorkManager | الأكثر موثوقية لـ iOS + Android |
|
||||
| Security Storage | flutter_secure_storage | Keychain/Keystore native |
|
||||
Reference in New Issue
Block a user