Control Panel Specifications
Create
Multi-step Create form. Metadata · EN · AR · FR · Sponsor tabs. Validation rules, slug auto-generation, no direct publish path.
المواصفات الكاملة لكل حقل في صفحة إضافة Founder File الجديد في لوحة التحكم. مكتوبة للمطوّر اللي هيبني الشاشة + الـ API + قاعدة البيانات، وكمان لفريق QC اللي هيتحقّق من التطبيق مقابل هذه المواصفات. لكل حقل ستجد: (1) بيعمل ايه، (2) مكانه بالظبط في الـ CP UI، (3) نوع backend والعلاقات، (4) قواعد التحقّق اللي QC يقدر يختبرها، (5) ما الذي يُنتجه على الصفحة العامة.
الجدول: FounderFile (السجلّ الأساسي)
هذه الحقول موجودة على تبويب **Metadata** في صفحة إضافة Founder File الجديد (باستثناء الحقول المختومة من النظام في الأسفل واللي للقراءة فقط في الـ UI).
Id (نظام)
- الغرض — مفتاح أساسي داخلي تستخدمه كل الجداول المرتبطة (صفوف اللغة، الموضوعات، نقاط التعلّم، العملاء، سجل التدقيق). لا يراه أي مستخدم.
- نوع DB — UNIQUEIDENTIFIER NOT NULL · PRIMARY KEY (clustered) · DEFAULT NEWID().
- التحقّق — اختبار QC: كل سجلّ يرجع من الـ API عنده UUID `id` مش فاضي.
FileNumber (نظام)
- الغرض — رقم تحريري تسلسلي ("File #04"). يُستخدم كهوية بشرية في الـ eyebrow على الكروت + صفحة التفاصيل + غلاف SVG الاحتياطي.
- نوع DB — INT NOT NULL · IDENTITY(1,1) · UNIQUE NONCLUSTERED.
- التحقّق — اختبار QC: الأرقام تسلسلية، بلا فجوات في التدفّق الطبيعي. مقفول بعد أول حفظ (لا يوجد affordance للتعديل في الـ UI).
Slug
- الغرض — المعرّف الآمن للـ URL تحت `/founder-files/{slug}`. حسّاس لـ SEO — بمجرد نشر الملف بـ slug، محرّكات البحث تفهرس هذا الـ URL.
- نوع DB — NVARCHAR(120) NOT NULL · UNIQUE NONCLUSTERED.
- التحقّق — Regex `^[a-z0-9]+(-[a-z0-9]+)*$` · 3–120 حرف. عند التكرار وقت الحفظ، السيرفر يضيف `-2`، `-3`. اختبارات QC: جرّب حفظ ملفين بنفس العنوان EN وأكّد إن الثاني يأخذ `-2`؛ جرّب تعديل الـ slug بعد النشر وأكّد إن الحقل معطّل.
Status (نظام + إجراءات أدمن)
- الغرض — حالة دورة الحياة. تتحكّم في الظهور العام (فقط `published` يظهر على الموقع الحيّ) وفي الإجراءات المتاحة في الـ CP.
- نوع DB — NVARCHAR(20) NOT NULL · CHECK Status IN ('draft','ready-for-review','under-review','published','needs-updates','archived') · DEFAULT 'draft'.
- التحقّق — اختبارات QC: تعديل أي حقل على ملف `published` يجب أن يبدّل الحالة تلقائياً لـ `needs-updates`؛ زرّ Archive يجب أن يكون معطّل لو في تسليمات عملاء معلّقة.
- سير العمل — آلة الحالة الكاملة في cp-specs:founder-files:status-workflow.
CategoryId
- الغرض — فئة باختيار واحد (Funding · Growth · Operations · Legal · Hiring). تتحكّم في label الـ meta-row على الصفحة العامة + الإدراج التلقائي تحت `/founder-files/categories/{slug}`. مشتركة مع Articles — اختيار `funding` هنا يستخدم نفس صفّ Category اللي بتعلّم بيه Articles.
- نوع DB — UNIQUEIDENTIFIER NOT NULL · FK → `Category(Id)` · ON DELETE NO ACTION · علاقة N:1.
- التحقّق — إلزامي. اختبارات QC: جرّب Submit-for-Review بـ Category فاضي → يجب أن يفشل بـ "Category required"؛ جرّب حذف Category تملك ملفات → يجب أن يفشل بـ FK constraint.
CoverImageUrl (رفع ملف)
- الغرض — صورة الغلاف 3:4 portrait اللي تظهر على كروت القائمة + أعلى صفحة التفاصيل. لمّا تكون مفقودة، الصفحة ترجع لـ SVG تحريري مكوّن من FileNumber + العنوان EN.
- نوع DB — NVARCHAR(500) NULL — يخزّن رابط Azure Blob اللي ترجعه خدمة الرفع.
- التخزين — مسار Azure Blob: `founder-files/{slug}/cover.{jpg|png|webp}` · container خاص · وصول عام عبر SAS URL قصير العمر.
- التحقّق — النسبة 3:4 (±2%) · حد أدنى 600×800 px · حد أقصى 5 MB · MIME ∈ {image/jpeg, image/png, image/webp}. اختبارات QC: ارفع 4:3 → يُرفض بـ "Wrong aspect ratio"؛ ارفع 5.5 MB → يُرفض؛ ارفع .pdf مموَّه كـ .jpg → server MIME sniff يرفضه.
OgImageUrl (رفع ملف، اختياري)
- الغرض — صورة override لمعاينات مشاركة وسائل التواصل (Facebook, Twitter, LinkedIn). نسبة مختلفة عن غلاف الـ 3:4 — هذه المنصّات بتفضّل 1.91:1.
- نوع DB — NVARCHAR(500) NULL.
- التخزين — مسار Azure Blob: `founder-files/{slug}/og.{ext}` · نفس الـ container الخاص.
- التحقّق — النسبة 1.91:1 (±5%، المفضّل 1200×630) · حد أدنى 1200×630 · حد أقصى 5 MB · نفس قواعد MIME زي الغلاف.
SponsorId
- الغرض — ربط اختياري بسجلّ راعي. لمّا يكون معيَّن، يتحكّم في شارة الراعي في الـ hero، كتلة الراعي المخصّصة على الصفحة، وشارة Sponsored على كرت القائمة. توجيه العملاء يتبع الراعي.
- نوع DB — UNIQUEIDENTIFIER NULL · FK → `FounderFileSponsor(Id)` · ON DELETE SET NULL · علاقة N:1.
- التحقّق — لو مش NULL: `Sponsor.ContractEnd >= GETUTCDATE()` وقت الحفظ، وإلا الحفظ يفشل بـ `SPONSOR_CONTRACT_EXPIRED`. اختبار QC: عيّن راعي بـ ContractEnd ماضي → الحفظ يجب أن يفشل بنفس كود الخطأ.
IsFeatured
- الغرض — يعلّم الملف كالمروَّج في فئته. يتحكّم في شارة "FEATURED" على الغلاف + خانة بانر hero في landing القائمة.
- نوع DB — BIT NOT NULL · DEFAULT 0.
- التحقّق — Filtered unique index `(CategoryId) WHERE IsFeatured = 1`. اختبار QC: شغّل Featured على ملف ثاني في فئة فيها واحد بالفعل → الحفظ يجب أن يفشل بـ `FEATURED_ALREADY_EXISTS` ويسمّي الملف المتعارض.
IsGated
- الغرض — يتحكّم فيما إذا كانت ضغطة Download تلتقط عميل. الافتراضي gated (=1). أوقفه فقط للملفات اللي التقاط عميل فيها يخالف عقد (نادر).
- نوع DB — BIT NOT NULL · DEFAULT 1.
- التحقّق — اختبار QC: مع `IsGated=1`، ضغطة Download تفتح مودال الموافقة؛ مع `IsGated=0`، ضغطة Download تذهب مباشرة لـ PDF stream (تحقّق إن صفّ DownloadLead لا يُنشأ).
IsDownloadable
- الغرض — kill-switch رئيسي لزرّ Download. اضبطه على 0 لإخفاء التحميل بالكامل (مثلاً الراعي سحب الحقوق، الملف بانتظار الأرشفة).
- نوع DB — BIT NOT NULL · DEFAULT 1.
- التحقّق — اختبار QC: مع `IsDownloadable=0`، صفحة التفاصيل العامة يجب ألا ترسم زرّ Download إطلاقاً (مش "disabled" — غائب تماماً من DOM).
ReadingTimeMinutes
- الغرض — تقدير تحريري يدوي لمدة قراءة الـ PDF. يساعد الزائر في قرار التحميل قبل الالتزام.
- نوع DB — TINYINT NOT NULL · CHECK (ReadingTimeMinutes BETWEEN 1 AND 120).
- التحقّق — عدد صحيح 1–120. اختبار QC: ادخل 0 أو 121 → المدخل يرفض + الـ CHECK على السيرفر يرفض.
PublishedAt (نظام)
- الغرض — تاريخ ظهور الملف الحيّ لأول مرة. يُستخدم في صفّ الـ meta + Article JSON-LD. لا يتغيّر عند إعادة النشر بعد needs-updates.
- نوع DB — DATETIME2 NULL · يُختم تلقائياً UTC عند أول انتقال لـ `published`.
- التحقّق — اختبار QC: أرشف ملف منشور ثم استعِده → PublishedAt يجب أن يبقى كما هو (مش يُحدَّث).
LastUpdatedAt (نظام)
- الغرض — تاريخ آخر تعديل على أي حقل. يتحكّم في سطر meta "Updated on" و `dateModified` في JSON-LD.
- نوع DB — DATETIME2 NOT NULL · يُختم تلقائياً UTC عند كل حفظ ناجح.
- التحقّق — اختبار QC: عدّل أي حقل واحد، احفظ → LastUpdatedAt يجب أن يقفز للوقت الحالي UTC؛ PublishedAt يبقى كما هو.
CreatedBy / LastUpdatedBy (نظام)
- الغرض — مسار تدقيق — مين أنشأ السجلّ ومين آخر مين لمسه. يستخدمه تبويب History + تقارير النشاط.
- نوع DB — UNIQUEIDENTIFIER NOT NULL · FK → `AdminUser(Id)` · ON DELETE NO ACTION (سجلّ التدقيق لازم يبقى لو الأدمن اتشال).
- التحقّق — اختبار QC: عطّل حساب AdminUser → مراجع CreatedBy الموجودة يجب أن تظل تحلّ (لا تصبح NULL).
ArchivedAt / ArchivedBy (نظام)
- الغرض — يُختم لمّا الملف يُحذف ناعماً. يتحكّم في نافذة الـ 30 يوم HTTP 410 + أهلية الاستعادة لـ 90 يوم.
- نوع DB — DATETIME2 NULL · UNIQUEIDENTIFIER NULL FK → AdminUser.
- التحقّق — اختبار QC: أرشف ثم استعِد خلال 90 يوم → ArchivedAt/By ترجع NULL؛ أرشف ثم انتظر 91 يوم → زرّ Restore يجب أن يختفي.
الجدول: FounderFileLang (1:N — صفّ لكل لغة)
هذه الحقول موجودة على تبويبات **EN** و **AR** و **FR** — نفس النموذج، ثلاث مرات. PK المركّب = (`FounderFileId`, `Lang`). تبويبات نموذج CP تسمح للأدمن بالوصول لـ `under-review` فقط لمّا 3 صفوف موجودة بكل الحقول الإلزامية معبَّأة (بوّابة النشر الثلاثية).
Lang (نظام، لكل صفّ)
- الغرض — يميّز اللغة اللي ينتمي لها الصفّ. يتحكّم في شارات `availableLanguages` على الصفحة العامة + في أي تبويب CP يخصّ الصفّ.
- نوع DB — CHAR(2) NOT NULL · CHECK Lang IN ('en', 'ar', 'fr') · جزء من الـ PK المركّب.
- التحقّق — اختبار QC: الملف لا يقدر يكون عنده صفّين Lang بنفس القيمة (الـ composite PK يفرض ده).
Title (لكل لغة)
- الغرض — العنوان الرئيسي. يُستخدم كـ h1 على صفحة التفاصيل العامة، عنوان الكرت، تبويب المتصفّح، ذيل breadcrumb، غلاف SVG الاحتياطي، وعنوان SEO (لمّا SeoTitle = NULL).
- نوع DB — NVARCHAR(80) NOT NULL.
- التحقّق — 10–80 حرف · بدون رموز تعبيرية · بدون علامات تنصيص مستقيمة. اختبار QC: جرّب عنوان 9 أحرف → يُرفض؛ 81 حرف → يُرفض؛ emoji → يُرفض؛ تأكّد إن العدّاد يتحدّث live.
Subtitle (لكل لغة)
- الغرض — tagline من جملة واحدة يظهر مباشرة تحت h1. يحدّد وعد القيمة. ويُستخدم احتياطياً كـ meta description لـ SEO لمّا SeoDescription = NULL.
- نوع DB — NVARCHAR(160) NOT NULL.
- التحقّق — 30–160 حرف · نصّ فقط. اختبار QC: ادخل 29 حرف → يُرفض؛ ادخل 161 حرف → يُرفض.
FullDescription (لكل لغة)
- الغرض — فقرة "لماذا هذا الملف مهم". تبيع الملف للزائر قبل التحميل. هي مش جسم الملف — الـ PDF هو الجسم.
- نوع DB — NVARCHAR(MAX) NOT NULL.
- التحقّق — 100–600 حرف · نصّ فقط (تاجات HTML تُقصّ تلقائياً عند الحفظ). اختبار QC: الصق `<script>` tag → السيرفر يجب أن يحذفه؛ الصق 99 حرف → يُرفض؛ الصق 601 حرف → يُرفض.
PdfUrl (لكل لغة — رفع ملف)
- الغرض — الـ PDF الفعلي اللي بيحمّله الزائر بعد الموافقة. واحد لكل لغة — الزائر يحمّل الـ PDF اللي يطابق لغة التصفّح.
- نوع DB — NVARCHAR(500) NOT NULL — يخزّن رابط Azure Blob.
- التخزين — مسار Azure Blob: `founder-files/{slug}/{lang}/v{N}.pdf`. آخر 5 نسخ محفوظة لكل لغة. container خاص — وصول عام فقط عبر SAS URL قصير العمر (ساعة) يُولَّد وقت التحميل. مفحوص للفيروسات بـ Azure Defender.
- التحقّق — حد أقصى 50 MB · MIME `application/pdf` فقط (sniff على السيرفر، مش امتداد بس). اختبارات QC: ارفع 51 MB → يُرفض قبل الرفع؛ ارفع .exe مغيّر اسمه لـ .pdf → server MIME sniff يرفضه؛ ارفع PDF حقيقي → الردّ يتضمّن `FileSizeBytes` + `PageCount` المحسوبَين تلقائياً؛ استبدل PDF موجود → النسخة السابقة تصبح `v{N-1}.pdf` في التخزين.
FileSizeBytes / PageCount (لكل لغة — نظام، تلقائي)
- الغرض — ميتاداتا مُستخرَجة تلقائياً عن الـ PDF المرفوع. تُستخدم لشارة المعاينة بعد الرفع في الـ CP + (مستقبلاً) عرض بجانب زرّ Download العام.
- نوع DB — BIGINT NOT NULL · INT NOT NULL.
- التحقّق — اختبار QC: ارفع PDF معروف 3.7 MB / 24 صفحة → معاينة CP تعرض "3.7 MB · 24 pages" بقيم مطابقة للمصدر.
SeoTitle / SeoDescription (لكل لغة، اختياري)
- الغرض — حقول override اختيارية لـ SEO. لمّا تكون مضبوطة، تأخذ الأولوية فوق Title/Subtitle لـ `<title>` tag + meta description. تُستخدم لمّا العنوان التحريري ممتاز للبشر لكن مش مثالي للبحث.
- نوع DB — NVARCHAR(120) NULL · NVARCHAR(300) NULL.
- التحقّق — لمّا مش NULL: 30–120 حرف (العنوان)، 70–300 حرف (الوصف). اختبار QC: اضبط SeoTitle، اعرض مصدر الصفحة → `<title>` يجب أن يطابق SeoTitle، مش Title الرئيسي.
الجدول: FounderFileTopic (وسيط N:M)
جدول وسيط يربط الملفات بالموضوعات. PK مركّب = (`FounderFileId`, `TopicId`). الموضوعات في جدول `Topic` مشترك مع Articles — slug الموضوع `early-stage` يحلّ لنفس الصفّ سواء عُلِّم على مقال أو ملف.
- TopicId — UNIQUEIDENTIFIER NOT NULL · FK → `Topic(Id)` · ON DELETE CASCADE على صفّ الـ junction فقط (الـ Topic يبقى).
- التحقّق — الحد الأدنى 1 موضوع قبل النشر. الحد الأقصى 8 لكل ملف (يُفرض ناعماً في CP).
الجدول: FounderFileLearningPoint (1:N)
صفّ لكل bullet في قائمة "ما الذي ستتعلّمه". أعمدة ثلاثية اللغة. 3–5 صفوف لكل ملف (يُفرض عند Submit-for-review).
- Id — UNIQUEIDENTIFIER NOT NULL · PK · افتراضي NEWID().
- FounderFileId — UNIQUEIDENTIFIER NOT NULL · FK → FounderFile · ON DELETE CASCADE.
- OrderIndex — TINYINT NOT NULL · 1–5 · يتحكّم في ترتيب العرض.
- TextEn — NVARCHAR(100) NOT NULL · 10–100 حرف.
- TextAr — NVARCHAR(100) NOT NULL · 10–100 حرف.
- TextFr — NVARCHAR(100) NOT NULL · 10–100 حرف.
الجدول: FounderFileSponsor (جدول مرجعي)
سجلّات الرعاة موجودة باستقلال عن الملفات؛ الراعي ممكن يدعم 0..N ملف. الـ CP للرعاة موديول منفصل — الحقول هنا للمرجعية على العلاقة فقط.
- Id — UNIQUEIDENTIFIER PK.
- Name — NVARCHAR(80) NOT NULL.
- Name_Ar — NVARCHAR(80) NOT NULL.
- Tier — NVARCHAR(20) NOT NULL CHECK Tier IN ('platinum','gold','silver','bronze').
- WebsiteUrl — NVARCHAR(500) NOT NULL.
- LogoUrl — NVARCHAR(500) NULL.
- ShortDescription / ShortDescription_Ar — NVARCHAR(300) NOT NULL لكل واحد.
- ContractStart / ContractEnd — DATETIME2 NOT NULL. سجلّ الراعي لا يقدر يدعم ملفات خارج هذه النافذة.
الجدول: DownloadLead (سجلّ قانوني، 1:N من FounderFile)
كل ضغطة Download مسوَّرة بالموافقة تُنشئ صفّ هنا. احتفاظ 7 سنوات بالسياسة. لا يُحذف نهائياً من الـ CP (فقط عبر سكربت Super Admin لـ GDPR erasure).
- Id — UNIQUEIDENTIFIER PK.
- FounderFileId — UNIQUEIDENTIFIER NOT NULL FK → FounderFile · ON DELETE NO ACTION (قانوني).
- PdfVersionDelivered — TINYINT NOT NULL — أي `vN.pdf` استلمها العميل (للتدقيق / إعادة الإرسال).
- LangDelivered — CHAR(2) NOT NULL — أي لغة PDF تم إرسالها.
- LeadEmail — NVARCHAR(254) NOT NULL · الحد الأقصى لـ RFC 5321.
- LeadRole — NVARCHAR(30) NOT NULL CHECK LeadRole IN (...).
- LeadCountry — CHAR(2) NOT NULL — ISO-3166-1 alpha-2.
- ConsentMarketing / ConsentPartners — BIT NOT NULL لكل واحد. يتحكّم في توجيه CRM (قائمة داخلية vs تسليم للراعي).
- CreatedAt / DeliveredAt — DATETIME2 NOT NULL · NULL. Delivered = وقت إرسال البريد مع الـ PDF.
ملخّص إعادة استخدام التصنيفات Cross-module
مشترك مع Articles
- جدول `Category` — نفس تصنيف الـ slug (funding, growth, operations, legal, hiring).
- جدول `Topic` — نفس تصنيف الـ multi-tag.
- جدول `AdminUser` لـ CreatedBy / LastUpdatedBy.
خاص بـ Founder Files
- `FounderFileSponsor` — الرعاة حصرية لهذا الموديول.
- `FounderFileLearningPoint` — قائمة النقاط بنية حصرية للملفات.
- `DownloadLead` — التقاط العميل موجود على الملفات فقط.
- `FounderFileAuditLog` — سجلّ انتقالات الحالة لكل ملف.
بوّابة Submit-for-review (تحقّق على السيرفر)
لمّا الأدمن يرسل POST لـ `submit-for-review`، السيرفر يتحقّق من كل:
- وجود 3 صفوف في `FounderFileLang` لهذا FileId (en + ar + fr)، كل واحد فيه Title + Subtitle + FullDescription + PdfUrl معبَّأة.
- بين 3 و5 صفوف في `FounderFileLearningPoint`، كل واحد فيه TextEn + TextAr + TextFr مش فاضي.
- `CoverImageUrl` ليس NULL ويشير إلى Blob موجود (فحص HEAD).
- `CategoryId` ليس NULL والـ FK يحلّ.
- على الأقل صفّ واحد في `FounderFileTopic` لهذا FileId.
- لو `SponsorId` معيَّن: `Sponsor.ContractEnd >= GETUTCDATE()`.
عند النجاح: UPDATE الـ `Status` لـ `under-review`، اكتب صفّ في سجل التدقيق، أرسل حدث `founder-file.submitted-for-review`. عند الفشل: ارجع مصفوفة أخطاء لكل حقل (HTTP 422 مع `errors: [{ field, code, message_en, message_ar, message_fr }]`).
معاينة الفورم — الحقول الفعلية اللي بيشوفها الأدمن
معاينة للقراءة فقط لكل مدخل كما سيظهر في الـ CP الحيّ. كل تبويب يُرسَم بعناصر فورم حقيقية (معطّلة للمعاينة فقط). QC يقدر يستخدم ده كمرجع عند التحقّق من التنفيذ.
التبويب 1 · Metadata
التبويبات 2 · 3 · 4 — EN / AR / FR (هيكل متطابق)
أعرض تبويب EN كمثال قياسي. تبويب AR يستخدم نفس الحقول لكن يحوّل المحرّر لـ RTL. تبويب FR مطابق لـ EN.
