# Lam-SAR Website — Backend

## نظرة عامة

باك-إند لموقع Lam-SAR، نظام CMS متكامل يدعم إدارة المحتوى، المستخدمين، والإعدادات مع دعم ثنائي اللغة (عربي/إنجليزي).

- **Framework:** Laravel 12
- **PHP:** 8.2+
- **قاعدة البيانات:** MySQL
- **المصادقة:** Laravel Sanctum (API tokens)
- **الأدوار والصلاحيات:** Spatie Laravel Permission
- **التعدد اللغوي:** Astrotomic Laravel Translatable
- **الخرائط:** Spatie Laravel Sitemap
- **الاختبارات:** Pest v3.8

---

## هيكل المجلدات

```
app/
├── Contracts/          # Strategy interfaces (login, reset password)
├── Filters/            # Query filters (CategoryFilter, NewsFilter, UserFilter…)
├── Helpers/helper.php  # Global helpers: generateCode(), availableLanguages(), setEnvValue()…
├── Http/
│   ├── Controllers/
│   │   ├── Dashboard/  # جميع controllers لوحة التحكم (Auth + Settings + Resources)
│   │   └── Site/       # controllers الموقع العام (SiteController, ContactUsController)
│   ├── Middleware/
│   │   └── SetAppLocale.php  # يحدد اللغة من Accept-Language header (ar/en فقط)
│   ├── Requests/       # Form request validation (50+ class)
│   └── Resources/      # API Resources (transformers للاستجابات)
├── Mail/               # Mailable classes (password reset, welcome…)
├── Models/             # Eloquent models (30+ model)
├── Services/
│   ├── Dashboard/      # Business logic لكل feature
│   │   ├── Auth/       # AuthenticationService + ResetPassword strategies
│   │   ├── Settings/   # Email, Font, Media, Website, Tracking services
│   │   └── BaseNewsService.php  # abstract base مشتركة بين News و LFII
│   └── Sender/
│       └── WhatsAppService.php
├── Traits/
│   ├── Filterable.php          # يضاف للـ Model لتفعيل query filters
│   ├── HandlesTranslations.php # CRUD مع ترجمات (storeWithTranslations, updateWithTranslations)
│   └── MediaTrait.php          # رفع وحذف الملفات (uploadFile, removeImage)
└── Enums/              # NewsStatusEnum وغيرها
```

---

## الـ Routes

### لوحة التحكم — prefix: `/dashboard`
middleware: `api`, `SetAppLocale`

**Auth (guest فقط):**
```
POST /dashboard/login
POST /dashboard/forgot
POST /dashboard/reset
POST /dashboard/reset-password
```

**Auth (auth:sanctum):**
```
POST /dashboard/change-contact
POST /dashboard/verify-contact
POST /dashboard/update-profile
POST /dashboard/update-password
```

**Resources (auth:sanctum):**
```
/dashboard/category        (CRUD)
/dashboard/tag             (CRUD)
/dashboard/news            (CRUD) + GET /featured-news + POST /toggle-featured/{id}
/dashboard/lfii            (CRUD) + POST /toggle-lfii-featured/{id}
/dashboard/page            (CRUD) + POST /toggle-page-status/{id}
/dashboard/widget          (CRUD) + POST /widget-update-status/{id}
/dashboard/ticket          (CRUD) + POST /message-replay/{id}
/dashboard/role            (CRUD) + GET /permissions
/dashboard/user            (CRUD) + GET /profile
/dashboard/maintenance     (CRUD) + POST /maintenance-changeStatus/{id}
```

**Settings (auth:sanctum):**
```
POST /dashboard/generate-sitemap
GET|POST /dashboard/email-settings
GET|POST /dashboard/website-settings
GET|POST /dashboard/seo-setting
POST /dashboard/update-media-settings
/dashboard/font            (CRUD)
GET /dashboard/get-timezone
GET /dashboard/get-dateformat
GET|POST /dashboard/update-tracking
GET|POST /dashboard/get-dashboard-settings
GET /dashboard/get-robots + POST /robots-import
```

**إحصائيات:**
```
GET /dashboard/lfii-statistics
GET /dashboard/latest-lfii
GET /dashboard/ticket-statistics
GET /dashboard/latest-ticket
```

