تطبيق أندرويد يعمل بدون إنترنت بالكامل لإدارة منتسبي نقابة تحالف
أساتذة موريتانيا، مبني بـ Flutter + Dart + SQLite، كبديل لنظام
tam_members_registry_v1 (PHP/MySQL) الأصلي.
تحديث أخير: أُضيف خط عربي حقيقي (Amiri Regular/Bold، مرخَّص SIL OFL) مضمَّنًا فعليًا في
assets/fonts/لتصدير PDF، بالإضافة إلى مجموعة اختبارات تكامل حقيقية (test/integration/) تشغّل قاعدة بيانات SQLite فعلية (عبرsqflite_common_ffi) لا مجرد دوال معزولة، وتغطي: المنتسبين والمؤسسات والمقاطعات، الاشتراكات وترحيل رصيد الصندوق بين السنوات، تسجيل الدخول والقفل بعد محاولات فاشلة، النسخ الاحتياطي/الاستعادة (بما في ذلك اكتشاف وإصلاح علّة حقيقية أثناء كتابة الاختبار — انظر القسم أدناه)، واستيراد CSV كاملًا من التحليل حتى الاعتماد النهائي.
هذا المستودع يحتوي على lib/ وpubspec.yaml وtest/ فقط (بيئة
التطوير الحالية لا تملك Flutter SDK لتوليد مجلدات android/ وios/
الأصلية). لتشغيله:
# أنشئ هيكل مشروع Flutter فارغ يحتوي على android/ الرسمية
flutter create --org com.tam.mobile --platforms=android tam_mobile_shell
# انسخ محتوى هذا المستودع فوقه (استبدل lib/ و pubspec.yaml و test/)
cp -r lib pubspec.yaml test tam_mobile_shell/
cd tam_mobile_shell
flutter pub get
flutter runflutter build apk --release
# الناتج: build/app/outputs/flutter-apk/app-release.apk
flutter build appbundle --release
# الناتج: build/app/outputs/bundle/release/app-release.aabقاعدة البيانات SQLite تُنشأ تلقائيًا عند أول تشغيل داخل مجلد بيانات
التطبيق الخاص (getApplicationDocumentsDirectory())، باسم tam.db.
لا صلاحية إنترنت مطلوبة، ويمكن حذف صلاحية INTERNET من
android/app/src/main/AndroidManifest.xml بأمان.
مبنيان فعليًا (lib/services/backup_service.dart +
lib/screens/settings/backup_screen.dart، من تبويب "المزيد"):
- نسخة احتياطية: ينسخ ملف
tam.dbالحالي إلى ملف باسمTAM_backup_YYYY-MM-DD.db، ثم يفتح قائمة المشاركة النظامية (واتساب/تيليجرام/بلوتوث/أي تطبيق آخر) — بدون إنترنت. - الاستعادة: يختار المستخدم ملفًا عبر منتقي الملفات، يتحقق التطبيق من توقيع SQLite (Magic Bytes) قبل أي شيء، يعرض حجم الملف، يطلب تأكيدًا صريحًا، ينشئ نسخة أمان تلقائية من البيانات الحالية أولًا، ثم يستبدل الملف ويعيد فتح الاتصال.
مبني فعليًا (lib/services/subscription_importer.dart +
lib/screens/subscriptions/import_screen.dart، من "المزيد ← استيراد
بيانات الاشتراكات"). يدعم نفس أعمدة ملف المنسقية الأصلي (السنة،
الدفعة، الاسم، المبلغ، تفاصيل الاشتراك، دفع مباشر للتنفيذي...)،
ويطابق تلقائيًا حسب الاسم/الدليل المالي، ويعرض معاينة قابلة للربط
اليدوي قبل الاعتماد النهائي، ويمنع تكرار الاستيراد عبر بصمة SHA-256
لكل سجل (مطابقةً لـ row_hash الأصلي).
- تصدير Excel الحقيقي (
.xlsxعبر حزمةexcelالموجودة فيpubspec.yaml): التقارير الحالية تُصدَّر CSV (يفتح مباشرة في إكسل) بدل.xlsxفعلي — أسرع للتنفيذ وكافٍ عمليًا، لكن أخبرني إن كنت تريد ملف.xlsxحرفيًا. السجلات التاريخية غير المرتبطة بمنتسب حالي (نوع "historical" في الاستيراد) تُحتسب في الملخص لكن لا تُدرَج كدفعة فعلية في قاعدة البيانات (لأنmember_idمفتاح أجنبي إلزامي) — تبقى بيانات الملف الأصلي قابلة لإعادة الاستيراد بعد إنشاء المنتسب أو تصحيح اسمه.
أي تعديل مستقبلي في البنية يُضاف كشرط جديد داخل _onUpgrade في
lib/core/database/app_database.dart مع رفع schemaVersion — لا
يُعدَّل _onCreate مباشرة حتى لا تُفقد بيانات المستخدمين الحاليين.
اسم المستخدم: admin
كلمة المرور الافتراضية: admin123
يُجبر التطبيق على تغييرها عند أول تسجيل دخول (must_change_password=1
في جدول users). بعد 5 محاولات فاشلة متتالية يُقفل الحساب مؤقتًا
لمدة 5 دقائق.
- الصندوق: أُعيد بناؤه كمفهوم سنوي مرحّل (رصيد افتتاحي لكل سنة =
الرصيد الختامي للسنة السابقة)، خلافًا لمنطق الكود الأصلي الذي كان
حسابًا تراكميًا واحدًا بلا فصل سنوي. انظر
lib/services/fund_service.dart. - الصلاحيات: أُبقيت بسيطة (مستخدم واحد بلا أدوار)، مطابقة للأصل.
lib/
core/database/ # فتح قاعدة البيانات + الترحيلات (migrations)
core/theme/ # Material 3 + RTL
models/ # كائنات البيانات (Member, SubscriptionPayment...)
repositories/ # كل SQL هنا فقط — لا SQL داخل الشاشات
services/ # منطق الأعمال الصِّرف (حساب الديون، الصندوق)
screens/ # الواجهات (الرئيسية، المنتسبون، الاشتراكات، الصندوق)
test/
subscription_calculator_test.dart # اختبارات الحسابات المالية
هذه اختبارات حقيقية — لا تُحاكي قاعدة البيانات، بل تشغّل SQLite
فعليًا عبر حزمة sqflite_common_ffi (تنفيذ أصلي بلغة C لنفس محرك
SQLite) في ملف مؤقت جديد لكل اختبار، عبر نفس طبقة
AppDatabase/Repositories/Services المستخدمة في التطبيق دون أي
تعديل. الشيء الوحيد المُحاكى هو قناة منصّة path_provider (لأنها
تحتاج جهازًا حقيقيًا)، ويُستبدل فقط بمسار المجلد المؤقت للاختبار نفسه.
الملفات:
member_repository_integration_test.dart— المقاطعات/المؤسسات/ المنتسبين، البحث المفهرَس، الأرشفة، وسلامة القيود (FK RESTRICT).subscription_and_fund_integration_test.dart— تراكم المدفوعات، حساب مداخيل الصندوق من حصة الجهوي فقط (باستثناء الدفعات المباشرة للتنفيذي)، وأهم نقطة: ترحيل الرصيد الختامي كرصيد افتتاحي للسنة التالية بالضبط (وهي بالضبط النقطة التي طلبتَ اختبارها بعناية في المواصفات الأصلية).auth_service_integration_test.dart— دخول ناجح، كلمة مرور خاطئة، تغيير كلمة المرور، والقفل بعد 5 محاولات فاشلة.backup_service_integration_test.dart— نسخة احتياطية حقيقية، رفض ملف غير صالح، واستعادة تُعيد فعليًا الحالة القديمة للبيانات.subscription_importer_integration_test.dart— مطابقة تلقائية، فصل رسم البطاقة، منع التكرار عند استيراد نفس الملف مرتين، والربط اليدوي لسجل غير مطابق.
اختبار الاستعادة كشف أن restoreBackup كانت تستدعي createBackup
نفسها لأخذ نسخة أمان قبل الاستبدال — وبما أن اسم ملف النسخة كان
مبنيًا على التاريخ فقط (TAM_backup_2026-09-12.db)، فإن نسخة
الأمان الداخلية كانت أحيانًا تحمل نفس اسم النسخة التي يجري
استعادتها منها، فتستبدلها بمحتوى قاعدة البيانات الحالية قبل قراءتها
فعليًا — أي أن الاستعادة كانت يمكن أن تُعيد نفس البيانات الحالية
بدل النسخة القديمة المطلوبة، في حال إنشاء نسختين في نفس اليوم. تم
إصلاحها بجعل اسم نسخة الأمان الداخلية مبنيًا على طابع زمني دقيق
(بالثانية) + رقم فريد، وبادئة مختلفة تمامًا (TAM_prerestore_) لا
يمكن أبدًا أن تتطابق مع اسم أي نسخة يدوية.
flutter testهذا الأمر يشغّل تلقائيًا كل الاختبارات (الوحدة + التكامل) لأنها كلها
تحت مجلد test/ — لا حاجة لأمر منفصل لاختبارات التكامل.
- الديون تبدأ من يناير 2026 بدل تاريخ أول دفعة.
- الدفعة الواحدة يمكن أن تغطي شهرًا أو عدة أشهر أو السنة كاملة.
- توزيع تلقائي من أقدم استحقاق غير مسدد، مع دعم الدفعة الجزئية.
payment_allocationsيربط كل مبلغ مدفوع بالأشهر المغطاة.subscription_ratesيحفظ قيمة الاشتراك حسب السنة والشهر بدل تغيير التاريخ بأثر رجعي.- لا يوجد أي جدول أو خدمة لإرسال رسائل واتساب في هذا التطبيق.