# دراسة بناء نظام الاعتماد > مواصفة بناء لنظام اعتماد مواسم سباقات الخيل. > **المنصة:** Laravel 12 · MariaDB · استضافة cPanel > **الحالة:** مسودة للمراجعة · **التاريخ:** 2026-09-26 --- ## الفهرس | # | القسم | |---|---| | ١ | [الغرض والنطاق](#١-الغرض-والنطاق) | | ٢ | [المعجم: أسماء الأشياء](#٢-المعجم-أسماء-الأشياء) | | ٣ | [المبادئ الحاكمة](#٣-المبادئ-الحاكمة) | | ٤ | [البنية التقنية](#٤-البنية-التقنية) | | ٥ | [نموذج البيانات](#٥-نموذج-البيانات) | | ٦ | [الحالات وسير العمل](#٦-الحالات-وسير-العمل) | | ٧ | [الصلاحيات والنطاقات](#٧-الصلاحيات-والنطاقات) | | ٨ | [التسجيل والمراجعة](#٨-التسجيل-والمراجعة) | | ٩ | [المناطق والأيام والصلاحية الفعلية](#٩-المناطق-والأيام-والصلاحية-الفعلية) | | ١٠ | [البطاقات: التصميم والإصدار والتصيير](#١٠-البطاقات-التصميم-والإصدار-والتصيير) | | ١١ | [الطباعة والتسليم](#١١-الطباعة-والتسليم) | | ١٢ | [البوابات والدخول](#١٢-البوابات-والدخول) | | ١٣ | [طلبات الدخول](#١٣-طلبات-الدخول) | | ١٤ | [البريد والإشعارات](#١٤-البريد-والإشعارات) | | ١٥ | [التكامل وواجهات API](#١٥-التكامل-وواجهات-api) | | ١٦ | [التقارير](#١٦-التقارير) | | ١٧ | [الأمن والخصوصية](#١٧-الأمن-والخصوصية) | | ١٨ | [سجل التدقيق](#١٨-سجل-التدقيق) | | ١٩ | [الواجهة والتعريب](#١٩-الواجهة-والتعريب) | | ٢٠ | [خطة التنفيذ](#٢٠-خطة-التنفيذ) | | ٢١ | [الأسئلة المفتوحة](#٢١-الأسئلة-المفتوحة) | --- ## ١. الغرض والنطاق ### ما يفعله النظام يدير اعتماد كل من يدخل حدث سباق الخيل: يستقبل طلبات الاعتماد، ويمرّرها على سلسلة موافقات، وينتج بطاقة تحمل صلاحيات دخول محددة بالمنطقة واليوم، ويتحقق منها عند البوابة. ### الحجم المستهدف | البند | المستهدف | |---|---| | المشاركون في الموسم | حتى 30,000 | | الجهات (الفئات الفرعية) | حتى 400 | | المناطق | حتى 60 | | البوابات المتزامنة | حتى 40 جهازاً | | ذروة المسح | 30 مسحة/ثانية لمدة ساعتين | | البطاقات المطبوعة يومياً في الذروة | 5,000 | ### داخل النطاق التسجيل · المراجعة والاعتماد · إدارة الفئات والجهات والمنسقين · تصميم البطاقات وإصدارها وطباعتها · التسليم · المناطق وصلاحيات الدخول وطلباتها · قرار البوابة · البريد والدعوات · التقارير · التكامل مع أنظمة خارجية · واجهة API لتطبيق البوابات. ### خارج النطاق تطبيق البوابات نفسه (نحدد واجهته ومتطلباته، ويُنفَّذ كمشروع مستقل) · أنظمة التذاكر والمبيعات · إدارة الفعاليات والجدولة · المحاسبة. --- ## ٢. المعجم: أسماء الأشياء هذه الأسماء ملزمة في الكود وقاعدة البيانات والواجهة. أي اسم خارجها ممنوع. ### الكيانات الأساسية | بالعربية | في الكود | ما هو | |---|---|---| | الموسم | `Season` | دورة واحدة من مواسم السباقات. الوعاء الذي يعزل كل بيانات الدورة عن غيرها | | يوم الحدث | `EventDay` | يوم مؤرَّخ داخل الموسم، له تاريخ ومسمّى وترتيب ونافذة تشغيل | | الشخص | `Person` | الهوية البشرية الثابتة عبر المواسم. مرساة منع التكرار | | الجهة | `Organization` | كيان خارجي (قناة، شركة، إسطبل) يعمّر عبر المواسم | | الفئة | `Category` | تصنيف عام: الإعلام، الملاك والمدربون، الموظفون | | الفئة الفرعية | `Subcategory` | الجهة المحددة داخل الفئة: قناة، شركة، إسطبل. **وحدة الضبط الأهم في النظام**: عندها رابط التسجيل والسعة وحقول النموذج والمناطق ونوع البطاقة وسير الموافقة ومندوب الاستلام | | الطلب | `Application` | طلب اعتماد قدّمه شخص لفئة فرعية في موسم | | الاعتماد | `Accreditation` | الحق الممنوح بعد القبول. صف مستقل لا قيمة في عمود | | إصدار البطاقة | `BadgeIssue` | نسخة بطاقة محددة لها رمزها ودورة حياتها. إعادة الإصدار تنتج صفاً جديداً | | واقعة الطباعة | `PrintEvent` | سطر لكل عملية طباعة فعلية | | التسليم | `Handover` | واقعة تسليم بطاقة لمستلم | | المنطقة | `Zone` | مكان في المضمار له اسم ولون ورقم ونوع | | المنح | `AccessGrant` | صلاحية دخول منطقة في يوم، من مصدر محدد | | الصلاحية الفعلية | `EffectiveAccess` | جدول محسوب يجمع كل المنوحات في صفوف جاهزة لقرار البوابة | | طلب الدخول | `AccessRequest` | طلب منح منطقة لم تكن مسموحة | | المسح | `ScanRecord` | واقعة مسح بطاقة عند بوابة | | التصميم | `BadgeDesign` | تخطيط بطاقة بعناصره | | نسخة التصميم | `BadgeDesignVersion` | لقطة مجمَّدة من التصميم تُربط بها البطاقات المُصدَرة | | مكان الطباعة | `PrintStation` | مكتب تُطبع فيه البطاقات | | مكان الاستلام | `PickupLocation` | مكان يستلم منه المشارك بطاقته | ### تمييزات لازمة - **مكان الطباعة ≠ مكان الاستلام.** الأول مكتب الموظف، والثاني وجهة المشارك. - **التصميم ≠ نوع البطاقة.** النوع تصنيف (عادية، ضيافة، كبار الشخصيات)، والتصميم تخطيط بصري يخدم نوعاً. - **الطلب ≠ الاعتماد ≠ البطاقة.** ثلاثة كيانات بدورات حياة مستقلة. - **المشارك ≠ المستخدم.** المشارك يُعتمد ويدخل الحدث، والمستخدم يسجّل الدخول ويشغّل النظام. قد يكون الشخص الواحد الاثنين، وهما صفّان مختلفان. - **إعادة الطباعة ≠ إعادة الإصدار.** الأولى طبع نفس البطاقة مرة أخرى (نفس الرمز)، والثانية إنتاج بطاقة جديدة تُبطل سابقتها. ### ممنوع في الكود - أرقام الحالات والمستويات. كل قيمة رمز نصي مفهوم. - اسم أي جهة بعينها في enum أو صنف أو مسار أو دور. الجهات بيانات لا كود. - اسم صفحة أو مسار داخل اسم صلاحية. - تثبيت «الجمعة» و«السبت» أو أي يوم بعينه في مخطط أو منطق. --- ## ٣. المبادئ الحاكمة هذه ثمانية مبادئ تحكم كل قرار في النظام. حين يتعارض حل مع أحدها، يسقط الحل. ### ١. الموسم جذر لا سمة كل جدول تشغيلي يحمل `season_id` غير قابل للإفراغ، وكل فهرس تشغيلي يبدأ به. أربعة كيانات فقط تعمّر عبر المواسم: `persons` و`organizations` و`users` والقوائم المرجعية العامة. وما عداها صف جديد لكل موسم. الكيان الذي يتغيّر بين موسمين **لا يُشارَك بل يُستنسخ**: المنطقة والتصميم والفئة والفئة الفرعية وقالب البريد كلها صفوف مستقلة لكل موسم، يُنشئها أمر `season:clone` مع عمود `cloned_from_id` كسلسلة نسب. فسؤال «ماذا يحدث لبطاقات الموسم الماضي إن غيّرنا لون منطقة؟» له جواب واحد: لا شيء، هي صف آخر في موسم آخر. ### ٢. الحالة محاور متعامدة لا حقل واحد كل محور معنى يملك عموده الخاص، ولا يجمع أي enum محورين: | المحور | العمود | القيم | |---|---|---| | المراجعة | `applications.review_status` | `draft` `submitted` `pending_coordinator` `pending_admin` `photo_rejected` `rejected` `approved` `withdrawn` | | الصورة | `portrait_photos.state` | `missing` `submitted` `approved` `rejected` | | الحق الممنوح | `accreditations.state` | `active` `suspended` `revoked` | | نسخة البطاقة | `badge_issues.state` | `pending_print` `printed` `handed_over` `lost` `void` `superseded` | | المصدر | `applications.source` | `public_link` `back_office` `api_create` `api_import` — **حقل مصدر لا حالة** | فتُمثَّل حالة «معتمد، مطبوعة بطاقته، صورته الجديدة مرفوضة، وارد من تكامل» كاملةً في آن واحد. ### ٣. الفشل مغلق كل نقص بيانات وكل خطأ في مسار قرار أمني يُترجم إلى **منع** مع رمز سبب وإنذار تشغيلي، لا إلى تخطّي الفحص. لا يوجد في النظام فرع «لا يُفحص». ### ٤. لا مسار كود ثانٍ الإجراء الجماعي حلقة حرفية فوق الإجراء الفردي. أي إجراء يُنفَّذ من شاشتين يمرّ على نفس الدالة بنفس الحرّاس. واختبار تكاملي يثبت تطابق النتيجة لكل تركيبة (دور × حالة × سياسة). ### ٥. كل إنتاج لبطاقة واقعة مسجَّلة أي مسار يُنتج ملف بطاقة — فردي، جماعي، معاينة، رابط مباشر — يترك أثراً يذكر من ولماذا ومتى وأين. ولا يوجد مسار إنتاج غير مسجَّل. ### ٦. الصلاحية والنطاق محوران منفصلان «ماذا تستطيع» (`resource.action`) منفصل تماماً عن «على مَن تستطيع» (`scope_assignments`). ولا يجوز لأي صلاحية أن توسّع النطاق ضمنياً. ### ٧. ما لا يُطبع لا يُعرض كل خيار في محرر التصميم له أثر في المخرج المطبوع. وقائمة الخطوط المعروضة هي حرفياً الخطوط المضمّنة في المستودع. ### ٨. القيد لا يُسقط متطلباً استضافة cPanel تحدّ الوسائل لا الأهداف. كل متطلب أمني أو تشغيلي يبقى محققاً بوسائل تعمل على المنصة، ويُذكر صراحةً ما نخسره وكيف نعوّضه. --- ## ٤. البنية التقنية ### الحزمة | الطبقة | الاختيار | السبب | |---|---|---| | الإطار | Laravel 12 (PHP 8.3+) | معتمد من العميل، ونظامه البيئي يغطي كل احتياجاتنا | | قاعدة البيانات | MariaDB 10.11+ / InnoDB | المتاح على cPanel. يدعم المفاتيح المركّبة والفهارس المغطية والمعاملات | | الواجهة | Blade + Livewire 3 | لوحة إدارة كثيفة بلا واجهة أمامية منفصلة. يقلل سطح الصيانة على استضافة مشتركة | | الجزر التفاعلية | Alpine.js، وVue لمحرر التصميم وحده | المحرر يحتاج حالة معقّدة؛ بقية الشاشات لا تحتاج | | الصلاحيات | spatie/laravel-permission | اصطلاحي ومعروف | | التفويض | Laravel Policies | نقطة إنفاذ واحدة لكل نموذج | | API | Laravel Sanctum | رموز للأجهزة والشركاء | | توليد PDF | **mPDF** | الخيار الوحيد في PHP الخالص الذي يشكّل العربية تشكيلاً صحيحاً ويضمّن الخطوط ويخرج نصاً متجهاً بمقاس صفحة دقيق | | رمز QR | bacon/bacon-qr-code (إخراج SVG) | متجه لا نقطي، فلا معنى للدقة عليه | | معالجة الصور | Intervention Image | قص وتحجيم ومصغّرات | | Excel | maatwebsite/excel مع التصدير المجزّأ | التصدير الكبير يُقسَّم شرائح | | البريد | SMTP لمزوّد رسائل تعاملية | حصص Gmail لا تكفي حدثاً بعشرات الآلاف | | الطوابير | `database` driver | لا Redis مضمون على cPanel | | الذاكرة المؤقتة | `file`، ويرتقي إلى Redis إن توفر | لا نفترض وجوده | | البحث | فهارس MariaDB وFULLTEXT | لا محرك بحث خارجي | ### بنية الوحدات أحادية معيارية: تطبيق واحد مقسّم وحدات داخلية بحدود واضحة، لا خدمات مصغّرة. النشر ملف واحد على cPanel، والحدود تُحفظ بالانضباط واختبارات معمارية. ``` app/Modules/ ├── Seasons/ الموسم، أيام الحدث، الاستنساخ ├── Directory/ الأشخاص، الجهات، الفئات، الفئات الفرعية ├── Applications/ التسجيل، المراجعة، الموافقات، الصور ├── Accreditations/ الحق الممنوح، النقل، التعليق والإلغاء ├── Access/ المناطق، المنوحات، الصلاحية الفعلية، الطلبات ├── Badges/ التصاميم، المحرر، الإصدار، التصيير ├── Production/ طوابير الطباعة، وقائع الطباعة، التسليم ├── Gates/ الأجهزة، المسح، حزم العمل دون اتصال ├── Comms/ قوالب البريد، الدعوات، الإشعارات ├── Integration/ واجهات API، الاستيراد، الشركاء ├── Reporting/ التقارير والتصدير └── Platform/ التدقيق، الصلاحيات، النطاقات، الإعدادات ``` ### حدود المنصة وما نفعله حيالها | الحد | الأثر | المعالجة | |---|---|---| | زمن تنفيذ PHP محدود (60–120 ثانية) | لا عملية طويلة | كل عمل ثقيل يُقسَّم شرائح مستأنَفة تحفظ موضعها | | لا عامل طوابير دائم | لا معالجة فورية مستمرة | `schedule:run` كل دقيقة من cron، يشغّل `queue:work --max-time=55 --stop-when-empty` | | ذاكرة PHP محدودة | لا تحميل دفعة كبيرة في الذاكرة | `chunkById` في كل معالجة جماعية، وبث التصدير سطراً سطراً | | لا Chromium | لا تصيير بمحرك متصفح | mPDF، مع بوابة جودة تعوّض فقدان التطابق البنيوي (القسم ١٠) | | لا Redis مضمون | لا قفل موزّع ولا عدّاد ذري سريع | أقفال قاعدة البيانات (`SELECT ... FOR UPDATE`) وجدول `job_locks` | | تخزين محلي | لا تخزين كائنات | ملفات خارج جذر الويب حتماً، تُقدَّم عبر مسار مُصرَّح به | > **متى نحتاج خادماً خاصاً:** إذا تجاوزت ذروة المسح 30/ثانية، أو تجاوز التصيير 5,000 بطاقة يومياً، أو احتجنا العمل دون اتصال بمزامنة لحظية. حتى ذلك الحد، cPanel كافٍ. --- ## ٥. نموذج البيانات ### الخريطة العامة ``` Season ──┬── EventDay ├── Category ── Subcategory ──┬── Application ── Accreditation ── BadgeIssue │ │ │ ├── PrintEvent │ │ │ └── Handover │ └── SubcategoryZoneAccess └── EffectiveAccess ├── Zone ── ZoneDaySchedule ├── BadgeType ── BadgeDesign ── BadgeDesignVersion ── DesignElement ├── PrintStation · PickupLocation · EmailTemplate └── AccessRequest · ScanRecord · GateDevice Person ──── (عابر المواسم) ──── Application Organization ── (عابر المواسم) ── Subcategory User ──── ScopeAssignment · Role · Permission ``` ### الجداول الأساسية #### الموسم والأيام **`seasons`** — `id`, `name_ar`, `name_en`, `code`, `state` (`draft|active|closed|archived`), `starts_on`, `ends_on`, `timezone`, `cloned_from_id` **`event_days`** — `id`, `season_id`, `label_ar`, `label_en`, `date`, `sort_order`, `opens_at`, `closes_at`, `state` > عدد الأيام غير محدود. لا يوجد في النظام `day1` ولا `day2` ولا أي إشارة ليوم أسبوع بعينه. #### الأشخاص والجهات **`persons`** — `id`, `national_id_hash`, `passport_hash`, `email_normalized`, `phone_e164`, `first_name_ar/en`, `last_name_ar/en`, `gender`, `nationality_id`, `birth_date`, `merged_into_id` عابر المواسم. مرساة منع التكرار وتاريخ الشخص عبر المواسم. **`organizations`** — `id`, `name_ar`, `name_en`, `type`, `external_ref`, `state` عابر المواسم. #### الفئات والفئات الفرعية **`categories`** — `id`, `season_id`, `name_ar/en`, `color_hex`, `sort_order`, `is_listed`, `state` **`subcategories`** — الجدول الأهم في النظام: | العمود | الغرض | |---|---| | `id`, `season_id`, `category_id`, `organization_id` | الانتماء | | `name_ar`, `name_en`, `code` | التعريف. `code` يدخل في رابط التسجيل | | `registration_token` | رمز عشوائي طويل. رابط التسجيل يبنى عليه لا على `id` | | `approval_flow` | `single_stage` \| `two_stage` — **المصدر الوحيد** لعدد مراحل الموافقة | | `registration_cap` | حد التسجيل. عند بلوغه **يُغلق نموذج التسجيل** ويعرض للزائر أن الجهة اكتملت | | `accreditation_cap` | حد القبول. يُحسب بكل الاعتمادات القائمة أياً كانت حالة بطاقاتها | | `badge_type_id` | نوع البطاقة | | `pickup_location_id` | مكان الاستلام الافتراضي | | `delegate_person_id` | مندوب الجهة الذي يستلم البطاقات دفعة | | `companions_allowed`, `companions_cap` | هل يُسمح بمرافقين، وكم لكل مشارك | | `external_ref` | رقم الربط مع الجهات الخارجية | | `state` | `active` \| `closed` \| `disabled` | **`subcategory_form_fields`** — `subcategory_id`, `field_key`, `is_enabled`, `is_required`, `sort_order`, `label_override_ar`, `label_override_en`, `help_text_ar`, `help_text_en`, `validation_json`, `options_json` **نموذج التسجيل ديناميكي بالكامل لكل فئة فرعية.** لا يوجد نموذج ثابت في الكود. الإدارة تفتح إعدادات الفئة الفرعية وتحدد لكل حقل: هل يظهر، وهل هو إلزامي، وترتيبه، ومسمّاه إن أرادت تسمية خاصة، ونص إرشادي تحته، وقواعد تحققه. فجهة إعلامية تُظهر «المجال الوظيفي» و«اسم الوسيلة»، وإسطبل يُظهر «اسم الخيل» و«المدرب»، وشركة مقاولات تُظهر «رقم الهوية» و«شهادة السلامة» — من الشاشة نفسها بلا برمجة. **`subcategory_zone_access`** — `subcategory_id`, `zone_id`, `event_day_id`, `granted_by`, `granted_at`, `source` المناطق المسموحة لمنسوبي الملف، **منفصلة لكل يوم**. #### الطلبات والاعتمادات **`applications`** — `id`, `season_id`, `subcategory_id`, `person_id`, `review_status`, `source`, `submitted_at`, `stage1_by`, `stage1_at`, `final_by`, `final_at`, `rejection_reason_code`, `rejection_note`, `edit_token` مع بيانات النموذج في `application_field_values`. **`portrait_photos`** — `id`, `application_id`, `state`, `original_path`, `processed_path`, `thumb_path`, `crop_json`, `rejection_reason_code`, `reviewed_by`, `reviewed_at` دورة حياة مستقلة تماماً عن الطلب. **`accreditations`** — `id`, `season_id`, `application_id`, `person_id`, `subcategory_id`, `state` (`active|suspended|revoked`), `issued_at`, `revoked_reason_code`, `is_companion`, `companion_of_id` > **المرافق اعتماد كامل.** له صف `persons` باسمه، وصف `applications` ببياناته وصورته، وصف `accreditations` مستقل، و**بطاقة مستقلة برمزها الخاص**. يرتبط بمرافَقه عبر `companion_of_id` للمتابعة والتقارير فقط — لا يشاركه بطاقة ولا رمزاً. > > **ويُحسب في السعة:** كل مرافق يشغل مقعداً في `registration_cap` و`accreditation_cap` كأي مشارك، لأنه شخص يدخل الحدث فعلاً. وعدد مرافقي المشارك الواحد محدود بـ`companions_cap`. > > **وصلاحياته مستقلة:** المرافق يرث مناطق الفئة الفرعية وأيامها كأي منسوب، ولا يرث صلاحيات مرافَقه الخاصة. وإلغاء اعتماد المرافَق يعلّق اعتمادات مرافقيه تلقائياً مع إشعار. **`accreditation_event_day`** — `accreditation_id`, `event_day_id` الأيام المسموحة لصاحب الاعتماد. **قيد:** لا يُصدَر اعتماد بلا صف واحد على الأقل هنا. > **قيد التفرّد:** فهرس فريد على `(season_id, subcategory_id, person_id)` حيث `state != 'revoked'`. فالشخص الواحد له اعتماد واحد في الفئة الفرعية الواحدة. ويمكنه اعتماد آخر في فئة فرعية أخرى إن سمح الإعداد. #### المناطق والصلاحيات **`zones`** — `id`, `season_id`, `name_ar/en`, `display_number`, `color_hex`, `type` (`guest|quarantine|preparation|dining`), `requires_admin_approval`, `requires_zone_manager_approval`, `connectivity_requirement` (`online_only|offline_ok`), `state` **`zone_day_schedule`** — `zone_id`, `event_day_id`, `opens_at`, `closes_at`, `is_open` **`zone_badge_slots`** — `zone_id`, `badge_type_id`, `slot_index` **خانة المنطقة على البطاقة صريحة ومخزَّنة.** تُسنَد مرة وتُقفل طوال الموسم. إضافة منطقة أو إيقافها لا يزحزح خانة غيرها. **`access_grants`** — `id`, `season_id`, `accreditation_id`, `zone_id`, `event_day_id`, `source` (`subcategory|direct|request|category`), `source_ref_id`, `granted_by`, `granted_at`, `revoked_at` **`effective_access`** — `season_id`, `accreditation_id`, `zone_id`, `event_day_id`, `source_grant_id`, `computed_at` جدول محسوب. فهرس فريد مغطٍّ على `(accreditation_id, zone_id, event_day_id)`. يُعاد بناؤه مجزّأً عند أي تغيير منح. الحجم المتوقع ~1.2 مليون صف للموسم. #### البطاقات **`badge_types`** — `id`, `season_id`, `code`, `name_ar/en`, `slot_grid_rows`, `slot_grid_cols` **`badge_designs`** — `id`, `season_id`, `badge_type_id`, `name`, `width_mm`, `height_mm`, `bleed_mm`, `safe_margin_mm`, `background_path`, `state` (`draft|published|retired`) **`badge_design_versions`** — `id`, `design_id`, `version_number`, `snapshot_json`, `published_by`, `published_at`, `checksum` **لقطة مجمَّدة.** البطاقة المُصدَرة تشير إلى نسخة لا إلى تصميم حيّ، فتعديل التصميم لا يغيّر بطاقة مطبوعة. **`design_elements`** — `design_id`, `element_type`, `x_mm`, `y_mm`, `width_mm`, `height_mm`, `z_index`, `font_key`, `font_size_pt`, `font_weight`, `color_hex`, `align`, `corner_radius_mm`, `is_visible`, `options_json` أنواع العناصر: `qr` · `photo` · `full_name` · `subcategory_name` · `job_title` · `organization` · `zone_grid` · `event_days` · `identifier` · `static_text` · `static_image` **`badge_issues`** — `id`, `season_id`, `accreditation_id`, `badge_type_id`, `design_version_id`, `issue_number`, `scan_token_hash`, `state`, `access_snapshot_id`, `photo_asset_id`, `replaces_id`, `replaced_by_id`, `issued_at`, `voided_at`, `void_reason_code` > **قيد:** فهرس فريد جزئي — بطاقة نشطة واحدة لكل `(accreditation_id, badge_type_id)`. **`badge_access_snapshots`** — `id`, `badge_issue_id`, `rows_json` لقطة الصلاحيات لحظة الإصدار: `(zone_id, event_day_id, slot_index, color_hex, display_number)`. فما يُطبع على البطاقة موثَّق ولو تغيّرت البيانات لاحقاً. #### الإنتاج **`print_events`** — `id`, `badge_issue_id`, `print_station_id`, `printed_by`, `printed_at`, `reason_code` (`initial|reprint_lost|reprint_damaged|reprint_data_change|reprint_quality`), `batch_id`, `output_kind` **`handovers`** — `id`, `badge_issue_id`, `recipient_type` (`holder|delegate`), `recipient_person_id`, `pickup_location_id`, `pickup_code`, `batch_code`, `state`, `confirmed_by`, `confirmed_at`, `reversed_at`, `reverse_reason` **`print_jobs`** — `id`, `season_id`, `kind`, `requested_by`, `params_json`, `state`, `total_items`, `done_items`, `cursor`, `output_path`, `expires_at` طابور التصيير المجزّأ. #### البوابات **`gate_devices`** — `id`, `season_id`, `label`, `device_fingerprint`, `assigned_zone_id`, `state`, `last_seen_at`, `bundle_version` **`scan_records`** — `id`, `season_id`, `badge_issue_id`, `zone_id`, `event_day_id`, `device_id`, `operator_id`, `result` (`allowed|denied`), `reason_code`, `matched_grant_id`, `evaluated_rules_json`, `scanned_at`, `synced_at`, `client_scan_id` `client_scan_id` فريد لمنع التكرار عند مزامنة المسوحات المتأخرة. #### المنصة **`audit_log`** — `id`, `season_id`, `actor_id`, `action`, `subject_type`, `subject_id`, `before_json`, `after_json`, `ip`, `user_agent`, `occurred_at`, `prev_hash`, `row_hash` **`scope_assignments`** — `user_id`, `season_id`, `scope_type` (`global|category|subcategory|zone|print_station`), `scope_id`, `granted_by`, `granted_at`, `expires_at` **`signing_keys`** — `id`, `season_id`, `algorithm`, `public_key`, `private_key_encrypted`, `state` (`active|retiring|retired`), `activated_at`, `retires_at` --- ## ٦. الحالات وسير العمل ### آلة حالة الطلب ``` ┌──────────────────────────┐ ▼ │ draft ──submit──► submitted ──┬─► pending_coordinator ──┬─► approved │ │ │ └─► pending_admin ──► approved │ ├─► photo_rejected ──resubmit_photo──► submitted ├─► rejected ──resubmit_data──► submitted └─► withdrawn ``` ### جدول الانتقالات | من | إلى | الفاعل | الشرط | |---|---|---|---| | `draft` | `submitted` | المشارك / موظف | اكتمال الحقول الإلزامية، وعدم بلوغ `registration_cap` | | `submitted` | `pending_coordinator` | تلقائي | الفئة الفرعية `two_stage` | | `submitted` | `approved` | منسق أو إدارة | الفئة الفرعية `single_stage`، ولم يُبلغ حد القبول | | `pending_coordinator` | `pending_admin` | منسق | الفئة الفرعية `two_stage` | | `pending_admin` | `approved` | إدارة | لم يُبلغ حد القبول | | `submitted` \| `pending_*` | `approved` | إدارة | موافقة مباشرة بصلاحية `application.approve_direct` | | أي حالة مراجعة | `rejected` | منسق أو إدارة | سبب إلزامي من قائمة مغلقة | | أي حالة مراجعة | `photo_rejected` | مراجع الصور | سبب إلزامي | | `photo_rejected` | `submitted` | المشارك | رفع صورة جديدة | | `rejected` | `submitted` | المشارك | تعديل البيانات عبر رابط موقّع | | أي حالة | `withdrawn` | المشارك أو إدارة | — | **قواعد ملزمة:** 1. **عدد المراحل من `approval_flow` وحده.** لا يتأثر بالسعة ولا بأي إعداد آخر. تغييره يحتاج صلاحية ويُسجَّل. 2. **الانتقال غير المعرَّف مرفوض** مهما كانت الشاشة أو القناة. 3. **الإجراء الجماعي حلقة فوق الفردي.** لا مسار كود ثانٍ. 4. **حد القبول** يُحسب بعدد الاعتمادات `active` — لا بحالة بطاقاتها. فالطباعة لا تُفرغ مقعداً. ### ما يحدث عند القبول عملية واحدة ذرّية: 1. `applications.review_status = approved` 2. إنشاء صف `accreditations` بحالة `active` 3. نسخ الأيام إلى `accreditation_event_day` 4. توليد `access_grants` من `subcategory_zone_access` 5. إعادة بناء `effective_access` لهذا الاعتماد 6. قيد الحدث في `audit_log` 7. جدولة إيميل القبول ### دورة حياة البطاقة ``` pending_print ──print──► printed ──handover──► handed_over │ │ │ │ └───report_lost────────►│ ▼ ▼ void ◄──────── supersede ──────────────────► superseded ``` **التمييز الحاسم:** | العملية | الأثر | الرمز | |---|---|---| | **إعادة طباعة** | `PrintEvent` جديد على نفس `BadgeIssue` | نفس الرمز | | **إعادة إصدار** | `BadgeIssue` جديد برقم إصدار أعلى، يُبطل سلفه ذرّياً | رمز جديد | يوجب إعادة الإصدار: تغيّر الصورة، أو الاسم، أو الفئة الفرعية، أو الأيام، أو المناطق المؤثرة على الشبكة. ولا يوجد مسار يُبقي بطاقة قديمة صالحة بعد تغيير مؤثر. --- ## ٧. الصلاحيات والنطاقات ### ثلاث طبقات مستقلة #### ١. المصادقة — من أنت جلسة للويب. رموز Sanctum للأجهزة والشركاء، عمرها 12 ساعة، مربوطة بجهاز مسجَّل، قابلة للإبطال الفوري. تحقّق بخطوتين إلزامي على كل حساب يملك موافقة نهائية أو تصدير بيانات شخصية أو إدارة أدوار. #### ٢. الصلاحية — ماذا تستطيع صلاحيات ذرّية بصيغة `resource.action` تصف فعلاً في المجال لا صفحة في الواجهة: ``` application.view · application.create · application.approve_stage_one application.approve_final · application.approve_direct · application.reject application.reject_photo · application.edit · application.withdraw accreditation.suspend · accreditation.revoke · accreditation.transfer badge.issue · badge.reissue · badge.print · badge.reprint · badge.void badge.preview · handover.confirm · handover.reverse access_grant.create · access_grant.revoke access_request.approve_zone · access_request.approve_admin design.edit · design.publish person.view_pii · person.export · report.view · report.export season.switch · season.clone · user.manage · role.manage ``` **ممنوع** ورود اسم مسار أو صفحة في أي صلاحية. اختبار معماري يفشل البناء عند المخالفة. **منفصلة عمداً:** المعاينة والطباعة وإعادة الطباعة والإبطال والتسليم والتراجع صلاحيات مستقلة. فمن يملك المعاينة لا يطبع، ومن يطبع لا يُبطل. #### ٣. النطاق — على مَن تستطيع جدول `scope_assignments` هو **المصدر الوحيد** لما يراه المستخدم، مفروض في طبقة الاستعلام: | النطاق | المعنى | |---|---| | `global` | كل الموسم | | `category` | فئة وما تحتها | | `subcategory` | فئة فرعية بعينها | | `zone` | منطقة بعينها وطلباتها ومسوحاتها | | `print_station` | مكان طباعة بعينه | **لا يجوز لأي صلاحية أن توسّع النطاق ضمنياً.** من يملك `design.publish` لا يرى بذلك كل الفئات. ### الأدوار | الدور | الصلاحيات | النطاق المعتاد | |---|---|---| | `super_admin` | الكل | `global` | | `event_manager` | إدارية واسعة، موافقة نهائية | `global` | | `category_coordinator` | مراجعة ومرحلة أولى | `category` | | `subcategory_coordinator` | مراجعة ومرحلة أولى وتسجيل منسوبيه | `subcategory` | | `zone_manager` | موافقة طلبات منطقته، مسح، إحصاءات | `zone` | | `print_operator` | طباعة وتسليم | `print_station` | | `gate_operator` | مسح فقط | `zone` | | `reviewer` | مراجعة الصور | `global` أو `category` | | `viewer` | عرض وتقارير بلا تعديل | حسب التعيين | الأدوار تُعرَّف في seeder محكوم بالإصدار وقابل لإعادة التشغيل بأمان — لا أمر يدوي. --- ## ٨. التسجيل والمراجعة ### قنوات الدخول | القناة | المسار | الحالة الابتدائية | |---|---|---| | رابط عام | `/r/{registration_token}` | `submitted` مع `source=public_link` | | إدخال إداري | لوحة التحكم | `submitted` أو `approved` مباشرة بالصلاحية | | إنشاء عبر API | `POST /api/v1/applications` | `submitted` مع `source=api_create` | | استيراد دفعة | `POST /api/v1/imports` | `submitted` مع `source=api_import` و`requires_intake_review` | > لا توجد حالة «جديد من API». المصدر حقل مستقل، والسجل الوارد يدخل دورة المراجعة العادية مع علامة تستوجب مراجعة أولية. ### رابط التسجيل مبني على `registration_token` عشوائي طويل، لا على `id` ولا على `code` مقروء. فلا يُخمَّن ولا يُعدَّد. النموذج يعرض **حقول تلك الفئة الفرعية فقط** من `subcategory_form_fields`. الحقول الممكنة: اللقب، الاسم بالعربية والإنجليزية، الجنس، البريد، الجوال، الجنسية، رقم الهوية وصورتها وانتهاؤها، الصورة الشخصية، الجهة، المنصب، المجال الوظيفي، المدينة، اللغة، اسم الخيل، المدرب، الملاحظات. **قيود:** - الرفع: JPG/PNG/PDF حتى 5MB، مع فحص نوع حقيقي لا امتداداً، وتجريد بيانات EXIF. - حد الطلبات: 20/دقيقة لكل IP على التسجيل، و10/دقيقة على فحص التكرار. - التحقق من التكرار يقابل `persons` عبر بصمة الهوية والبريد المعياري. - **المشارك لا يختار مناطقه.** المناطق تأتي من فئة فرعيةه أو بطلب معتمد. - عند بلوغ `registration_cap` **يُغلق النموذج فوراً** ويعرض صفحة «اكتمل التسجيل لهذه الجهة» مع بيانات التواصل مع المنسق. لا قائمة انتظار: من يحتاج مقعداً إضافياً يرفع الأمر لمنسق جهته، والمنسق يطلب رفع السعة من الإدارة. ### روابط التعديل الذاتي بعد الرفض أو رفض الصورة، يصل المشارك رابط **موقّع ومنتهي الصلاحية** (Laravel signed URL، صلاحية 14 يوماً) خاص بطلبه. لا يُبنى على معرّف تسلسلي، ولا يُعاد استخدامه بعد الإرسال. ### مراجعة الصور شاشة مستقلة تعرض الصور حسب حالتها. المراجع يقصّ ويضبط، أو يقبل كما هي، أو يرفض بسبب من قائمة مغلقة (وجه غير واضح، خلفية غير مناسبة، جودة منخفضة، صورة غير شخصية). **أثر تغيير صورة معتمدة:** تبقى `applications.review_status = approved` وتبقى وقائع التسليم كما هي، وتُنشأ **بطاقة جديدة** تُبطل سابقتها. فلا تُمحى واقعة، ولا تبقى بطاقة قديمة صالحة. --- ## ٩. المناطق والأيام والصلاحية الفعلية ### الأيام `event_days` صفوف بيانات: تاريخ ومسمّى وترتيب ونافذة تشغيل. العدد غير محدود. - **الاعتماد** يرتبط بأيام محددة عبر `accreditation_event_day`. ولا يُصدَر اعتماد بلا يوم واحد على الأقل. - **الفئة الفرعية** له مناطق منفصلة لكل يوم في `subcategory_zone_access`. - **المنطقة** لها جدول فتح لكل يوم في `zone_day_schedule`. - **البطاقة** تطبع مسمّيات أيامها من `event_days` مباشرة. ### مصادر صلاحية الدخول | المصدر | كيف تُنشأ | |---|---| | `subcategory` | من مناطق الفئة الفرعية لحظة القبول | | `direct` | منح فردي لشخص بعينه، بصلاحية `access_grant.create` | | `request` | من طلب دخول معتمد | | `category` | من منح على مستوى الفئة كلها | كلها تُكتب في `access_grants` بنفس الشكل، وكلها تخضع لنفس فحوص اليوم والمنطقة. **لا مصدر يتجاوز الفحوص.** ### الصلاحية الفعلية `effective_access` إسقاط محسوب يجمع كل المنوحات القائمة بعد خصم الملغاة، في صفوف `(accreditation_id, zone_id, event_day_id)`. يُعاد بناؤه بمهمة مجزّأة عند: قبول طلب، تغيير مناطق ملف، اعتماد طلب دخول، إلغاء منح، تعليق اعتماد أو إلغائه، تغيير جدول فتح منطقة. البناء بدفعات 1,000 صف مع علامة زمنية وحذف المتقادم، فيبقى ضمن حدود زمن تنفيذ PHP. **الفائدة:** قرار البوابة يصبح بحثاً واحداً في فهرس مغطٍّ بدل حساب شجري فوق أربعة مصادر. ### شبكة المناطق على البطاقة كل نوع بطاقة له شبكة بأبعاد معرَّفة في `badge_types` (`slot_grid_rows` × `slot_grid_cols`). وكل منطقة تُسنَد إلى **رقم خانة صريح ومخزَّن** في `zone_badge_slots`. **القواعد:** 1. الخانة تُسنَد مرة وتُقفل طوال الموسم. 2. إيقاف منطقة أو إضافة أخرى **لا يزحزح** خانة غيرها. 3. المناطق المسموحة تظهر بلونها ورقمها، وغير المسموحة تبقى خانة فارغة محتفظة بموضعها. 4. إذا تجاوز عدد المناطق المسموحة عدد الخانات المتاحة، **يُمنع إصدار البطاقة** ويُعرض خطأ صريح يسمّي المناطق الزائدة. لا اختفاء صامت. 5. الشبكة تُطبع من `badge_access_snapshots` لا من البيانات الحيّة. --- ## ١٠. البطاقات: التصميم والإصدار والتصيير ### القرار التقني الأصعب الهدف: تشكيل عربي صحيح، وتطابق المعاينة مع المطبوع، ودقة طباعية، وخطوط داخل المستودع — **بلا Chromium**، لأن cPanel لا يوفّره. **الاختيار: mPDF.** | الخيار | التشكيل العربي | مقاس دقيق | نص متجه | متاح على cPanel | |---|---|---|---|---| | **mPDF** | ✅ مدمج | ✅ | ✅ | ✅ | | Dompdf | ❌ ضعيف | ✅ | ✅ | ✅ | | TCPDF | ⚠️ مقبول، وCSS محدود جداً | ✅ | ✅ | ✅ | | Browsershot/Chromium | ✅ ممتاز | ✅ | ✅ | ❌ يحتاج Node وChromium | | رسم نقطي (GD + Pango) | ⚠️ هشّ | ⚠️ | ❌ نقطي | ✅ | mPDF يشكّل العربية ويربط حروفها ويعالج النص ثنائي الاتجاه، ويضمّن الخطوط من ملفات، ويخرج نصاً متجهاً بمقاس صفحة بالمليمتر. ### ما نخسره وكيف نعوّضه mPDF لا يفهم CSS الحديث (لا flexbox ولا grid)، فالتطابق مع معاينة المتصفح **ليس بنيوياً** كما لو استعملنا نفس المحرك. التعويض بثلاثة إجراءات: 1. **قاموس عناصر مقيَّد.** البطاقة ليست HTML حرّاً، بل مجموعة عناصر من قائمة مغلقة، كلها موضوعة بإحداثيات مطلقة بالمليمتر. لا تخطيط انسيابي أصلاً، فلا فرق في التخطيط بين المحركين. 2. **مصيِّر واحد للطرفين.** صنف `BadgeDocument` واحد يحوّل نسخة التصميم إلى HTML مقيَّد. المعاينة في المتصفح ومخرج mPDF يستهلكان **نفس المخرج**، لا نسختين. 3. **بوابة جودة قبل النشر.** لا يُنشر تصميم حتى يولّد النظام PDF فعلياً ويعرضه على المصمم لاعتماده، ويفحص آلياً: اختراق مناطق الأمان، وتجاوز النص حدود عنصره، وتوفر كل خط مستعمل. ### الوحدات والخطوط - **الإحداثيات بالمليمتر** في قاعدة البيانات. لا بكسل، ولا تحويل سحري. - **مقاس صفحة PDF = مقاس التصميم** بالضبط، فتُطبع ١:١ بلا ملاءمة. - **الخطوط في `resources/fonts/badge/`** داخل المستودع، تُسجَّل في `mPDF fontdata`. لا خط من نظام التشغيل ولا من `~/.fonts`. - **فحص إقلاع** يتحقق من وجود كل ملف خط وبصمته، ويرفض تشغيل خدمة التصيير عند النقص. - **قائمة الخطوط في المحرر = الخطوط المضمّنة فعلاً.** فما لا يُطبع لا يُعرض. - **رمز QR يُرسم SVG** لا صورة، فلا معنى للدقة عليه. - الصور النقطية وحدها لها `target_dpi` بحد أدنى 300. ### المحرر المرئي واجهة Vue داخل اللوحة: رفع خلفية، وإضافة عناصر من القائمة المغلقة، وسحبها، مع شبكة ارتكاز وخطوط محاذاة وإدخال رقمي بالمليمتر وتحريك بالأسهم. ولكل عنصر: الموضع والأبعاد، وحجم الخط ووزنه ولونه، والمحاذاة، والزوايا، والطبقة، والإظهار. **النشر يجمّد نسخة:** `badge_design_versions` تحفظ لقطة JSON كاملة ببصمة. والبطاقة المُصدَرة تشير إلى النسخة، فتعديل التصميم بعدها لا يغيّر بطاقة قائمة. ### الإصدار عند إصدار بطاقة: 1. تُحسب لقطة الصلاحيات من `effective_access` وتُحفظ في `badge_access_snapshots`. 2. تُجمَّد الصورة المستعملة (`photo_asset_id`). 3. يُولَّد رمز مسح جديد ويُخزَّن **مجزّأً** (`scan_token_hash`) لا نصاً صريحاً. 4. تُربط البطاقة بنسخة التصميم. 5. إن وُجدت بطاقة نشطة سابقة، تُنقل إلى `superseded` في **نفس المعاملة**. ### رمز المسح حمولة موقّعة لا رقم عشوائي: ``` payload = { season_id, badge_issue_id, issue_number, key_id } token = base64url(payload) + "." + base64url(Ed25519_signature) ``` - التوقيع بمفتاح الموسم من `signing_keys`. - تدوير المفتاح يمرّ بحالة `retiring` يُقبل فيها المفتاحان معاً، فلا يُبطل بطاقة سارية. - الخادم يخزّن بصمة الرمز لا الرمز. - **البطاقة المُعاد إصدارها لها رمز جديد**، والقديم يصبح `superseded` فيُرفض عند البوابة. > **الأثر:** تصوير بطاقة بالجوال لم يعد كافياً للدخول إلى الأبد. وإبطال بطاقة لا يحتاج قائمة مفقودات فقط، بل يقع بنيوياً عند إعادة الإصدار. --- ## ١١. الطباعة والتسليم ### مسارات الإنتاج | المسار | يُنتج ملفاً | يُسجَّل | |---|---|---| | معاينة بطاقة | نعم | نعم — `badge.preview` في سجل التدقيق | | طباعة فردية | نعم | نعم — `PrintEvent` | | طباعة دفعة | نعم | نعم — `PrintEvent` لكل بطاقة بنفس `batch_id` | | طباعة كل بطاقات ملف | نعم | نعم | **لا يوجد مسار إنتاج غير مسجَّل.** وكل طباعة تحمل `reason_code` إلزامياً من قائمة مغلقة، فإعادة الطباعة تُعرف بسببها. ### قيود الإنتاج - **لا تُطبع بطاقة إلا لاعتماد `active`.** لا طباعة لمرفوض ولا لمعلّق ولا لقيد المراجعة. - **مقاس الورق واحد لكل دفعة.** الدفعة تُقسَّم آلياً حسب مقاس التصميم، وتُنتج ملفاً لكل مقاس. لا خلط صامت. - **الدفعة الكبيرة مهمة مجزّأة** في `print_jobs`: شرائح 50 بطاقة، تحفظ موضعها في `cursor`، وتستأنف بعد انقطاع. فلا تصطدم بزمن تنفيذ PHP. - ملف المخرج ينتهي بعد مدة (`expires_at`) ويُحذف، فلا تتراكم البطاقات على القرص. ### التسليم | المستلم | الرمز | الإيميلات | |---|---|---| | صاحب البطاقة | رمز استلام لكل بطاقة | مكان الاستلام وساعاته والرمز | | مندوب الجهة | رمز دفعة واحد | لكل مشارك: بطاقتك عند مندوب جهتك. وللمندوب: العدد والأسماء ورمز الدفعة | شاشة التسليم تبحث بالرمز أو الاسم أو الجوال، وتعرض **صورة المشارك للتحقق البصري**، ثم تؤكد التسليم. - لا يُسلَّم مرتين. - التراجع عن تسليم خاطئ ممكن بصلاحية `handover.reverse` مع سبب، ويُسجَّل. - **مندوب الجهة يُقرأ من فئة فرعية كل مشارك على حدة** لا من أول مشارك في التحديد. وإن اختلفت الجهات في التحديد، تُقسَّم الدفعة تلقائياً. --- ## ١٢. البوابات والدخول ### قرار الدخول: دالة خالصة مُعلَّلة ```php AccessDecision::evaluate(BadgeIssue $badge, Zone $zone, EventDay $day, Carbon $now): Decision // تعيد: { result, reason_code, matched_grant_id, evaluated_rules } ``` ترتيب الفحوص مثبَّت وموثَّق، وأول قاعدة تفشل هي التي تحكم: | # | الفحص | رمز الرفض | |---|---|---| | ١ | صحة التوقيع وانتماء الرمز للموسم | `SIGNATURE_INVALID` · `UNKNOWN_TOKEN` · `WRONG_SEASON` | | ٢ | حالة البطاقة | `BADGE_VOID` · `BADGE_LOST` · `BADGE_SUPERSEDED` | | ٣ | حالة الاعتماد | `ACCREDITATION_SUSPENDED` · `ACCREDITATION_REVOKED` | | ٤ | اليوم مُرسَل وصالح وضمن أيام الاعتماد | `MISSING_DAY` · `DAY_NOT_ALLOWED` | | ٥ | المنطقة مفتوحة في ذلك اليوم وضمن نافذتها | `ZONE_CLOSED` · `OUTSIDE_WINDOW` | | ٦ | وجود صف في `effective_access` | `NO_GRANT` | | ٧ | نافذة التكرار المتلاحق | `DUPLICATE_ENTRY` | | ٨ | حالة الجهاز والمشغّل | `DEVICE_SUSPENDED` | **قواعد ملزمة:** - **لا فرع «لا يُفحص».** كل نقص بيانات رفض برمز سبب وإنذار تشغيلي. - **كل محاولة تُسجَّل** — المسموحة والممنوعة — مع `evaluated_rules_json`. فسؤال «لماذا مُنع فلان؟» يُجاب من السجل بجملة واحدة. - **الخادم يعيد رموزاً فقط.** الترجمة والعرض مسؤولية التطبيق. - الرد يحمل اسم المشارك وصورته ليتحقق الحارس بصرياً أنها بطاقته. ### الأداء بحث واحد في فهرس مغطٍّ على `effective_access`. لا حساب شجري لحظي. مع ذاكرة مؤقتة أمامه إن توفّرت. المستهدف: 30 مسحة/ثانية. وبقياس متحفّظ على MariaDB، بحث الفهرس المغطي دون المللي ثانية، فالعنق في الشبكة لا في القاعدة. ### العمل دون اتصال الشبكة في المضمار قد تنقطع، والبوابة لا تحتمل التوقف. **حزمة اليوم:** يُنزِّل الجهاز حزمة موقّعة تخص **يوماً واحداً ومنطقته** فقط: - بصمات الرموز الصالحة وصلاحياتها لتلك المنطقة ذلك اليوم - البطاقات المُبطلة والمفقودة - إصدار الحزمة ووقت انتهائها **القواعد:** - الجهاز **يرفض تقييم يوم غير يوم حزمته** — لا تخمين. - المناطق ذات `connectivity_requirement = online_only` (كالمقصورة الملكية) **لا تُقيَّم دون اتصال** إطلاقاً. - المسوحات المحلية تُخزَّن بـ`client_scan_id` فريد، وتُزامَن عند عودة الاتصال بلا تكرار. - الحزمة لها عمر قصير، وتنتهي بانتهاء اليوم. - عند عودة الاتصال يُطبَّق أي إبطال جرى أثناء الانقطاع، وتُرفع إنذارات لأي مسح سُمح به ثم تبيّن أنه لبطاقة مُبطلة. ### شاشات مرتبطة - **الإحصاءات:** سجل المسح مع فلترة باليوم والمنطقة والنتيجة، والتصدير. - **المفقودات:** تسجيل بطاقة مفقودة، فتُرفض فوراً وتُدرج في حزم الأجهزة. - **غرفة العمليات:** لوحة حيّة بحالة الأجهزة وآخر ظهور لكل منها ومعدل المسح ونسب الرفض حسب السبب. --- ## ١٣. طلبات الدخول ### الأنواع | النوع | لمن | عند القبول | |---|---|---| | فردي | مشارك واحد | **يُنشأ `access_grant` فعلي** فيظهر على البطاقة ويُقبل عند البوابة | | لفئة فرعية | كل منسوبي الفئة الفرعية | تُضاف المناطق إلى `subcategory_zone_access` وتُولَّد منوحات لكل منسوبيه | | لفئة | كل ملفات الفئة | تُطبَّق على ملفات الفئة كلها | > **القبول يمنح فعلياً.** لا يوجد طلب «مقبول» بلا أثر، ولا خطوة منح يدوية منفصلة في شاشة أخرى. ### مراحل الموافقة كل منطقة لها خاصيتان: `requires_zone_manager_approval` و`requires_admin_approval`. - **لا تحتاج موافقة إدارة:** مدير المنطقة يقبل ← تُطبَّق فوراً. - **تحتاج موافقة إدارة:** مدير المنطقة يقبل ← يُرفع للإدارة ← الإدارة تقبل ← تُطبَّق. - منطقة واحدة تحتاج موافقة ترفع الطلب كله. - الإدارة تستطيع القبول مباشرة. - الرفض يوقف الطلب بلا أي تطبيق، مع سبب إلزامي. الطلب يحدد المناطق **لكل يوم على حدة**. وتُنشأ إشعارات عند الإنشاء لأصحاب الشأن. ### أثر إعادة الإصدار منح منطقة جديدة لمشارك **طُبعت بطاقته** يستوجب إعادة إصدار، لأن الشبكة على البطاقة تغيّرت. النظام ينبّه إلى ذلك ويعرض إصدار بطاقة بديلة، ويُبطل القديمة عند الإصدار. --- ## ١٤. البريد والإشعارات ### القوالب نص عربي وإنجليزي، وصورة رأس لكل لغة، ومرفق اختياري. يُربط القالب بفئة فرعية أو فئة أو يبقى عاماً. الاختيار: قالب الفئة الفرعية ← قالب الفئة ← القالب العام. | النوع | متى | |---|---| | `application_received` | بعد التسجيل | | `application_approved` | عند القبول النهائي | | `application_rejected` | عند الرفض، مع السبب ورابط التعديل | | `photo_rejected` | عند رفض الصورة، مع رابط الرفع | | `badge_ready` | جاهزية البطاقة ومكان استلامها ورمزه | | `badge_with_delegate` | البطاقة عند مندوب الجهة | | `delegate_batch` | للمندوب: العدد والأسماء ورمز الدفعة | | `handover_confirmed` | تأكيد التسليم | | `coordinator_welcome` | بيانات دخول المنسق ورابط تسجيل جهته | | `invitation` / `invitation_reminder` | دعوات التسجيل والتذكير | الرموز: `[NAME]` `[LINK]` `[LOCATION]` `[LOCATION_ADDRESS]` `[PICKUP_HOURS]` `[DATE]` `[CODE]` `[COUNT]` `[PROFILE]` `[DAYS]` ### البطاقة تُرسَل برابط لا بمرفق إيميل القبول يحمل **رابطاً موقّعاً منتهي الصلاحية** لتحميل البطاقة، لا ملف PDF مرفقاً. **الأثر:** - الإرسال الجماعي لا يولّد آلاف ملفات PDF مسبقاً، فلا يصطدم بحدود المنصة. - سحب البطاقة بعد إعادة الإصدار فوري: الرابط القديم يعطي البطاقة الجديدة أو يرفض. - حجم الرسائل صغير فتتحسن نسبة الوصول. - التحميل يُسجَّل، فيُعرف من سحب بطاقته ومتى. ### الإرسال مزوّد رسائل تعاملية عبر SMTP مع SPF وDKIM وDMARC مضبوطة على النطاق. والطابور `database` مع `queue:work --max-time=55` من cron. معالجة الارتداد تُعلّم العنوان `bounced` وتوقف المحاولات، وتُظهره في شاشة الدعوات. ### الدعوات قائمة بريد مع الفئة الفرعية واللغة. الحالات: `scheduled` `sent` `opened` `registered` `bounced` `cancelled`. وزر تذكير لمن لم يسجّل. ### الإشعارات الداخلية جرس بعدد غير المقروء. تُولَّد عند: تسجيل جديد في نطاقك، طلب ينتظر موافقتك، طلب منطقة يخصك، اكتمال مهمة طباعة، فشل مزامنة جهاز، بلوغ سعة فئة فرعية. ولكل مستخدم تفضيلات لما يصله. والإشعارات القديمة تُؤرشف ولا تُحذف — وهي ليست بديلاً عن سجل التدقيق. --- ## ١٥. التكامل وواجهات API كل المسارات تحت `/api/v1`، بمصادقة Sanctum، وحدود طلبات لكل عميل. ### تطبيق البوابات | المسار | الوظيفة | |---|---| | `POST /auth/device` | تسجيل جهاز ومصادقته | | `GET /gate/zones` | مناطق المشغّل وأيامها | | `GET /gate/bundle?day=` | حزمة اليوم الموقّعة للعمل دون اتصال | | `POST /gate/scan` | مسح بطاقة. يعيد النتيجة ورمز السبب وبيانات المشارك | | `POST /gate/scans/sync` | رفع مسوحات مخزّنة محلياً | | `GET /gate/stats` | إحصاءات مناطق المشغّل | | `POST /gate/handover` | تسليم من التطبيق | | `POST /gate/requests/{id}/decide` | بتّ طلب منطقة | ### التكامل الخارجي | المسار | الوظيفة | |---|---| | `POST /applications` | إنشاء طلب | | `PATCH /applications/{ref}` | تعديل طلب | | `POST /imports` | استيراد دفعة | | `GET /imports/{id}` | حالة الاستيراد ونتيجة كل سجل | | `GET /reference/{subcategories\|titles\|nationalities}` | القوائم المرجعية | **الاستيراد:** - إلزامي: `external_ref`, `first_name`, `email`, `subcategory_external_ref`. اختياري: الأيام، المرافق، الصورة برابط. - `subcategory_external_ref` يُطابَق مع `subcategories.external_ref`. - **تحقق مسبق قبل أي كتابة**: يُفحص كل السجلات ويُعاد تقرير بالأخطاء، ثم يُنفَّذ الاستيراد أو يُلغى كاملاً. - البريد أو الهوية المكررة تُرفَض بسطرها مع سبب، ولا تُسقط الدفعة. - الصور تُنزَّل بحد حجم ومهلة وفحص نوع. - الاستيراد مهمة مجزّأة تُستأنف. --- ## ١٦. التقارير | التقرير | المحتوى | |---|---| | لوحة الموسم | المسجلون، قيد المراجعة، المعتمدون، المرفوضون، المطبوعون، المسلَّمون | | حسب الفئة والفئة الفرعية | الأعداد نفسها موزّعة، مع نسبة الإنجاز لكل جهة | | الطباعة | حسب المكان والنوع والسبب والموظف واليوم | | إعادة الطباعة | من أُعيدت طباعة بطاقته وكم مرة ولماذا — مؤشر رقابي | | التسليم | المسلَّم والمعلّق، وما مضى عليه وقت بلا استلام | | الدخول | المسوحات حسب المنطقة واليوم والنتيجة وسبب الرفض | | الرفض عند البوابات | توزيع أسباب المنع — يكشف خللاً في المنوحات قبل أن يصبح أزمة | | النشاط | من فعل ماذا ومتى | كل الجداول تُصدَّر إلى Excel بتصدير مجزّأ يبثّ السطور ولا يحمّلها في الذاكرة. --- ## ١٧. الأمن والخصوصية ### البيانات الشخصية النظام يحفظ صوراً شخصية ووثائق هوية وطنية. وهذا يخضع لنظام حماية البيانات الشخصية السعودي. | الإجراء | التفصيل | |---|---| | **التخزين خارج جذر الويب** | كل الملفات في `storage/app/private/`. لا شيء في `public/` ولا رابط رمزي إليه | | **التقديم عبر التطبيق** | كل ملف يمرّ على متحكّم يفحص الصلاحية والنطاق، ويعيده برابط موقّع قصير العمر | | **تشفير وثائق الهوية** | مشفّرة على القرص بمفتاح التطبيق | | **تسجيل الوصول** | كل عرض لوثيقة هوية يُقيَّد في سجل التدقيق باسم من عرضها | | **صلاحية منفصلة** | `person.view_pii` منفصلة عن عرض بقية البيانات | | **الاحتفاظ** | تُحذف وثائق الهوية بعد انتهاء الموسم بمدة محددة، وتبقى البيانات الإحصائية مجهّلة | | **التصدير** | `person.export` صلاحية مستقلة، وكل تصدير يُسجَّل بمن ومتى وكم سجلاً | ### حماية التطبيق - روابط التسجيل والتعديل مبنية على رموز عشوائية طويلة أو روابط موقّعة منتهية، لا على معرّفات تسلسلية. - حدود طلبات على كل مسار عام. - فحص نوع الملفات الحقيقي، وتجريد EXIF، ومنع رفع ما ليس صورة أو PDF. - تحقّق بخطوتين على الحسابات الحسّاسة. - رؤوس أمان كاملة، وجلسات آمنة، وCSRF على كل نموذج. - المعرّفات العلنية ULID لا أرقاماً تسلسلية. - نسخ احتياطي يومي لقاعدة البيانات والملفات، مع اختبار استرجاع دوري موثَّق. --- ## ١٨. سجل التدقيق جدول `audit_log` يقيّد كل إجراء ذي أثر: من، وماذا، وعلى ماذا، ومتى، ومن أي عنوان، وما القيمة قبل وبعد. **غير قابل للتلاعب عملياً:** كل صف يحمل `prev_hash` و`row_hash`، فتتشكل سلسلة تجزئة. أي تعديل أو حذف لصف يكسر السلسلة، وأمر تحقق دوري يفحصها ويرفع إنذاراً عند الكسر. **لا يُحذف.** الاحتفاظ دائم، والأرشفة إلى جداول باردة لا الحذف. **الأحداث المقيَّدة:** تقديم طلب، موافقة بمرحلتها، رفض، رفض صورة، إصدار بطاقة، طباعة، إعادة طباعة بسببها، إبطال، تسليم، تراجع عن تسليم، منح منطقة، سحب منح، بتّ طلب دخول، تغيير دور أو نطاق، عرض وثيقة هوية، تصدير، نشر تصميم، تبديل موسم، تسجيل بطاقة مفقودة. --- ## ١٩. الواجهة والتعريب - **ثنائي اللغة كامل**: كل نص من ملفات ترجمة، ولا نص مكتوب في الكود. - **RTL أصيل**: التخطيط بخصائص منطقية (`margin-inline`, `padding-inline`) لا بيمين ويسار. - **التقويمان**: الميلادي والهجري بجدول أم القرى محكوم بالإصدار. كل تاريخ يُعرض بالتقويمين حيث يلزم. - **الأسماء بالحقلين**: عربي وإنجليزي لكل اسم، والبطاقة تطبع ما يناسب تصميمها. - **شاشات التشغيل السريع**: شاشة التسليم وشاشة الطباعة مصمّمتان للاستعمال تحت ضغط — بحث واحد، وصورة كبيرة للتحقق، وزر واحد للإجراء. - **إمكانية الوصول**: تباين كافٍ، وتنقّل بلوحة المفاتيح، وحالات تركيز ظاهرة. --- ## ٢٠. خطة التنفيذ سبع مراحل، كل منها تنتهي بشيء قابل للاستعمال والاختبار. ### المرحلة ١ — الأساس (أسبوعان) الهيكل والوحدات · المواسم وأيام الحدث · الأشخاص والجهات · المستخدمون والأدوار والصلاحيات والنطاقات · سجل التدقيق بسلسلة التجزئة · الترجمة وRTL. **معيار القبول:** إنشاء موسم بأيامه، وإنشاء مستخدمين بأدوار ونطاقات، وكل إجراء يظهر في سجل التدقيق وسلسلته سليمة. ### المرحلة ٢ — الفئات والتسجيل (أسبوعان) الفئات والفئات الفرعية بكل إعداداتها · حقول النموذج · رابط التسجيل العام · التحقق ومنع التكرار · رفع الملفات والصور · الاستيراد الأولي. **معيار القبول:** فتح رابط تسجيل ملف، وتقديم طلب يصل بحالة `submitted`، ورفض المكرر، واحترام حد التسجيل. ### المرحلة ٣ — المراجعة والاعتماد (أسبوعان) آلة حالة الطلب · الموافقة بمرحلة ومرحلتين · الرفض ورفض الصورة · روابط التعديل الموقّعة · مراجعة الصور وقصّها · إنشاء الاعتماد · النطاقات مطبَّقة على الشاشات. **معيار القبول:** اختبار لكل تركيبة (دور × حالة × سير موافقة) يثبت أن الانتقال المسموح يقع والممنوع يُرفض، وأن الإجراء الجماعي يطابق الفردي. ### المرحلة ٤ — المناطق والصلاحيات (أسبوعان) المناطق وجداول فتحها · خانات البطاقة · مناطق الفئات الفرعية لكل يوم · المنوحات · `effective_access` وإعادة بنائه المجزّأ · طلبات الدخول بمراحلها. **معيار القبول:** منح منطقة بأي مصدر ينعكس في `effective_access`، وقبول طلب دخول يُنشئ منحاً فعلياً، وإعادة البناء تكتمل على 30,000 اعتماد ضمن حدود المنصة. ### المرحلة ٥ — البطاقات (ثلاثة أسابيع) أنواع البطاقات والتصاميم · المحرر المرئي · تجميد النسخ · محرك التصيير بـmPDF · بوابة الجودة · الإصدار ولقطة الصلاحيات · رمز المسح الموقّع ودورة المفاتيح. **معيار القبول:** تصميم بطاقة بالفأرة، ونشرها بعد اعتماد PDF الفعلي، وإصدار بطاقة تُطبع ١:١ بمقاسها، والعربية مشكَّلة صحيحة، والشبكة تعرض المناطق في خاناتها الثابتة. > أطول مرحلة وأعلاها مخاطرة. **يُبنى نموذج أولي لتصيير mPDF في الأسبوع الأول من المشروع كله**، لا في هذه المرحلة، لنتأكد مبكراً من جودة التشكيل العربي قبل أن نبني عليه. ### المرحلة ٦ — الإنتاج والتسليم (أسبوعان) أماكن الطباعة وربط الموظفين · الطباعة الفردية والجماعية · طابور الطباعة المجزّأ · وقائع الطباعة وأسبابها · التسليم بنوعيه · التراجع · المفقودات. **معيار القبول:** طباعة دفعة 1,000 بطاقة تكتمل بالتقسيم دون تجاوز حدود المنصة، وكل بطاقة لها سطر في سجلها، وتسليم دفعة لمندوب جهة يرسل الإيميلات الصحيحة. ### المرحلة ٧ — البوابات والتشغيل (ثلاثة أسابيع) واجهات تطبيق البوابات · محرك قرار الدخول · حزم العمل دون اتصال · المزامنة · الأجهزة · غرفة العمليات · التقارير · البريد والدعوات · لوحات الإحصاء. **معيار القبول:** اختبار حمل يثبت 30 مسحة/ثانية، واختبار انقطاع يثبت استمرار البوابة على الحزمة المحلية ثم المزامنة بلا تكرار، وكل رفض له رمز سبب مفسَّر في السجل. ### عبر كل المراحل - اختبارات Pest: وحدة للمنطق، وتكامل لسير العمل، ومعمارية تفشل البناء عند خرق المبادئ. - اختبار معماري يفشل عند: نموذج تشغيلي بلا `BelongsToSeason`، أو صلاحية تحمل اسم مسار، أو عمود `level`، أو مقارنة رقمية لحالة. - توثيق يُحدَّث مع الكود لا بعده. --- ## ٢١. الأسئلة المفتوحة هذه تحتاج قرارك، وبعضها يغيّر التصميم لا الجدول فقط. ### تحتاج إجابة قبل المرحلة ٢ 1. **الشخص في أكثر من فئة فرعية** — مصوّر يعمل لقناتين، أو مدرب يملك خيلاً أيضاً. هل يُسمح؟ التصميم الحالي يسمح بإعداد قابل للضبط: اعتماد مستقل لكل فئة فرعية، وبطاقة لكل اعتماد. والبديل منعه، فيُجبر الشخص على اختيار جهة واحدة. **الفرق العملي:** لو سُمح، فالمصوّر يحمل بطاقتين بصلاحيتين مختلفتين، وإحصاءات الحضور تعدّه مرتين ما لم نربطهما بـ`person_id`. ولو مُنع، فجهته الثانية لا تستطيع تسجيله أصلاً وتحتاج استثناءً يدوياً. ### قرارات معتمدة من العميل | القرار | ما اعتُمد | أين طُبّق | |---|---|---| | المصطلح | **«الفئة الفرعية»** هي الاسم المعتمد، و`Subcategory` في الكود | المعجم والوثيقة كلها | | امتلاء السعة | **يُغلق التسجيل** عند بلوغ الحد. لا قائمة انتظار | القسم ٨ وجدول `subcategories` | | المرافقون | **بطاقة مستقلة** لكل مرافق برمزها الخاص، ويُحسب في السعة | القسم ٥ | | حقول النموذج | **ديناميكية بالكامل** لكل فئة فرعية من شاشة الإعدادات بلا برمجة | القسم ٥ والقسم ٨ | ### تحتاج إجابة قبل المرحلة ٥ 5. **عدد التصاميم** المطلوبة هذا الموسم وأنواع البطاقات. 6. **طريقة الطباعة** — على ورق أبيض بالكامل، أم على بطاقات مطبوعة الخلفية مسبقاً فلا يُطبع فوقها إلا البيانات؟ يحدد وضع «بلا خلفية». 7. **الطابعات** — نوعها ومقاس البطاقة بالمليمتر بالضبط، وهل تحتاج علامات قصّ. ### تحتاج إجابة قبل المرحلة ٧ 8. **الشبكة في المضمار** — هل تنقطع فعلاً؟ يحدد أولوية العمل دون اتصال. 9. **تطبيق البوابات** — نبنيه نحن أم فريق آخر؟ نحن نحدد الواجهة في الحالتين. 10. **عدد البوابات والأجهزة** المتوقعة، وذروة الدخول الفعلية. ### تحتاج إجابة للتشغيل 11. **نوع الاستضافة** — cPanel مشتركة أم VPS بلوحة cPanel؟ المشتركة تحدّ الذاكرة وزمن التنفيذ، وتجعل تصيير الدفعات الكبيرة أبطأ وأكثر تقسيماً. 12. **مزوّد البريد** — هل لديكم حساب مزوّد رسائل تعاملية، أم نختار واحداً؟ 13. **الموسم القادم** — تاريخه، لنعرف ما الذي يجب أن يكون جاهزاً ومتى. --- ## ملحق: لماذا هذه القرارات بعض قرارات هذه الدراسة غير بديهية، وهذه مبرراتها في سطر لكل واحد: | القرار | السبب | |---|---| | محاور حالة منفصلة | الحقل الواحد يختار قيمة ويمحو الباقي، فتضيع وقائع حدثت فعلاً | | البطاقة كيان مستقل | لتُتتبَّع كل نسخة، وتُبطل القديمة عند إعادة الإصدار | | خانة المنطقة صريحة ومقفلة | لئلا يزحزح تغيير في منطقة مواضع المناطق على بطاقات مطبوعة | | الموسم جذر لا سمة | لئلا تتلوث بيانات موسم بموسم، ولتصبح «لا تحذف منطقة في منتصف الموسم» غير ذات معنى | | الأيام بيانات | لئلا يكلّف يوم ثالث تعديل مخطط في كل مكان | | الفشل مغلق | نقص البيانات في نظام تحكّم بالدخول يجب أن يمنع لا أن يسمح | | رمز موقّع | لئلا تكفي صورة بالجوال للدخول، ولتُبطل النسخ القديمة بنيوياً | | الصلاحية والنطاق منفصلان | لئلا تمنح صلاحية إدارية رؤيةً لم يقصدها أحد | | الموافقة معزولة عن السعة | لئلا يُلغي حقل إداري ضابطاً أمنياً | | كل إنتاج مسجَّل | إعادة طباعة البطاقة واقعة أمنية، وعدّادها بلا قيمة إن كانت بعض المسارات لا تُسجَّل | | البريد برابط | لئلا ينهار الإرسال الجماعي تحت وزن المرفقات | | سلسلة تجزئة للتدقيق | لأن سجلاً يمكن تعديله ليس سجلاً | --- *نهاية الدراسة · للمراجعة والتعديل قبل بدء التنفيذ*