### API العام — prefix: `/api`
```
POST /api/create-contact-us
GET  /api/get-lfii
GET  /api/get-blog
GET  /api/get-teamwork
GET  /api/user   (auth:sanctum)
```

---

## الـ Models الرئيسية

| Model | ملاحظات |
|-------|---------|
| `User` | translatable (first_name, last_name, slug) — password يُهاش تلقائيًا بـ mutator + cast |
| `News` | type: 'normal' أو 'blog' — حقول: `main_image`, `meta_image`, `is_featured`, `order_featured`, `status`, `is_published`, `schudle_date`, `schudle_time` |
| `Category` / `Tag` | translatable |
| `Page` | translatable — لها `location` لتحديد مكانها في الموقع |
| `Widget` | translatable — لها `content_type` و `page_location` |
| `Ticket` | support tickets — بدون ترجمة |
| `EmailVerification` | لتغيير الإيميل/الجوال، تنتهي بعد 60 ثانية |
| `ResetCodePassword` | كود إعادة تعيين كلمة المرور، ينتهي بعد 10 دقائق، يُقفل الحساب بعد 3 محاولات |

**ملاحظة:** جميع الـ models translatable تستخدم `Astrotomic\Translatable` — يحتاج `translatedAttributes` في الـ model وجدول `{model}_translations` في قاعدة البيانات.

---

## الأنماط المتكررة في الكود

### Service → Controller Pattern
الـ services ترجع string status codes، والـ controllers تحوّلها لـ JSON:
```php
// في الـ Service:
return 'success' | 'error' | 'not_found';

// في الـ Controller:
return $response == 'error'
    ? $this->respondError(...)
    : $this->respondData($response, ...);
```

### BaseController — Response Methods
```php
$this->respondData($data, $message, $code = 200)
$this->respondError($message, $code = 422)
$this->respondMessage($message, $code = 200)
$this->respondWithPagination($data, $message)
```

### HandlesTranslations Trait
```php
$this->storeWithTranslations($data, Model::class, function($model, $data) { ... });
$this->updateWithTranslations($data, $model, function($model, $data) { ... });
```
البيانات اللي فيها `ar` و `en` keys تروح للـ translation table تلقائيًا.

### MediaTrait
```php
$this->uploadFile($uploadedFile, 'photos/news')   // يرجع الـ URL
$this->removeImage($storagePath)                   // يحذف الملف
```
**تحقق دائمًا من:** `$file instanceof \Illuminate\Http\UploadedFile` قبل الرفع (مش `is_file()`).

### Filterable Trait
يضاف للـ Model: `use Filterable;` مع `protected $filter = SomeFilter::class;`
ثم في query: `News::filter()->get();`

---

## نقاط مهمة

- **اللغات المدعومة:** `ar` و `en` فقط (محددة في `availableLanguages()` في helper.php)
- **رفع الملفات:** تُحفظ في `storage/app/public/uploads/{dir}/{Y-m}/`، الـ URL يبدأ بـ `/storage/`
- **كود التحقق:** 5 أرقام، يُولَّد بـ `generateCode()` في helper.php
- **كلمة المرور:** تُهاش تلقائيًا عبر `setPasswordAttribute` mutator في `User` model
- **الحقول اللي تبدو خطأ لكنها صح:** `schudle_date`, `schudle_time` — هذه أسماء الأعمدة الفعلية في DB (خطأ إملائي في الـ migration الأصلية)
- **ApperanceController/Service:** الاسم خطأ إملائي أصلي (Apperance بدل Appearance) — لا تعدله بدون تعديل كل المراجع

---

## متغيرات البيئة المهمة

```env
APP_URL=                # URL السيرفر
FRONTEND_URL=           # URL الفرونت-إند (يُستخدم في CORS)
DB_DATABASE=lam_sar
SANCTUM_STATEFUL_DOMAINS=
IsProduct=              # إذا true يضيف /public لـ gethost()
```

---

## الاختبارات

```bash
php artisan test        # يشغل Pest tests
php artisan migrate:fresh --seed   # إعادة تهيئة DB مع البيانات الأساسية
php artisan storage:link           # ربط storage للملفات
```

---

## الإصلاحات المنفذة (2026-06-03)

