30 KiB
30 KiB
خطة تنفيذ مُصادَق — Flutter Mobile App
خارطة الطريق الشاملة بالأولويات والمكتبات والتفاصيل
أولاً: قرارات المكتبات النهائية
هيكل pubspec.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)
// 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)]
// 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);
}
}
}
// 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:
// 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 المطلوب إضافته:
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)
// 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 كامل
// 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 من الصور
// 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
// 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
// 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
// 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
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:
-- جدول جديد: 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
// 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 عند أول تشغيل
// 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
// 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 (pub.dev) | دعم RTL + عربية | |
| Recording | record | أخف من flutter_sound، كافٍ للمهمة |
| Background Sync | WorkManager | الأكثر موثوقية لـ iOS + Android |
| Security Storage | flutter_secure_storage | Keychain/Keystore native |