دليل المطوّر
كيف يعمل الـ API وكيف تبنون عليه تطبيق Flutter. التفاصيل الكاملة لكل رابط في التوثيق التفاعلي.
1. فكرة التطبيق
تطبيق لعائلة واحدة (عائلة الخضري): شجرتها كاملة، وملف لكل فرد حيّاً أو متوفى، وأخبار العائلة.
- النسب عن طريق الأب فقط. لكل شخص
father_idواحد (أو لا شيء للجذر). لا يوجد أمهات ولا زوجات. الأبناء والبنات يُرتّبون معاً من الأكبر. - التصفح كله بدون حساب: الشجرة والأفراد والأخبار.
- أنواع المستخدمين:
| الدور | ماذا يستطيع |
|---|---|
| زائر (بدون حساب) | يتصفح كل شيء |
user عضو | يعلّق ويعجب بالأخبار (إذا كان الخبر يسمح) |
staff موظف | يضيف ويعدّل الأفراد والأخبار. بدون صلاحية persons.direct / posts.direct تذهب تعديلاته للمدير للموافقة (رد 202) |
admin مدير العائلة | كل شيء + الموافقة على طلبات الموظفين + المستخدمين والإعدادات. يصله إشعار بكل عملية موظف |
2. الأساسيات
Base URL: https://api-production-34bd.up.railway.app/api/v1
| Header | متى |
|---|---|
Accept: application/json | دائماً |
Accept-Language: ar أو en | دائماً — يحدد لغة البيانات والتسميات ورسائل الأخطاء |
Authorization: Bearer <token> | بعد تسجيل الدخول (اختياري للتصفح، لكن أرسلوه إن وُجد: يغيّر can وliked_by_me) |
شكل الرد الناجح
{ "message": "تم الحفظ", // في عمليات الكتابة فقط
"data": { ... } أو [ ... ],
"meta": { ... } } // أحياناً: عدادات أو معلومات إضافية
القوائم (pagination)
GET /persons و/posts وأمثالها ترجع صفحات. أرسلوا page وper_page، واقرؤوا:
"links": { "next": "https://…/persons?page=2", "prev": null, … },
"meta": { "current_page": 1, "last_page": 48, "per_page": 25, "total": 1188 }
للتحميل اللانهائي: اطلبوا الصفحة التالية ما دام links.next ليس null.
شكل الخطأ (دائماً)
{ "message": "البيانات المدخلة غير صحيحة.", // مترجمة: اعرضوها للمستخدم
"code": "VALIDATION_FAILED", // ثابتة: اعتمدوا عليها في الكود
"errors": { "email": ["…"] } } // في 422 فقط: خطأ لكل حقل
أشياء يجب معرفتها
- المعرّفات نصوص ULID (مثل
01m45sp9akn922fwjqjtbgxycg) وليست أرقاماً.ref_noهو رقم الشخص في سجل العائلة (للعرض والبحث). - القيم الثابتة (الجنس، حالة الحياة…) تأتي كـ
{"value": "male", "label": "ذكر"}: استخدمواvalueفي المنطق وlabelللعرض. القائمة الكاملة:GET /meta/enums. - التواريخ بصيغة ISO. تاريخ الميلاد والوفاة معه دقة
birth_date_precision:exact/month/year/approx— اعرضوا السنة فقط إذا كانتyear. - الحقول المترجمة (الاسم، النبذة، عنوان الخبر…) ترجع بلغة
Accept-Language. عند الإرسال:{"ar": "أحمد", "en": "Ahmad"}، أو نص عادي فيُحفظ بلغةContent-Language. - الصور روابط كاملة جاهزة:
thumb(صغيرة للقوائم) وpreview(للملف) وoriginal. - حدود الطلبات: 120 طلب/دقيقة، الدخول 10/دقيقة، التعليقات والرفع 10–20/دقيقة. عند تجاوزها:
429 TOO_MANY_REQUESTS. - اللغات المفعّلة واتجاهها:
GET /meta/languages. إعدادات عامة (التسجيل مفتوح؟ حجم الرفع):GET /meta/settings.
3. الدخول والحساب
| الرابط | الجسم | |
|---|---|---|
| تسجيل جديد | POST /auth/register | name, email, password, password_confirmation, phone?, device_name? |
| دخول | POST /auth/login | email, password, device_name? |
| نسيت كلمة السر | POST /auth/forgot-password | email |
| حسابي | GET /auth/me | — |
| تعديل حسابي | PATCH /auth/me | name, phone, locale… |
| تغيير كلمة السر | PUT /auth/me/password | current_password, password, password_confirmation |
| خروج | POST /auth/logout | — |
| حذف الحساب | DELETE /auth/me | — |
الدخول والتسجيل يرجعان:
{ "data": { "token": "12|AbC…", "token_type": "Bearer",
"user": { "id", "name", "email", "roles": ["staff"],
"permissions": ["persons.create", "persons.update", …] } } }
- احفظوا
tokenفيflutter_secure_storage. التوكن لا ينتهي حتى الخروج، وكل جهاز له توكن (أرسلواdevice_nameمثل اسم الجهاز). - أي رد
401 UNAUTHENTICATED= احذفوا التوكن وارجعوا لوضع الزائر. - استخدموا
permissionsلإظهار الأزرار (مثلاً زر "إضافة" إذا فيهاpersons.create). وكل فرد وخبر يرجع معهcanجاهزة (انظر تحت).
4. الشجرة (الرسم)
GET /graph يرجع جزءاً من الشجرة حول شخص، جاهزاً للرسم:
| Parameter | المعنى |
|---|---|
root | الشخص في المنتصف (افتراضياً جذر العائلة) |
depth_up / depth_down | كم جيلاً للأعلى (الآباء) وللأسفل (الأبناء) |
max_nodes | حد أقصى للعقد (حتى 2000 = الشجرة كاملة) |
{ "data": {
"root_id": "01m45…",
"nodes": [ { "id", "ref_no", "label": "سالم الخضري", "gender": "male",
"life_status": "deceased", "birth_year", "death_year", "thumb_url",
"generation": 1, "father_id": "01m45…", "children_count": 3,
"has_more_up": false, "has_more_down": true } ],
"families": [ { "father_id": "01m45…", "children": ["id1", "id2"] } ] // مرتبة من الأكبر
},
"meta": { "graph_version": 2424, "nodes_count": 3, "truncated": false } }
- الرسم: كل عنصر في
families= خط من الأب إلى أبنائه بالترتيب. (مكتبة مثلgraphviewتكفي). - التوسيع عند الضغط: إذا
has_more_down= true اعرضوا زر "+" واطلبوا GET/graph/expand/{id}?direction=down&depth=2وادمجوا النتيجة. - التخزين: الرد يحمل
ETag. أرسلوه فيIf-None-Matchفي المرة القادمة: إذا لم يتغير شيء يرجع 304 بدون جسم، فاعرضوا النسخة المخزّنة (يعمل بدون إنترنت أيضاً).
5. الأفراد
| الرابط | ملاحظات | |
|---|---|---|
| القائمة | GET /persons | فلاتر: filter[search]، filter[gender]، filter[life_status]، filter[generation]، filter[father_id]. ترتيب: sort=birth_date أو -birth_date، generation… |
| بحث سريع | GET /persons/search?q= | 20 نتيجة مختصرة. الاسم بأي لغة، والعربي موحّد ("احمد" تجد "أحمد"). رقم = ref_no |
| الملف الكامل | GET /persons/{id} | كل الحقول: الألقاب، الكنية، الميلاد والوفاة ومكانها، الدفن، المهنة، النبذة، الأب |
| بطاقة الملف | GET /persons/{id}/card | الأنسب لشاشة الملف: الاسم بالنسب (lineage_name: فلان بن فلان)، العمر، الأب، الأبناء، العدادات، وcan |
| الأبناء | GET /persons/{id}/children | مرتّبين من الأكبر، وmeta فيها عدد الأبناء والبنات |
| الأقارب | GET /persons/{id}/relatives | الأب، الجد، الإخوة، الأعمام والعمات، الأبناء |
| الأجداد | GET /persons/{id}/ancestors?depth=6 | سلسلة الآباء للأعلى |
| الذرية | GET /persons/{id}/descendants?depth=3 | مقسمة حسب الجيل |
| صلة القرابة | GET /persons/{id}/relationship/{otherId} | label جاهز ("عم"، "بنت عم"، "أخت")، والجد المشترك |
| أخبار الشخص | GET /persons/{id}/posts | الأخبار المرتبطة به (نعي، زفاف…) |
كائن can في البطاقة يقول ماذا يستطيع المستخدم الحالي:
"can": { "update": true, "delete": false, "add_relative": true, "add_child": true,
"needs_review": true } // true = تعديلاته ستذهب للمدير: اعرضوا تنبيهاً بذلك
6. الأخبار
| الرابط | ملاحظات | |
|---|---|---|
| القائمة | GET /posts | type (news, obituary, wedding, birth, event, announcement)، q، person_id. المثبّت أولاً |
| الخبر | GET /posts/{id} | العنوان، النص، الصور، الأشخاص المرتبطون |
| التعليقات | GET /posts/{id}/comments | صفحات |
| إضافة تعليق | POST /posts/{id}/comments | {"body": "…"} — يحتاج حساب |
| حذف تعليق | DELETE /comments/{id} | صاحبه أو المدير (يرجع 204 بدون جسم) |
| إعجاب / إلغاء | POST / DELETE /posts/{id}/like | يحتاج حساب |
- كل خبر فيه
allow_commentsوallow_likes: إذا كانتfalseأخفوا الزر (العدّاد يرجعnull). المحاولة رغم ذلك ترجعCOMMENTS_DISABLED/LIKES_DISABLED. liked_by_meيكون صحيحاً فقط إذا أرسلتم التوكن.cover= أول صورة (للقائمة)، وimages= كل الصور.
7. الإضافة والتعديل والموافقة
العمليات الأساسية
| الرابط | الجسم | |
|---|---|---|
| إضافة قريب | POST /persons/{id}/relatives | {"relation": "son", "person": {"first_name": {"ar": "أحمد"}, "birth_date": "1990-03-12"}}relation: son، daughter، brother، sister، father |
| تعديل شخص | PATCH /persons/{id} | الحقول المتغيرة فقط، مثلاً {"life_status": "deceased", "death_date": "2010-12-01"} |
| تغيير الأب | PUT /persons/{id}/father | {"father_id": "…"} أو null |
| صورة الشخص | POST /persons/{id}/photo | multipart، حقل photo (jpg/png/webp، حتى 5MB) |
| ترتيب الأبناء | PUT /persons/{id}/children/order | {"order": ["id1", "id2", …]} (للتوائم أو التواريخ المجهولة) |
| حذف / استرجاع | DELETE /persons/{id} ثم POST /persons/{id}/restore | الحذف ناعم: الأبناء يبقون |
| خبر جديد | POST /posts | multipart: title[ar]، body[ar]، type، status (published/draft)، published_at (للجدولة)، allow_comments، allow_likes، is_pinned، person_ids[]، images[] (حتى 10) |
| تعديل / حذف خبر | PATCH / DELETE /posts/{id} | الصور: POST /posts/{id}/images وDELETE /posts/{id}/images/{imageId} |
قواعد تتحقق منها الشجرة تلقائياً (ترجع 422 برمز واضح): الأب ذكر وأكبر بـ 12 سنة على الأقل، لا يكون الشخص جداً لنفسه، الميلاد قبل الوفاة… انظر رموز الأخطاء.
تحذيرات بدون رفض: إذا كانت التواريخ تقريبية، أو وُلد الشخص بعد وفاة أبيه، يُحفظ الطلب ويرجع معه "meta": {"warnings": ["INVALID_PARENT_AGE"]} أو BIRTH_AFTER_PARENT_DEATH — اعرضوا تنبيهاً للمستخدم ليتأكد.
الموافقة: انتبهوا لـ 201 مقابل 202
نفس الطلب قد يرجع ردين مختلفين حسب صلاحية المستخدم:
| الرد | المعنى | ماذا تعرضون |
|---|---|---|
| 200 / 201 | حُفظ مباشرة (مدير، أو موظف معه *.direct) | "تم الحفظ" وحدّثوا الشاشة |
| 202 | لم يُحفظ بعد: أُرسل للمدير كطلب تعديل | "أُرسل للمدير للموافقة" — لا تضيفوا الشخص للشجرة محلياً |
// 202
{ "message": "أُرسل طلبك للمدير للموافقة",
"data": { "change_request": { "id", "status": {"value": "pending"}, "action": {"label": "إضافة شخص"},
"subject": "أحمد الخضري", "preview": { … } } } }
الطلبات: GET /change-requests (الموظف يرى طلباته، المدير يرى الكل)، والمدير يوافق POST /change-requests/{id}/approve أو يرفض …/reject مع {"note": "…"}. الموظف يلغي طلبه بـ DELETE.
8. الإشعارات
| القائمة | GET /notifications | كل إشعار فيه title وbody جاهزان للعرض، وdata (مثل change_request_id للانتقال) |
| عدد غير المقروء | GET /notifications/unread-count | للشارة على الأيقونة |
| تعليم كمقروء | POST /notifications/{id}/read أو /notifications/read-all |
لا يوجد Push حالياً (FCM مخطط لاحقاً): اطلبوا unread-count عند فتح التطبيق وكل دقيقة أو دقيقتين أثناء استخدامه.
9. شاشات المدير
كلها تحت /admin وتحتاج صلاحيات المدير:
GET /admin/stats— أرقام لوحة الإحصائيات./admin/users— القائمة، الإضافة، التعديل، الإيقاف (/suspend،/activate)، الأدوارPUT /admin/users/{id}/roles، والصلاحيات المباشرة للموظفPUT /admin/users/{id}/permissions(مثل منحpersons.direct).GET /admin/permissionsو/admin/roles— لبناء شاشة الصلاحيات./admin/languagesو/admin/settings— اللغات والإعدادات.PATCH /family— اسم العائلة، الجذر، إعدادات عرض النسب.
10. رموز الأخطاء
| HTTP | code | المعنى |
|---|---|---|
| 401 | UNAUTHENTICATED | بدون توكن أو توكن منتهي |
| 401 | INVALID_CREDENTIALS | الإيميل أو كلمة السر خطأ |
| 403 | ACCOUNT_SUSPENDED | الحساب موقوف |
| 403 | FORBIDDEN | لا توجد صلاحية |
| 404 | NOT_FOUND | غير موجود |
| 422 | VALIDATION_FAILED | حقول خاطئة: التفاصيل في errors |
| 429 | TOO_MANY_REQUESTS | طلبات كثيرة، أعيدوا بعد قليل |
| 422 | PARENT_GENDER_MISMATCH | الأب يجب أن يكون ذكراً |
| 422 | INVALID_PARENT_AGE | الأب أكبر من الابن بأقل من 12 سنة (عندما يكون التاريخان دقيقين) |
| 422 | CYCLE_DETECTED | الشخص لا يكون جداً لنفسه |
| 422 | INVALID_DATE_ORDER | الميلاد بعد الوفاة أو تاريخ في المستقبل |
| 422 | INVALID_DEATH_DATA | بيانات وفاة لشخص غير متوفى |
| 409 | PARENT_ALREADY_SET | له أب مسبقاً |
| 422 | PARENTS_REQUIRED_FOR_SIBLING | أضيفوا الأب أولاً لإضافة أخ أو أخت |
| 422 | GENDER_CHANGE_CONFLICT | لا يمكن تغيير جنس شخص له أبناء |
| 422 | REF_NO_TAKEN | الرقم المرجعي مستخدم |
| 422 | TRANSLATION_REQUIRED | القيمة بالعربية (اللغة الافتراضية) مطلوبة |
| 403 | COMMENTS_DISABLED / LIKES_DISABLED | مغلقة على هذا الخبر |
| 422 | TOO_MANY_IMAGES | أكثر من 10 صور للخبر |
| 409 | CHANGE_REQUEST_CLOSED | الطلب عولج مسبقاً |
| 409 | CHANGE_REQUEST_FAILED | تعذرت الموافقة لأن الشجرة تغيرت |
القاعدة البسيطة: اعرضوا message للمستخدم كما هي (مترجمة)، واستخدموا code فقط عندما تحتاجون تصرفاً خاصاً (مثل 401 → شاشة الدخول).
11. Flutter: البداية
عميل dio واحد للتطبيق
final dio = Dio(BaseOptions(
baseUrl: 'https://api-production-34bd.up.railway.app/api/v1',
headers: {'Accept': 'application/json'},
connectTimeout: const Duration(seconds: 60), // السيرفر قد يأخذ وقتاً في أول طلب
));
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
options.headers['Accept-Language'] = currentLocale; // 'ar' أو 'en'
final token = await storage.read(key: 'token');
if (token != null) options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (e, handler) async {
final body = e.response?.data;
if (e.response?.statusCode == 401) await auth.logoutLocally();
handler.reject(e.copyWith(error: ApiError(
code: body is Map ? body['code'] : 'NETWORK',
message: body is Map ? body['message'] : 'تحقق من الاتصال',
fields: body is Map ? body['errors'] : null,
)));
},
));
الحفظ مع الموافقة
final res = await dio.post('/persons/$id/relatives', data: {
'relation': 'son',
'person': {'first_name': {'ar': name}, 'birth_date': date},
});
if (res.statusCode == 202) {
showSnack('أُرسل للمدير للموافقة');
} else {
showSnack(res.data['message']);
refreshTree();
}
رفع صورة
await dio.post('/persons/$id/photo', data: FormData.fromMap({
'photo': await MultipartFile.fromFile(file.path),
}));
الشجرة مع التخزين
final res = await dio.get('/graph',
queryParameters: {'depth_down': 12, 'max_nodes': 2000},
options: Options(
headers: {if (savedEtag != null) 'If-None-Match': savedEtag},
validateStatus: (s) => s == 200 || s == 304,
));
if (res.statusCode == 304) return cachedGraph;
save(res.data, res.headers.value('etag'));
12. كل شاشة وروابطها
| الشاشة | الروابط |
|---|---|
| البداية (Splash) | /meta/languages، /meta/settings، /family، و/auth/me إذا يوجد توكن |
| الشجرة | /graph، /graph/expand/{id}، وعند الضغط على شخص: /persons/{id}/card في Bottom Sheet |
| الأفراد | /persons بالفلاتر، و/persons/search لمربع البحث |
| ملف شخص | /persons/{id}/card، تبويبات: /persons/{id} (التفاصيل)، /relatives، /children، /posts |
| صلة القرابة | /persons/search لاختيار الشخصين ثم /persons/{a}/relationship/{b} |
| الأخبار | /posts (تبويبات حسب type) |
| خبر | /posts/{id}، /comments، /like |
| إضافة / تعديل شخص | /persons/search (لاختيار الأب)، POST /relatives، PATCH /persons/{id}، /photo |
| الإشعارات | /notifications، /unread-count، /read |
| طلبات التعديل | /change-requests، /approve، /reject |
| حسابي | /auth/me، /auth/me/password، /auth/logout |
| المدير | /admin/stats، /admin/users، /admin/settings |
⚠️ هذا هو السيرفر الحقيقي ببيانات العائلة. للتجربة استخدموا حساب موظف بدون صلاحية مباشرة: كل إضافاتكم تذهب للمدير بدل أن تُكتب في الشجرة.
أي سؤال أو رابط ناقص؟ تواصلوا مع مسؤول المشروع.