1. **BaseNewsService** `toogleFeatured()`: أُضيف `where('is_featured',1)->where('order_featured','>',0)` قبل `increment()` — كان يؤثر على كل الأخبار
2. **AuthenticationController** `changeEmail()`: حُذف سطر dead code بعد `return`
3. **AuthenticationService** `updateProfile()`: استُبدل `is_file()` بـ `instanceof UploadedFile`
4. **NewsService** `handleNewsUpdateOperations()`: نفس إصلاح `is_file()`
5. **NewsService** `destroy()`: صُحح اسم الحقل من `image` إلى `main_image`
6. **SetAppLocale**: أُضيف تحقق من اللغة (ar/en فقط)
7. **cors.php**: `allowed_origins` بقى يقرأ من `FRONTEND_URL` في `.env`

---

## الإصلاحات المنفذة (2026-07-27) — بلاغات لوحة التحكم

1. **routes مفقودة تمامًا** — الميثودز كانت موجودة في الـ controllers بدون routes فكان الرد 404:
   - `POST /dashboard/lfii-change-status/{id}` → `LIFIController@changeStatus` (زر "تغيير الحالة")
   - `GET /dashboard/lfii-status` → `LIFIController@get_news_status`
   - `POST /dashboard/news-change-status/{id}` + `GET /dashboard/news-status`
   - `POST /dashboard/user-update-status/{id}` → `UserController@toggleStatus`
2. **LIFIService** `update()`: حُذف `dd($e)` — كان بيوقّف الريكوست ويطبع stack trace في الإنتاج
3. **LIFIService**: استُبدل `is_file()` بـ `instanceof UploadedFile` في 4 مواضع (نفس إصلاح NewsService)
4. **LIFIService** `destroy()`: صُحح `image` → `main_image` + حذف الـ `file` + `tags()->detach()`
5. **حذف الترجمات cascade** — جداول `*_translations` مفيهاش foreign key فعلي (الـ migrations
   استخدمت `unsignedBigInteger()->references()` وده **no-op** في Laravel)، فأي حذف كان بيسيب
   ترجمات يتيمة. الحل: `News::enableDeleteTranslationsCascade()` في `AppServiceProvider@boot`
   (الخاصية static على الـ trait فبتغطي كل الموديلات)
6. **عمود `published_at`** جديد في جدول `news` + backfill من `created_at` للمنشور حاليًا.
   قبل كده ماكانش فيه أي حقل بيسجّل *إمتى* اتنشر — بس `is_published` (0/1)، فعمود
   "تاريخ النشر" في الجدول كان بيطلع "غير منشور" دايمًا
7. **`release_date`** في `LIFIResource` و `NewsResource`: `schudle_date` → `published_at` → `created_at`
   عشان عمود "تاريخ الإصدار" ما يفضلش `—`
8. **`news:publish-scheduled`** command + `Schedule::command(...)->everyMinute()` في `routes/console.php`.
   ماكانش فيه أي scheduler في المشروع، فالمجدول كان بيفضل مجدول للأبد
9. **الإحصائيات**: `StatisticsController` بقى يرجّع `{key}_stats` فيها `current_month` و
   `previous_month` و `change_percentage` و `trend`. النِسب في الـ dashboard كانت hardcoded
   في الفرونت لأن الـ API ماكانش بيرجّع أي مقارنة. **المفاتيح القديمة اتساب زي ما هي أرقام**
10. **`publisher_id`**: عمود varchar بيتخزن فيه ID أو اسم جهة. أُضيف accessor `publisher_name`
    بيرجّع القيمة الخام لو العلاقة ماحلّتش (بدل `null`)، و`LIFISeeder` بقى يحط user id صحيح

**ملاحظات مهمة:**
- `is_published` و `is_featured` **مقصود** إنها تفضل `0|1` مش boolean عشان متكسرش الفرونت
- الـ scheduler محتاج cron على السيرفر: `* * * * * cd /path && php artisan schedule:run >> /dev/null 2>&1`
- **التصدير (CSV/Excel/PDF) لسه مش متعمول** — مفيش routes ولا مكتبات (`maatwebsite/excel`,
  `barryvdh/laravel-dompdf`)، و`CategoryController@export` / `TagController@export` بينادوا
  ميثودز مش موجودة في الـ services

### داتا تجريبية في الإنتاج
`LIFISeeder` بيعمل 60 صف لكل حالة (240 إجمالًا) — ده سبب ظهور `60 / 60 / 60` في الـ dashboard.
الأرقام صحيحة بس البيانات وهمية.
