﻿# 📋 قائمة مهام المرحلة السادسة — Apex Logistics Dashboard
# (التتبع الحي، التوزيع الآلي، الإشعارات الفورية — 9 تيكتات هندسية)

> **القاعدة الذهبية:** لا تبدأ تيكت جديدة حتى تُغلق التيكت السابقة بالكامل وتتأكد من نجاح **كل** نقاط الفحص فيها.
>
> **مسار المشروع:** `D:\Important Projects\Apex_Logistics\Dashboard`
>
> **الملفات المرجعية اللي لازم تفتحها قبل ما تشتغل:**
> - [DASHBOARD_ROADMAP.md](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/DASHBOARD_ROADMAP.md)
> - [.env](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/.env)
> - [compose.yaml](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/compose.yaml)
> - [OrderObserver.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Observers/OrderObserver.php)
> - [CreateOrder.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Pages/CreateOrder.php)
> - [OrderStateMachine.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Services/OrderStateMachine.php)
> - [AdminPanelProvider.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Providers/Filament/AdminPanelProvider.php)
> - [GeneralSettings.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Settings/GeneralSettings.php)
> - [config/services.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/config/services.php)

---

> [!WARNING]
> ## ⛔ قاعدة حديدية — ممنوع الاسكريبتات
>
> **ممنوع استخدام اسكريبتات لحذف كود أو إضافة كود أو أي اسكريبت ممكن ولو بنسبة ضئيلة يخرب حاجة.**
> كل التعديلات لازم تتعمل يدوياً بفهم كامل لكل سطر.

---

> [!CAUTION]
> ## 📝 قاعدة التوثيق الإلزامية (Mandatory Changelog Rule)
>
> **بعد إغلاق كل تيكت، لازم تحدّث ملف التوثيق:**
> **المسار:** `D:\Important Projects\Apex_Logistics\Dashboard\PHASE6_CHANGELOG.md`
>
> **كل تغيير بيتسجل بالحرف الواحد — بدون استثناء:**
> - ✏️ كل **ملف جديد** اتأنشأ → سجّل اسمه الكامل ومحتواه بالكامل.
> - 🔧 كل **سطر اتعدّل** في ملف موجود → سجّل الملف ورقم السطر والكود القديم والكود الجديد (بصيغة diff).
> - ➕ كل **سطر اتضاف** → سجّل الملف ورقم السطر والكود المضاف.
> - ➖ كل **سطر اتحذف** → سجّل الملف ورقم السطر والكود المحذوف.
> - 📦 كل **حزمة اتثبتت** → سجّل اسمها وأمر التثبيت.
> - 🗃️ كل **migration اتعملت** → سجّل اسم الملف وكل الأعمدة والقيود.
> - ⚙️ كل **أمر terminal اتشغّل** → سجّل الأمر ونتيجته.
>
> **الصيغة المطلوبة لكل تيكت في الـ Changelog:**
> ```markdown
> # 🎟️ التيكت [رقم]: [الاسم]
> **تاريخ البدء:** YYYY-MM-DD HH:MM
> **تاريخ الانتهاء:** YYYY-MM-DD HH:MM
>
> ## الملفات الجديدة (New Files)
> ### `path/to/file.php`
> ```php
> // المحتوى الكامل للملف
> ```
>
> ## الملفات المعدّلة (Modified Files)
> ### `path/to/existing_file.php`
> ```diff
> - الكود القديم (سطر XX)
> + الكود الجديد (سطر XX)
> ```
>
> ## الأوامر المُنفّذة (Commands Executed)
> | الأمر | النتيجة |
> |-------|--------|
> | `composer require xyz` | ✅ نجح |
>
> ## نتائج الفحص (Verification Results)
> | الفحص | النتيجة |
> |-------|--------|
> | فحص X | ✅ |
> ```
>
> **⚠️ ممنوع إغلاق أي تيكت بدون تحديث هذا الملف. هذا ليس اختياري.**

---

## 🗺️ خريطة الاعتماديات بين التيكتات

```
التيكت 1 (Redis)
    ├──→ التيكت 2 (Reverb + Events) ──→ التيكت 3 (Broadcasting Wiring) ──→ التيكت 5 (Live Map)
    │                                                                   └──→ التيكت 6 (Auto-Dispatch) ──→ التيكت 7 (Dispatch Protection)
    ├──→ التيكت 4 (FCM) — مستقل عن 2 و 3، يعتمد على 1 فقط
    └──→ التيكت 8 (Widgets) ──→ التيكت 9 (Charts)
```

> [!TIP]
> **التيكتات 4, 8 يمكن تشغيلها بالتوازي مع 2, 3.** لكن لو أنت شخص واحد، نفّذ بالترتيب 1→2→3→4→5→6→7→8→9.

---

> [!CAUTION]
> ## ⚠️ متطلبات مسبقة — لازم تتأكد منها قبل أي تيكت
>
> **1. علاقة `driver()` في `User.php`:**
> الملف الحالي [User.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Models/User.php) — 68 سطر.
> عدة أماكن في Phase 6 تستخدم `$user->driver` (مثل Channel Authorization وFCM).
> **افتح الملف وتأكد إن العلاقة دي موجودة:**
> ```php
> public function driver(): \Illuminate\Database\Eloquent\Relations\HasOne
> {
>     return $this->hasOne(\App\Models\Driver::class);
> }
> ```
> **لو مش موجودة** → أضفها قبل `}` الأخير في الكلاس. **لو موجودة** → لا تعدل شيء.
>
> **2. عمود `version` في جدول `orders`:**
> التيكت السادسة تستخدم `$order->version`. تأكد إن العمود ده موجود في الداتابيز.
> ```bash
> php artisan tinker --execute="echo Schema::hasColumn('orders', 'version') ? 'EXISTS' : 'MISSING';"
> ```
> لو `MISSING` → هيتم التعامل معاه بـ `?? 1` (fallback آمن).

---
---

# 🎟️ التيكت الأولى: تشغيل Redis عبر Docker وتحويل Cache/Queue

> **الهدف:** تشغيل Redis من Docker Compose الموجود أصلاً في المشروع، تثبيت `predis/predis`، تحويل الـ Cache والـ Queue من `file`/`sync` إلى `redis`.
>
> **ليه بنعمل ده؟** الـ Cache الحالي `file` بطيء ومش بيدعم Broadcasting. الـ Queue الحالي `sync` معناه إن كل Job بيشتغل لحظياً ويبطّئ الريكوست. Redis بيحل المشكلتين دول + بيديك Pub/Sub لـ WebSockets.
>
> **الاعتماديات:** لا شيء. هذه التيكت مستقلة تماماً.
> **التقدير الزمني:** ساعة.
>
> **⚠️ قبل ما تبدأ:** تأكد إن Docker Desktop شغال على جهازك.

---

## 1.1 — تشغيل Redis من Docker Compose

> **معلومة مهمة:** ملف [compose.yaml](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/compose.yaml) في المشروع **فيه service اسمه `redis` أصلاً**! يعني مش محتاجين ننشئ حاجة. بس نشغله.

### 1.1.1 تشغيل Redis container
- [x] **الأمر:**
  ```bash
  cd "D:\Important Projects\Apex_Logistics\Dashboard"
  docker compose up -d redis
  ```
- [x] **⚠️ ملاحظة:** بنشغل `redis` **بس** — مش محتاجين `laravel.test` ولا `mysql`. لو عاوز تشغل كل الـ services اكتب `docker compose up -d` بس إحنا مش محتاجين ده دلوقتي.

### 1.1.2 تأكد إن Redis شغال
- [x] **الأمر:**
  ```bash
  docker compose exec redis redis-cli ping
  ```
- [x] **النتيجة المتوقعة:** `PONG`
- [x] **لو طبع Error:** يبقى Redis مش شغال. شغّل `docker compose logs redis` وشوف اللوج. لو Port 6379 مشغول: `docker compose down` الأول ثم `docker compose up -d redis`.

---

## 1.2 — تثبيت حزمة predis/predis

> **ليه predis مش phpredis؟** لأن `phpredis` محتاج تثبيت PHP Extension من PECL وده معقد على Windows. `predis` هو Pure PHP — بس `composer require` وخلاص. على Production (Linux) ممكن نحوّل لـ `phpredis` للسرعة القصوى.

### 1.2.1 تثبيت الحزمة
- [x] **الأمر:**
  ```bash
  cd "D:\Important Projects\Apex_Logistics\Dashboard"
  composer require predis/predis
  ```
- [x] **✅ فحص:** افتح `composer.json` → ابحث عن `"predis/predis"` في `require`. لازم يكون موجود.

---

## 1.3 — تعديل `.env`

> **⚠️ اقرا بعناية!** الملف [.env](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/.env) فيه 66 سطر حالياً. هنعدل 4 أسطر بس.

### 1.3.1 تغيير REDIS_CLIENT (السطر 45)
- [x] **الملف:** `.env`
- [x] **الموجود حالياً (سطر 45):**
  ```
  REDIS_CLIENT=phpredis
  ```
- [x] **غيّره لـ:**
  ```
  REDIS_CLIENT=predis
  ```
- [x] **ليه؟** لأننا ثبتنا `predis` مش `phpredis`.

### 1.3.2 تغيير REDIS_HOST (السطر 46)
- [x] **الموجود حالياً (سطر 46):**
  ```
  REDIS_HOST=redis
  ```
- [x] **غيّره لـ:**
  ```
  REDIS_HOST=127.0.0.1
  ```
- [x] **ليه؟** `redis` هو اسم Docker service وبيشتغل بس جوه شبكة Docker (لما تشغل `laravel.test` عبر Sail). إحنا بنشغل `php artisan serve` من خارج Docker، فمحتاجين `127.0.0.1` عشان نتصل بـ Redis المعروض على `localhost:6379`.

### 1.3.3 تغيير CACHE_STORE (السطر 40)
- [x] **الموجود حالياً (سطر 40):**
  ```
  CACHE_STORE=file
  ```
- [x] **غيّره لـ:**
  ```
  CACHE_STORE=redis
  ```

### 1.3.4 تغيير QUEUE_CONNECTION (السطر 38)
- [x] **الموجود حالياً (سطر 38):**
  ```
  QUEUE_CONNECTION=sync
  ```
- [x] **غيّره لـ:**
  ```
  QUEUE_CONNECTION=redis
  ```

### 1.3.5 **⚠️ ممنوع تغيير:**
- [x] **لا تلمس** `SESSION_DRIVER=database` (سطر 30). لو غيّرته لـ redis هتتطرد كل الجلسات.
- [x] **لا تلمس** `DB_CONNECTION=sqlite` (سطر 23).
- [x] **لا تلمس** `BROADCAST_CONNECTION=log` (سطر 36) — ده هنغيره في التيكت الثانية.

---

## 1.4 — مسح الكاش القديم

> **ليه؟** لأن الكاش القديم اتخزن كملفات في `storage/framework/cache`. بعد التحويل لـ Redis، الكاش القديم مش هيضر بس الأفضل ننظفه.

- [x] **الأمر:**
  ```bash
  php artisan cache:clear
  php artisan config:clear
  ```
- [x] **النتيجة المتوقعة:** `Cache cleared successfully.` و `Configuration cache cleared!`

---

## ✅ فحص نهائي للتيكت الأولى

- [x] **فحص 1 — Redis متصل:**
  ```bash
  php artisan tinker --execute="Cache::put('phase6_test', 'redis_works', 60); echo Cache::get('phase6_test');"
  ```
  **لازم يطبع:** `redis_works`

- [x] **فحص 2 — Queue شغال عبر Redis:**
  ```bash
  php artisan queue:work --once --stop-when-empty
  ```
  **لازم يشتغل بدون Error** (حتى لو مفيش Jobs).

- [x] **فحص 3 — Lock شغال عبر Redis:**
  ```bash
  php artisan tinker --execute="Cache::lock('test_lock', 10)->get(); echo 'Lock acquired via Redis';"
  ```
  **لازم يطبع:** `Lock acquired via Redis`

- [x] **فحص 4 — التأكد من Redis فعلاً (مش file):**
  ```bash
  docker compose exec redis redis-cli KEYS "*phase6*"
  ```
  **لازم يرجع key فيه `phase6_test`** (ده الـ Cache اللي حطيناه في فحص 1).

- [x] **فحص 5 — الداشبورد شغال:**
  ```bash
  php artisan serve
  ```
  افتح `http://localhost:8000/admin` → لازم يفتح بدون أخطاء.

## 📝 توثيق التيكت الأولى (إلزامي)
- [x] **أنشئ `PHASE6_CHANGELOG.md`** في المسار `D:\Important Projects\Apex_Logistics\Dashboard\PHASE6_CHANGELOG.md`
- [x] سجّل أمر `docker compose up -d redis` ونتيجته.
- [x] سجّل أمر `composer require predis/predis` ونتيجته.
- [x] سجّل كل تعديل في `.env` بصيغة diff (4 أسطر: `REDIS_CLIENT`, `REDIS_HOST`, `CACHE_STORE`, `QUEUE_CONNECTION`).
- [x] سجّل كل أوامر الفحص الخمسة ونتائجها.

---
---

# 🎟️ التيكت الثانية: بنية WebSockets (Reverb + Events + Channels + Echo)

> **الهدف:** تسطيب Laravel Reverb (WebSocket server)، إنشاء 3 Broadcasting Events، تسجيل Channel Authorization، إعداد Laravel Echo في الـ Frontend.
>
> **ليه بنعمل ده؟** عشان الداشبورد يعرض التحديثات لحظياً بدون ما المستخدم يعمل Refresh. لما مندوب يحدّث موقعه أو طلب يتغيّر حالته، المشرف يشوف التغيير فوراً.
>
> **الاعتماديات:** التيكت الأولى (Redis لازم يكون شغال).
> **التقدير الزمني:** 3 ساعات.

---

## 2.1 — تثبيت Laravel Reverb

### 2.1.1 تثبيت الحزمة
- [x] **الأمر:**
  ```bash
  cd "D:\Important Projects\Apex_Logistics\Dashboard"
  composer require laravel/reverb
  ```

### 2.1.2 تشغيل أمر التسطيب
- [x] **الأمر:**
  ```bash
  php artisan reverb:install
  ```
- [x] **⚠️ هذا الأمر بيعمل حاجات كتيرة أوتوماتيك:**
  1. يُنشئ ملف `config/reverb.php`
  2. يضيف متغيرات في `.env` (مثل `REVERB_APP_ID`, `REVERB_APP_KEY`, `REVERB_APP_SECRET`, `REVERB_HOST`, `REVERB_PORT`, `REVERB_SCHEME`)
  3. يضيف متغيرات Vite (مثل `VITE_REVERB_APP_KEY`, `VITE_REVERB_HOST`, `VITE_REVERB_PORT`, `VITE_REVERB_SCHEME`)
  4. قد يُحدّث `config/broadcasting.php`
- [x] **✅ بعد التشغيل:** افتح `.env` وتأكد إن المتغيرات الجديدة موجودة.

### 2.1.3 تغيير BROADCAST_CONNECTION
- [x] **الملف:** `.env`
- [x] **الموجود حالياً (سطر 36):**
  ```
  BROADCAST_CONNECTION=log
  ```
- [x] **غيّره لـ:**
  ```
  BROADCAST_CONNECTION=reverb
  ```
- [x] **ليه؟** عشان Laravel يبث الأحداث عبر Reverb بدل ما يكتبها في الـ log file.

### 2.1.4 تأكد إن Reverb يشتغل
- [x] **الأمر (في terminal منفصل — خليه مفتوح):**
  ```bash
  php artisan reverb:start
  ```
- [x] **النتيجة المتوقعة:** يطبع حاجة زي:
  ```
  Starting server on 0.0.0.0:8080
  ```
  لو ظهر Error على Port 8080، افتح `.env` وغيّر `REVERB_PORT=8080` لـ `REVERB_PORT=6001` (أو أي port فاضي).
- [x] **⚠️ هام:** سيرفر Reverb لازم يفضل شغال طول ما الداشبورد شغال. ده WebSocket server منفصل عن `php artisan serve`.

---

## 2.2 — تثبيت Frontend Dependencies

### 2.2.1 تثبيت laravel-echo و pusher-js
- [x] **الأمر:**
  ```bash
  npm install --save-dev laravel-echo pusher-js
  ```
- [x] **⚠️ ليه pusher-js مع Reverb؟** لأن Laravel Echo بيستخدم Pusher Protocol كـ transport layer حتى مع Reverb. Reverb بيتكلم نفس البروتوكول.
- [x] **✅ فحص:** افتح `package.json` → لازم تلاقي `"laravel-echo"` و `"pusher-js"` في `devDependencies`.

---

## 2.3 — إنشاء Echo Setup

### 2.3.1 إنشاء `resources/js/echo.js`
- [x] **ملف جديد:** `resources/js/echo.js`
- [x] **المحتوى الكامل:**
  ```javascript
  import Echo from 'laravel-echo';

  import Pusher from 'pusher-js';
  window.Pusher = Pusher;

  window.Echo = new Echo({
      broadcaster: 'reverb',
      key: import.meta.env.VITE_REVERB_APP_KEY,
      wsHost: import.meta.env.VITE_REVERB_HOST,
      wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
      wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
      forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
      enabledTransports: ['ws', 'wss'],
  });
  ```

### 2.3.2 تعديل `resources/js/bootstrap.js`
- [x] **الملف الحالي** (`resources/js/bootstrap.js`) محتواه:
  ```javascript
  import axios from 'axios';
  window.axios = axios;

  window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
  ```
- [x] **أضف في آخر الملف (بعد آخر سطر):**
  ```javascript

  import './echo';
  ```
- [x] **⚠️ متحذفش أي حاجة موجودة.** بس أضف السطر ده في الآخر.

---

## 2.4 — إنشاء الـ Broadcasting Events

> **ليه Events وليه 3؟**
> - `OrderStatusChanged` → لما طلب يتغيّر حالته (assigned, delivered, cancelled, etc.)
> - `NewOrderCreated` → لما طلب جديد يتأنشأ
> - `DriverLocationUpdated` → لما مندوب يبعث موقعه الجغرافي
>
> كل Event بيتبث (broadcast) على قنوات (Channels) خاصة. المشرف يسمع كل حاجة. المندوب يسمع قناته بس.

### 2.4.1 إنشاء `app/Events/OrderStatusChanged.php`
- [x] **ملف جديد:** `app/Events/OrderStatusChanged.php`
- [x] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Events;

  use Illuminate\Broadcasting\InteractsWithSockets;
  use Illuminate\Broadcasting\PrivateChannel;
  use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
  use Illuminate\Foundation\Events\Dispatchable;
  use Illuminate\Queue\SerializesModels;

  /**
   * يُبث لحظياً عند تغيير حالة أي طلب.
   * ShouldBroadcastNow = يتبث فوراً بدون Queue (عشان التحديث يوصل لحظياً).
   */
  class OrderStatusChanged implements ShouldBroadcastNow
  {
      use Dispatchable, InteractsWithSockets, SerializesModels;

      public function __construct(
          public int $orderId,
          public string $newStatus,
          public string $oldStatus,
          public ?int $driverId,
          public ?int $batchId,
          public string $orderType,
      ) {}

      /**
       * القنوات اللي الـ Event هيتبث عليها.
       * - admin.dashboard: المشرف يشوف كل التغييرات.
       * - driver.{id}: المندوب المعين يشوف التغييرات على طلباته بس.
       */
      public function broadcastOn(): array
      {
          $channels = [
              new PrivateChannel('admin.dashboard'),
          ];

          if ($this->driverId) {
              $channels[] = new PrivateChannel("driver.{$this->driverId}");
          }

          return $channels;
      }

      /**
       * اسم الـ Event في الـ Frontend.
       * في Echo هتسمع عليه كده: .listen('.order.status.changed', ...)
       */
      public function broadcastAs(): string
      {
          return 'order.status.changed';
      }

      /**
       * البيانات اللي بتتبعت مع الـ Event.
       */
      public function broadcastWith(): array
      {
          return [
              'order_id' => $this->orderId,
              'new_status' => $this->newStatus,
              'old_status' => $this->oldStatus,
              'driver_id' => $this->driverId,
              'batch_id' => $this->batchId,
              'order_type' => $this->orderType,
              'timestamp' => now()->toISOString(),
          ];
      }
  }
  ```

### 2.4.2 إنشاء `app/Events/NewOrderCreated.php`
- [x] **ملف جديد:** `app/Events/NewOrderCreated.php`
- [x] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Events;

  use Illuminate\Broadcasting\InteractsWithSockets;
  use Illuminate\Broadcasting\PrivateChannel;
  use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
  use Illuminate\Foundation\Events\Dispatchable;
  use Illuminate\Queue\SerializesModels;

  /**
   * يُبث لحظياً عند إنشاء طلب جديد.
   * المشرف بس هو اللي يشوف ده.
   */
  class NewOrderCreated implements ShouldBroadcastNow
  {
      use Dispatchable, InteractsWithSockets, SerializesModels;

      public function __construct(
          public int $orderId,
          public string $orderType,
          public string $customerName,
      ) {}

      public function broadcastOn(): array
      {
          return [
              new PrivateChannel('admin.dashboard'),
          ];
      }

      public function broadcastAs(): string
      {
          return 'order.created';
      }

      public function broadcastWith(): array
      {
          return [
              'order_id' => $this->orderId,
              'order_type' => $this->orderType,
              'customer_name' => $this->customerName,
              'timestamp' => now()->toISOString(),
          ];
      }
  }
  ```

### 2.4.3 إنشاء `app/Events/DriverLocationUpdated.php`
- [x] **ملف جديد:** `app/Events/DriverLocationUpdated.php`
- [x] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Events;

  use Illuminate\Broadcasting\InteractsWithSockets;
  use Illuminate\Broadcasting\PrivateChannel;
  use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
  use Illuminate\Foundation\Events\Dispatchable;
  use Illuminate\Queue\SerializesModels;

  /**
   * يُبث لحظياً عند تحديث موقع مندوب.
   * ⚠️ المشرف بس يشوف مواقع المناديب — ده معلومات حساسة.
   */
  class DriverLocationUpdated implements ShouldBroadcastNow
  {
      use Dispatchable, InteractsWithSockets, SerializesModels;

      public function __construct(
          public int $driverId,
          public float $latitude,
          public float $longitude,
          public string $driverName,
          public ?int $activeBatchId,
          public int $activeOrdersCount,
      ) {}

      public function broadcastOn(): array
      {
          return [
              new PrivateChannel('admin.dashboard'),
          ];
      }

      public function broadcastAs(): string
      {
          return 'driver.location.updated';
      }

      public function broadcastWith(): array
      {
          return [
              'driver_id' => $this->driverId,
              'latitude' => $this->latitude,
              'longitude' => $this->longitude,
              'driver_name' => $this->driverName,
              'active_batch_id' => $this->activeBatchId,
              'active_orders_count' => $this->activeOrdersCount,
              'timestamp' => now()->toISOString(),
          ];
      }
  }
  ```

---

## 2.5 — إنشاء Channel Authorization

> **ليه Channels؟** لأن الـ Events بتتبث على Private Channels. يعني اللي يسمع عليها لازم يكون authenticated ومصرّح له. الملف `routes/channels.php` هو اللي بيحدد مين يسمع على أنهي Channel.

### 2.5.1 تعديل `routes/channels.php`
- [x] **الملف موجود أصلاً** ومحتواه:
  ```php
  <?php

  use Illuminate\Support\Facades\Broadcast;

  Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
      return (int) $user->id === (int) $id;
  });
  ```
- [x] **أضف بعد آخر `});` (بعد السطر الأخير):**
  ```php

  /*
  |--------------------------------------------------------------------------
  | Phase 6: Real-time Channels
  |--------------------------------------------------------------------------
  */

  /**
   * قناة لوحة المشرف — المشرف (super_admin) بس يقدر يسمع.
   * عليها: تغييرات الطلبات، مواقع المناديب، طلبات جديدة.
   */
  Broadcast::channel('admin.dashboard', function ($user) {
      return $user->hasRole('super_admin');
  });

  /**
   * قناة خاصة بكل مندوب — المندوب يسمع قناته بس.
   * عليها: تحديثات طلباته، إشعارات التعيين.
   */
  Broadcast::channel('driver.{id}', function ($user, $id) {
      return $user->driver && $user->driver->id === (int) $id;
  });

  /**
   * قناة خاصة بكل طلب — المشرف أو المندوب المعين بس.
   */
  Broadcast::channel('order.{id}', function ($user, $id) {
      if ($user->hasRole('super_admin')) {
          return true;
      }

      if ($user->driver) {
          return \App\Models\Order::where('id', $id)
              ->where('driver_id', $user->driver->id)
              ->exists();
      }

      return false;
  });
  ```

---

## ✅ فحص نهائي للتيكت الثانية

- [x] **فحص 1 — Reverb يشتغل:**
  ```bash
  php artisan reverb:start
  ```
  لازم يطبع "Starting server..." بدون Error. (اقفله بعد الفحص بـ Ctrl+C).

- [x] **فحص 2 — Events تتبث بدون Error:**
  ```bash
  php artisan tinker --execute="broadcast(new App\Events\NewOrderCreated(orderId: 999, orderType: 'standard', customerName: 'Test'));"
  ```
  لازم يتنفذ بدون Exception. (لو Reverb مش شغال هيطلع Connection Error — شغله أولاً).

- [x] **فحص 3 — Vite يبني:**
  ```bash
  npm run build
  ```
  لازم ينجح بدون Error.

- [x] **فحص 4 — الملفات موجودة:**
  - `app/Events/OrderStatusChanged.php` ✅
  - `app/Events/NewOrderCreated.php` ✅
  - `app/Events/DriverLocationUpdated.php` ✅
  - `resources/js/echo.js` ✅

- [x] **فحص 5 — الداشبورد شغال:** `php artisan serve` → بدون أخطاء.

## 📝 توثيق التيكت الثانية (إلزامي)
- [x] سجّل أمر `composer require laravel/reverb` ونتيجته.
- [x] سجّل أمر `php artisan reverb:install` وكل الملفات اللي أنشأها/عدّلها.
- [x] سجّل تغيير `BROADCAST_CONNECTION` في `.env`.
- [x] سجّل أمر `npm install --save-dev laravel-echo pusher-js` ونتيجته.
- [x] سجّل كل ملف Event (3 ملفات) بالمحتوى الكامل.
- [x] سجّل ملف `resources/js/echo.js` بالمحتوى الكامل.
- [x] سجّل التعديل على `resources/js/bootstrap.js` بصيغة diff.
- [x] سجّل التعديل على `routes/channels.php` بصيغة diff.

---
---

# 🎟️ التيكت الثالثة: دمج Broadcasting مع الكود الحالي (Wiring)

> **الهدف:** ربط الـ Events اللي أنشأناها في التيكت 2 بالكود الموجود فعلاً — الـ Observers والـ Controllers.
>
> **ليه بنعمل ده؟** الـ Events مش هتعمل حاجة لوحدها. لازم حد ينادي `broadcast(new EventXyz(...))` في اللحظة الصحيحة.
>
> **الاعتماديات:** التيكتان الأولى والثانية.
> **التقدير الزمني:** ساعتين.

---

## 3.1 — ربط تغيير حالة الطلب بالـ Broadcasting

### 3.1.1 تعديل `app/Observers/OrderObserver.php`

> **⚠️ اقرا الملف الحالي بعناية قبل ما تعدل!**
> **الملف الحالي** ([OrderObserver.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Observers/OrderObserver.php)) — 62 سطر.
> الدالة `updated()` (سطر 20-36) فيها بلوك واحد: استرداد المخزون عند الإلغاء.
> إحنا هنضيف **بعد** هذا البلوك (وليس جواه) — Broadcasting لأي تغيير حالة.

- [x] **المطلوب:** في دالة `updated()` (سطر 20)، **أضف الكود التالي بعد القوس `}` بتاع الـ `if` الموجود (سطر 35) وقبل القوس `}` بتاع الدالة (سطر 36):**

  ```php

          // === Phase 6: Broadcasting ===
          if ($order->isDirty('status')) {
              $oldStatus = $order->getOriginal('status');

              broadcast(new \App\Events\OrderStatusChanged(
                  orderId: $order->id,
                  newStatus: $order->status->value,
                  oldStatus: $oldStatus instanceof \App\Enums\OrderStatus
                      ? $oldStatus->value
                      : (string) $oldStatus,
                  driverId: $order->driver_id,
                  batchId: $order->batch_id,
                  orderType: $order->order_type->value,
              ));
          }
  ```

- [x] **⚠️ ملاحظة دقيقة جداً عن `getOriginal()`:**
  - `$order->status` بعد التعديل = Enum Object (لأن Model فيه Cast).
  - `$order->getOriginal('status')` = **ممكن** يرجع Enum أو String حسب نسخة Laravel.
  - عشان كده الكود أعلاه بيتعامل مع الحالتين: `instanceof` لو Enum، أو `(string)` لو String.
  - **متستخدمش** `$order->getOriginal('status')->value` مباشرة — لو رجع String هيرمي Error.

- [x] **الملف بعد التعديل لازم يبقى شكله كده (الدالة `updated` بس):**
  ```php
  public function updated(Order $order): void
  {
      if ($order->isDirty('status') && $order->status === \App\Enums\OrderStatus::Cancelled && $order->order_type === \App\Enums\OrderType::Standard) {
          \Illuminate\Support\Facades\DB::transaction(function () use ($order) {
              $items = $order->items->sortBy('product_id');
              foreach ($items as $item) {
                  $product = \App\Models\Product::where('id', $item->product_id)->lockForUpdate()->first();
                  if ($product) {
                      $product->stock += $item->quantity;
                      $product->save();
                  }
              }
          });
      }

      // === Phase 6: Broadcasting ===
      if ($order->isDirty('status')) {
          $oldStatus = $order->getOriginal('status');

          broadcast(new \App\Events\OrderStatusChanged(
              orderId: $order->id,
              newStatus: $order->status->value,
              oldStatus: $oldStatus instanceof \App\Enums\OrderStatus
                  ? $oldStatus->value
                  : (string) $oldStatus,
              driverId: $order->driver_id,
              batchId: $order->batch_id,
              orderType: $order->order_type->value,
          ));
      }
  }
  ```

---

## 3.2 — ربط إنشاء الطلب بالـ Broadcasting

### 3.2.1 تعديل `app/Filament/Resources/Orders/Pages/CreateOrder.php`

> **الملف الحالي** ([CreateOrder.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Pages/CreateOrder.php)) — 87 سطر.
> الدالة `afterCreate()` (سطر 16-23) حالياً بتعمل PricingManager::snapshot للطلبات Standard بس.

- [x] **المطلوب:** أضف **بعد** سطر `$order->save();` (سطر 21) وقبل القوس `}` بتاع الـ `if` (سطر 22):
  ```php
  ```
  ثم **أضف كود Broadcasting بعد الـ `if` block بالكامل (بعد سطر 22) وقبل القوس `}` بتاع الدالة (سطر 23):**
  ```php

          // === Phase 6: Broadcasting ===
          broadcast(new \App\Events\NewOrderCreated(
              orderId: $order->id,
              orderType: $order->order_type->value,
              customerName: $order->customer?->name ?? 'غير محدد',
          ));
  ```

- [x] **الدالة `afterCreate()` بعد التعديل لازم تبقى:**
  ```php
  protected function afterCreate(): void
  {
      $order = $this->record;
      if ($order->order_type === \App\Enums\OrderType::Standard) {
          app(\App\Services\PricingManager::class)->snapshot($order);
          $order->save();
      }

      // === Phase 6: Broadcasting ===
      broadcast(new \App\Events\NewOrderCreated(
          orderId: $order->id,
          orderType: $order->order_type->value,
          customerName: $order->customer?->name ?? 'غير محدد',
      ));
  }
  ```
- [x] **⚠️ ملاحظة:** الـ `broadcast()` **برا** الـ `if` — يعني بيتبث لكل أنواع الطلبات (Standard و Errand).

---

## 3.3 — ربط تحديث موقع المندوب بالـ Broadcasting

### 3.3.1 تعديل `app/Http/Controllers/Api/Driver/LocationController.php`

> **⚠️ قبل ما تعدل:** افتح الملف واقرأه بالكامل أولاً. تأكد من الهيكل الحالي.
> الملف الحالي (31 سطر) فيه دالة `update()` بتستخدم `Redis::setex()` مباشرة.
> **⚠️ ملاحظة مهمة:** الكود الحالي يستخدم `$request->latitude` (من Form Request) وليس `$validated['latitude']`.
> كمان الكود بيخزن `$driverId = $request->user()->driver->id` — مفيش متغير `$driver` جاهز.

- [x] **المطلوب (خطوة 1):** في بداية الدالة `update()` — **غيّر** السطر:
  ```php
  $driverId = $request->user()->driver->id;
  ```
  **لـ:**
  ```php
  $driver = $request->user()->driver;
  $driverId = $driver->id;
  ```
  > **ليه؟** عشان نقدر نستخدم `$driver` في الـ broadcast بعد كده.

- [x] **المطلوب (خطوة 2):** أضف **بعد** `Redis::setex(...)` block وقبل الـ `return`:
  ```php

          // === Phase 6: Broadcasting ===
          broadcast(new \App\Events\DriverLocationUpdated(
              driverId: $driver->id,
              latitude: (float) $request->latitude,
              longitude: (float) $request->longitude,
              driverName: $driver->name ?? "Driver #{$driver->id}",
              activeBatchId: $driver->deliveryBatches()
                  ->where('status', \App\Enums\BatchStatus::Active)
                  ->first()?->id,
              activeOrdersCount: $driver->orders()
                  ->whereNotIn('status', [
                      \App\Enums\OrderStatus::Delivered->value,
                      \App\Enums\OrderStatus::Cancelled->value,
                  ])->count(),
          ));
  ```

- [x] **⚠️ ملاحظة:** الـ LocationController بيستخدم `Redis::setex()` مباشرة — مش `Cache::put()`. المهم إن الـ `broadcast()` يتحط **بعد** حفظ الموقع وقبل الـ `return`.

---

## 3.4 — إضافة Echo + Toast Notifications في الداشبورد

### 3.4.1 إنشاء View للإشعارات اللحظية
- [x] **أنشئ المجلد:** `resources/views/filament/hooks/` (لو مش موجود)
- [x] **ملف جديد:** `resources/views/filament/hooks/echo-notifications.blade.php`
- [x] **المحتوى الكامل:**
  ```html
  {{-- Phase 6: Real-time Echo Notifications --}}
  @auth
  @vite(['resources/js/app.js'])
  <script type="module">

      // Wait for Echo to be ready (max 15 seconds)
      let echoAttempts = 0;
      const waitForEcho = setInterval(() => {
          echoAttempts++;
          if (window.Echo) {
              clearInterval(waitForEcho);
              initEchoListeners();
          } else if (echoAttempts > 30) {
              clearInterval(waitForEcho);
              console.warn('Echo: Timed out waiting for Echo to load.');
          }
      }, 500);

      function initEchoListeners() {
          // Listen on admin dashboard channel
          window.Echo.private('admin.dashboard')
              .listen('.order.created', (e) => {
                  showToast(
                      '🆕 طلب جديد',
                      `طلب #${e.order_id} — ${e.order_type === 'errand' ? 'مشوار' : 'عادي'} — ${e.customer_name}`,
                      'info'
                  );
              })
              .listen('.order.status.changed', (e) => {
                  const statusLabels = {
                      'pending': 'معلق',
                      'assigned': 'معيّن',
                      'pending_pricing': 'بانتظار التسعير',
                      'priced': 'مسعّر',
                      'picked_up': 'تم الاستلام',
                      'delivered': 'تم التوصيل',
                      'partially_delivered': 'توصيل جزئي',
                      'cancellation_requested': 'طلب إلغاء',
                      'cancelled': 'ملغي',
                      'disputed': 'نزاع',
                  };
                  showToast(
                      '🔄 تحديث طلب',
                      `طلب #${e.order_id}: ${statusLabels[e.old_status] || e.old_status} ← ${statusLabels[e.new_status] || e.new_status}`,
                      e.new_status === 'cancelled' ? 'danger' : 'success'
                  );
              })
              .listen('.driver.location.updated', (e) => {
                  // Location updates are silent — they update the map only (Ticket 5)
                  // Dispatch a custom event for the map to pick up
                  window.dispatchEvent(new CustomEvent('driver-location-updated', { detail: e }));
              });

          console.log('✅ Echo: Listening on admin.dashboard channel');
      }

      function showToast(title, body, type = 'info') {
          // Browser notification (الطريقة الأضمن والأوثق)
          if (Notification.permission === 'granted') {
              new Notification(title, { body: body });
          } else if (Notification.permission !== 'denied') {
              Notification.requestPermission().then(perm => {
                  if (perm === 'granted') {
                      new Notification(title, { body: body });
                  }
              });
          }

          // Console log as backup
          console.log(`[${type}] ${title}: ${body}`);
      }
  </script>
  @endauth
  ```

### 3.4.2 تعديل `app/Providers/Filament/AdminPanelProvider.php`

> **⚠️ اقرا الملف الحالي أولاً!** الملف فيه `renderHook` واحد أصلاً (بتاع Heartbeat).
> إحنا هنضيف hook تاني — مش هنعدل القديم.

- [x] **المطلوب:** أضف **hook جديد** بعد الـ `renderHook` الموجود (بتاع Heartbeat) وقبل `->authMiddleware`:
  ```php
              // === Phase 6: Echo Real-time Notifications ===
              ->renderHook(
                  \Filament\View\PanelsRenderHook::BODY_END,
                  fn (): string => \Illuminate\Support\Facades\Blade::render(
                      file_get_contents(resource_path('views/filament/hooks/echo-notifications.blade.php'))
                  )
              )
  ```
- [x] **⚠️ ملاحظة:** الـ Heartbeat hook الموجود هو render hook تاني بنفس الـ `BODY_END` — ده عادي. Filament بيسمح بـ multiple hooks على نفس الموقع.

---

## ✅ فحص نهائي للتيكت الثالثة

- [x] **فحص 1 — Broadcasting يشتغل بدون Error:**
  1. شغّل `php artisan reverb:start` (في terminal منفصل)
  2. شغّل `php artisan serve` (في terminal تاني)
  3. في terminal تالت:
     ```bash
     php artisan tinker --execute="broadcast(new App\Events\OrderStatusChanged(orderId: 1, newStatus: 'assigned', oldStatus: 'pending', driverId: null, batchId: null, orderType: 'standard'));"
     ```
  لازم يتنفذ بدون Exception.

- [x] **فحص 2 — الداشبورد شغال:** `php artisan serve` → افتح `/admin` → بدون أخطاء.

- [x] **فحص 3 — Echo script محمّل:**
  افتح الداشبورد في المتصفح → افتح DevTools Console → لازم تلاقي:
  ```
  ✅ Echo: Listening on admin.dashboard channel
  ```
  (لو مش ظاهر: تأكد إنك عملت `npm run build` + Reverb شغال).

## 📝 توثيق التيكت الثالثة (إلزامي)
- [x] سجّل التعديل على `OrderObserver.php` بصيغة diff (الكود المضاف ومكانه بالسطر).
- [x] سجّل التعديل على `CreateOrder.php` بصيغة diff.
- [x] سجّل التعديل على `LocationController.php` بصيغة diff.
- [x] سجّل ملف `echo-notifications.blade.php` بالمحتوى الكامل.
- [x] سجّل التعديل على `AdminPanelProvider.php` بصيغة diff.

---
---

# 🎟️ التيكت الرابعة: Firebase Cloud Messaging (FCM)

> **الهدف:** بناء نظام Push Notifications كامل — من الـ Backend (إرسال) للـ Frontend (استقبال) مروراً بالـ Database (تخزين Tokens).
>
> **ليه بنعمل ده؟** WebSocket (Reverb) بيشتغل بس لما المتصفح مفتوح. لو المشرف أو المندوب قفل المتصفح، مش هيعرف إن في طلب جديد. FCM بيبعث Push Notification حتى لو المتصفح مقفول.
>
> **الاعتماديات:** التيكت الأولى (Redis).
> **التقدير الزمني:** 3 ساعات.
>
> **⚠️ قبل ما تبدأ:** لازم يكون عندك:
> 1. **Firebase Project** — أنشئه من [Firebase Console](https://console.firebase.google.com/).
> 2. **Service Account JSON** — من Firebase Console → Project Settings → Service Accounts → Generate New Private Key.
> 3. **Firebase Config Object** — من Firebase Console → Project Settings → General → Your Apps → Web App → Config.
> 4. **VAPID Key** — من Firebase Console → Project Settings → Cloud Messaging → Web Push Certificates → Generate Key Pair.

---

## 4.1 — إنشاء جدول `fcm_tokens`

### 4.1.1 إنشاء الـ Migration
- [x] **الأمر:**
  ```bash
  php artisan make:migration create_fcm_tokens_table
  ```
- [x] **افتح الملف اللي اتأنشأ** في `database/migrations/` واستبدل محتوى `up()` و `down()`:
  ```php
  public function up(): void
  {
      Schema::create('fcm_tokens', function (Blueprint $table) {
          $table->id();
          $table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
          $table->string('token', 512)->unique();       // FCM device token — فريد لكل جهاز
          $table->string('device_type', 20)->nullable(); // 'web', 'android', 'ios'
          $table->string('device_name')->nullable();     // "Chrome on Windows 10"
          $table->timestamp('last_used_at')->nullable(); // آخر مرة اتبعت عليه إشعار
          $table->timestamps();

          $table->index('user_id'); // بحث سريع عن tokens المستخدم
      });
  }

  public function down(): void
  {
      Schema::dropIfExists('fcm_tokens');
  }
  ```
- [x] **شغّل الـ migration:**
  ```bash
  php artisan migrate
  ```
- [x] **✅ فحص:**
  ```bash
  php artisan tinker --execute="echo Schema::hasTable('fcm_tokens') ? 'TABLE EXISTS' : 'MISSING';"
  ```
  لازم يطبع `TABLE EXISTS`.

---

## 4.2 — إنشاء Model `FcmToken`

### 4.2.1 إنشاء `app/Models/FcmToken.php`
- [x] **ملف جديد:** `app/Models/FcmToken.php`
- [x] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Models;

  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Database\Eloquent\Relations\BelongsTo;

  /**
   * يُخزّن FCM Token لكل جهاز مسجّل.
   * لا SoftDeletes — لو التوكن انتهى نحذفه مباشرة.
   */
  class FcmToken extends Model
  {
      protected $fillable = [
          'user_id',
          'token',
          'device_type',
          'device_name',
          'last_used_at',
      ];

      protected $casts = [
          'last_used_at' => 'datetime',
      ];

      public function user(): BelongsTo
      {
          return $this->belongsTo(User::class);
      }
  }
  ```

---

## 4.3 — إضافة علاقة `fcmTokens()` في User Model

### 4.3.1 تعديل `app/Models/User.php`
- [x] **⚠️ اقرا الملف أولاً** وحدد مكان آخر علاقة (relationship) موجودة.
- [x] **أضف** في آخر الـ class (قبل القوس `}` الأخير) الدالة التالية:
  ```php

      /**
       * FCM tokens المسجلة لهذا المستخدم (أجهزة متعددة).
       */
      public function fcmTokens(): \Illuminate\Database\Eloquent\Relations\HasMany
      {
          return $this->hasMany(\App\Models\FcmToken::class);
      }
  ```
- [x] **⚠️ متحذفش أي حاجة موجودة.** أضف الدالة بس.

---

## 4.4 — إضافة FCM Config

### 4.4.1 تعديل `config/services.php`
- [x] **الملف الحالي** ([config/services.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/config/services.php)) — 39 سطر. آخر entry هو `slack`.
- [x] **أضف بعد array الـ `slack` (بعد سطر 36) وقبل الـ `];` الأخير (سطر 38):**
  ```php

      'fcm' => [
          'credentials_path' => env('FCM_CREDENTIALS_PATH', storage_path('app/firebase/service-account.json')),
          'project_id' => env('FCM_PROJECT_ID'),
      ],
  ```

### 4.4.2 إضافة متغيرات FCM في `.env`
- [x] **أضف في آخر ملف `.env`:**
  ```
  FCM_PROJECT_ID=your-firebase-project-id
  FCM_CREDENTIALS_PATH="storage/app/firebase/service-account.json"
  ```
- [x] **ضع ملف `service-account.json`** (اللي نزّلته من Firebase Console) في المسار:
  ```
  D:\Important Projects\Apex_Logistics\Dashboard\storage\app\firebase\service-account.json
  ```
- [x] **⚠️ أمان:** أضف المسار ده في `.gitignore`:
  ```
  storage/app/firebase/
  ```

---

## 4.5 — إنشاء FCM Notification Service

### 4.5.1 إنشاء `app/Services/FcmNotificationService.php`
- [x] **ملف جديد:** `app/Services/FcmNotificationService.php`
- [x] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Services;

  use App\Models\FcmToken;
  use App\Models\User;
  use Illuminate\Support\Facades\Http;
  use Illuminate\Support\Facades\Log;

  /**
   * خدمة إرسال Push Notifications عبر Firebase Cloud Messaging (FCM) HTTP v1 API.
   *
   * تعمل بدون أي حزمة خارجية — تستخدم Laravel HTTP Client فقط.
   *
   * المتطلبات:
   * - ملف Service Account JSON في المسار المحدد في config('services.fcm.credentials_path')
   * - FCM_PROJECT_ID في .env
   */
  class FcmNotificationService
  {
      private ?string $accessToken = null;
      private ?int $tokenExpiresAt = null;

      /**
       * ابعث إشعار لمستخدم معين (على كل أجهزته المسجلة).
       */
      public function sendToUser(User $user, string $title, string $body, array $data = []): void
      {
          $tokens = $user->fcmTokens()->pluck('token')->toArray();

          if (empty($tokens)) {
              return; // المستخدم مسجّلش أي جهاز
          }

          $this->sendToTokens($tokens, $title, $body, $data);
      }

      /**
       * ابعث إشعار لكل مستخدمي role معين (مثلاً super_admin).
       */
      public function sendToRole(string $role, string $title, string $body, array $data = []): void
      {
          $tokens = FcmToken::whereHas('user', function ($query) use ($role) {
              $query->role($role);
          })->pluck('token')->toArray();

          if (empty($tokens)) {
              return;
          }

          $this->sendToTokens($tokens, $title, $body, $data);
      }

      /**
       * ابعث إشعار لقائمة tokens.
       * لو token غير صالح → يتحذف أوتوماتيك.
       */
      public function sendToTokens(array $tokens, string $title, string $body, array $data = []): void
      {
          $accessToken = $this->getAccessToken();
          if (!$accessToken) {
              Log::error('FCM: Failed to obtain access token');
              return;
          }

          $projectId = config('services.fcm.project_id');
          $url = "https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send";

          $invalidTokens = [];

          foreach ($tokens as $token) {
              $payload = [
                  'message' => [
                      'token' => $token,
                      'notification' => [
                          'title' => $title,
                          'body' => $body,
                      ],
                      'data' => array_map('strval', $data), // FCM data values must be strings
                  ],
              ];

              try {
                  $response = Http::withToken($accessToken)
                      ->timeout(10)
                      ->post($url, $payload);

                  if ($response->status() === 404 || $response->status() === 400) {
                      // Token is invalid (UNREGISTERED or NOT_FOUND)
                      $invalidTokens[] = $token;
                      Log::info("FCM: Invalid token removed", ['token' => substr($token, 0, 20) . '...']);
                  } elseif ($response->failed()) {
                      Log::warning("FCM: Failed to send", [
                          'status' => $response->status(),
                          'body' => $response->body(),
                      ]);
                  } else {
                      // Update last_used_at
                      FcmToken::where('token', $token)->update(['last_used_at' => now()]);
                  }
              } catch (\Throwable $e) {
                  Log::error("FCM: Exception", ['message' => $e->getMessage()]);
              }
          }

          // Cleanup invalid tokens
          if (!empty($invalidTokens)) {
              $this->cleanupInvalidTokens($invalidTokens);
          }
      }

      /**
       * احذف tokens غير صالحة من DB.
       */
      private function cleanupInvalidTokens(array $invalidTokens): void
      {
          FcmToken::whereIn('token', $invalidTokens)->delete();
      }

      /**
       * جلب OAuth2 Access Token من Service Account JSON.
       * يستخدم JWT لتوليد الـ token. مع Caching لمدة ساعة.
       */
      private function getAccessToken(): ?string
      {
          // Return cached token if still valid
          if ($this->accessToken && $this->tokenExpiresAt && time() < $this->tokenExpiresAt - 60) {
              return $this->accessToken;
          }

          $credentialsPath = config('services.fcm.credentials_path');

          if (!file_exists($credentialsPath)) {
              Log::error("FCM: Service account file not found at {$credentialsPath}");
              return null;
          }

          $credentials = json_decode(file_get_contents($credentialsPath), true);

          if (!$credentials || !isset($credentials['client_email']) || !isset($credentials['private_key'])) {
              Log::error('FCM: Invalid service account JSON');
              return null;
          }

          $now = time();
          $expiry = $now + 3600; // 1 hour

          // Build JWT Header
          $header = base64_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));

          // Build JWT Claim Set
          $claim = base64_encode(json_encode([
              'iss' => $credentials['client_email'],
              'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
              'aud' => 'https://oauth2.googleapis.com/token',
              'iat' => $now,
              'exp' => $expiry,
          ]));

          // Sign
          $signatureInput = "{$header}.{$claim}";
          $privateKey = openssl_pkey_get_private($credentials['private_key']);
          if (!$privateKey) {
              Log::error('FCM: Invalid private key in service account');
              return null;
          }

          openssl_sign($signatureInput, $signature, $privateKey, OPENSSL_ALGO_SHA256);
          $jwt = "{$signatureInput}." . base64_encode($signature);

          // Exchange JWT for Access Token
          try {
              $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [
                  'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                  'assertion' => $jwt,
              ]);

              if ($response->successful()) {
                  $data = $response->json();
                  $this->accessToken = $data['access_token'];
                  $this->tokenExpiresAt = $now + ($data['expires_in'] ?? 3600);
                  return $this->accessToken;
              }

              Log::error('FCM: Token exchange failed', ['body' => $response->body()]);
          } catch (\Throwable $e) {
              Log::error('FCM: Token exchange exception', ['message' => $e->getMessage()]);
          }

          return null;
      }
  }
  ```

---

## 4.6 — إنشاء API Controller لتسجيل/إلغاء Token

### 4.6.1 إنشاء `app/Http/Controllers/Api/FcmTokenController.php`
- [x] **أنشئ الملف:**
  ```php
  <?php

  namespace App\Http\Controllers\Api;

  use App\Http\Controllers\Controller;
  use App\Models\FcmToken;
  use Illuminate\Http\JsonResponse;
  use Illuminate\Http\Request;

  class FcmTokenController extends Controller
  {
      /**
       * تسجيل FCM Token جديد أو تحديث موجود.
       * POST /api/fcm/register
       */
      public function register(Request $request): JsonResponse
      {
          $validated = $request->validate([
              'token' => 'required|string|max:512',
              'device_type' => 'nullable|string|in:web,android,ios',
              'device_name' => 'nullable|string|max:255',
          ]);

          FcmToken::updateOrCreate(
              ['token' => $validated['token']],
              [
                  'user_id' => $request->user()->id,
                  'device_type' => $validated['device_type'] ?? 'web',
                  'device_name' => $validated['device_name'] ?? null,
                  'last_used_at' => now(),
              ]
          );

          return response()->json(['message' => 'FCM token registered successfully.']);
      }

      /**
       * إلغاء تسجيل FCM Token (عند logout مثلاً).
       * DELETE /api/fcm/unregister
       */
      public function unregister(Request $request): JsonResponse
      {
          $validated = $request->validate([
              'token' => 'required|string|max:512',
          ]);

          FcmToken::where('token', $validated['token'])
              ->where('user_id', $request->user()->id)
              ->delete();

          return response()->json(['message' => 'FCM token removed successfully.']);
      }
  }
  ```

### 4.6.2 إضافة Routes
- [x] **الملف:** [routes/api.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/routes/api.php)
- [x] **أضف في نهاية الملف تماماً (بعد كل الـ groups الموجودة لتجنب التداخل):**
  ```php

  // === Phase 6: FCM Token Registration ===
  Route::middleware('auth:sanctum')->group(function () {
      Route::post('/fcm/register', [\App\Http\Controllers\Api\FcmTokenController::class, 'register']);
      Route::delete('/fcm/unregister', [\App\Http\Controllers\Api\FcmTokenController::class, 'unregister']);
  });
  ```

---

## 4.7 — إنشاء Service Worker

### 4.7.1 إنشاء `public/firebase-messaging-sw.js`
- [x] **ملف جديد:** `public/firebase-messaging-sw.js`
- [x] **المحتوى الكامل:**
  ```javascript
  /**
   * Firebase Messaging Service Worker
   * هذا الملف لازم يكون في root الـ public/ عشان المتصفح يلاقيه.
   * بيستقبل Push Notifications حتى لو المتصفح مقفول أو الـ Tab مش مفتوح.
   */
  importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js');
  importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js');

  // جلب البيانات من الـ URL Parameters (المرسلة من fcm-init.blade.php)
  const urlParams = new URL(location).searchParams;
  const firebaseConfig = {
    apiKey: urlParams.get('apiKey'),
    authDomain: urlParams.get('authDomain'),
    projectId: urlParams.get('projectId'),
    storageBucket: urlParams.get('storageBucket'),
    messagingSenderId: urlParams.get('messagingSenderId'),
    appId: urlParams.get('appId')
  };

  firebase.initializeApp(firebaseConfig);

  const messaging = firebase.messaging();

  // Handle background messages (when the browser/tab is not focused)
  messaging.onBackgroundMessage(function (payload) {
      const notificationTitle = payload.notification?.title || 'Apex Logistics';
      const notificationOptions = {
          body: payload.notification?.body || '',
          icon: '/favicon.ico',
          badge: '/favicon.ico',
          data: payload.data,
          tag: payload.data?.order_id ? `order-${payload.data.order_id}` : undefined,
      };

      self.registration.showNotification(notificationTitle, notificationOptions);
  });

  // Handle notification click
  self.addEventListener('notificationclick', function (event) {
      event.notification.close();

      // Open the dashboard when notification is clicked
      event.waitUntil(
          clients.openWindow('/admin')
      );
  });
  ```
- [x] **⚠️ هام جداً:** استبدل `YOUR_API_KEY`, `YOUR_PROJECT_ID`, إلخ. بالقيم الحقيقية من Firebase Console.

---

## 4.8 — إنشاء Frontend Firebase Init

### 4.8.1 إنشاء `resources/views/filament/hooks/fcm-init.blade.php`
- [x] **ملف جديد:** `resources/views/filament/hooks/fcm-init.blade.php`
- [x] **المحتوى الكامل:**
  ```html
  {{-- Phase 6: Firebase Cloud Messaging Initialization --}}
  @auth
  <script type="module">
      // الاعتماد على متغيرات البيئة لمنع التكرار
      const firebaseConfig = {
          apiKey: "{{ env('FCM_API_KEY') }}",
          authDomain: "{{ env('FCM_AUTH_DOMAIN') }}",
          projectId: "{{ env('FCM_PROJECT_ID') }}",
          storageBucket: "{{ env('FCM_STORAGE_BUCKET') }}",
          messagingSenderId: "{{ env('FCM_MESSAGING_SENDER_ID') }}",
          appId: "{{ env('FCM_APP_ID') }}"
      };

      // ⚠️ استبدل بالـ VAPID Key من Firebase Console
      const vapidKey = 'YOUR_VAPID_KEY';

      import('https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js').then(async ({ initializeApp }) => {
          const { getMessaging, getToken, onMessage } = await import('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging.js');

          const app = initializeApp(firebaseConfig);
          const messaging = getMessaging(app);

          // Register Service Worker
          const swUrl = `/firebase-messaging-sw.js?apiKey=${firebaseConfig.apiKey}&authDomain=${firebaseConfig.authDomain}&projectId=${firebaseConfig.projectId}&storageBucket=${firebaseConfig.storageBucket}&messagingSenderId=${firebaseConfig.messagingSenderId}&appId=${firebaseConfig.appId}`;
          const swRegistration = await navigator.serviceWorker.register(swUrl);

          // Request notification permission
          const permission = await Notification.requestPermission();
          if (permission !== 'granted') {
              console.warn('FCM: Notification permission denied');
              return;
          }

          try {
              // Get FCM Token
              const token = await getToken(messaging, {
                  vapidKey: vapidKey,
                  serviceWorkerRegistration: swRegistration,
              });

              if (token) {
                  // Register token with our backend
                  const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
                  await fetch('/api/fcm/register', {
                      method: 'POST',
                      headers: {
                          'Content-Type': 'application/json',
                          'X-CSRF-TOKEN': csrfToken,
                          'Accept': 'application/json',
                      },
                      body: JSON.stringify({
                          token: token,
                          device_type: 'web',
                          device_name: navigator.userAgent.substring(0, 100),
                      }),
                  });
                  console.log('✅ FCM: Token registered successfully');
              }
          } catch (err) {
              console.warn('FCM: Failed to get token', err);
          }

          // Handle foreground messages
          onMessage(messaging, (payload) => {
              console.log('FCM: Foreground message received', payload);
              // Show browser notification
              new Notification(payload.notification?.title || 'Apex Logistics', {
                  body: payload.notification?.body || '',
                  icon: '/favicon.ico',
              });
          });
      }).catch(err => {
          console.warn('FCM: Firebase import failed', err);
      });
  </script>
  @endauth
  ```

### 4.8.2 تعديل AdminPanelProvider
- [x] **أضف hook ثالث** (بعد hook الـ Echo اللي ضفناه في التيكت 3):
  ```php
              // === Phase 6: Firebase Cloud Messaging ===
              ->renderHook(
                  \Filament\View\PanelsRenderHook::BODY_END,
                  fn (): string => \Illuminate\Support\Facades\Blade::render(
                      file_get_contents(resource_path('views/filament/hooks/fcm-init.blade.php'))
                  )
              )
  ```

---

## 4.9 — ربط FCM بتغيير حالة الطلب

### 4.9.1 تعديل `app/Observers/OrderObserver.php`
- [x] **في نفس بلوك الـ Broadcasting** اللي ضفناه في التيكت 3 (بعد `broadcast(new OrderStatusChanged(...))`):
- [x] **أضف بعده:**
  ```php

              // === Phase 6: FCM Push Notification ===
              if ($order->driver_id && $order->driver?->user) {
                  app(\App\Services\FcmNotificationService::class)->sendToUser(
                      $order->driver->user,
                      'تحديث الطلب #' . $order->id,
                      'الحالة الجديدة: ' . $order->status->value,
                      ['order_id' => (string) $order->id, 'status' => $order->status->value]
                  );
              }
  ```
- [x] **⚠️ ملاحظة:** الكود ده جوه الـ `if ($order->isDirty('status'))` اللي ضفناه في التيكت 3.

---

## ✅ فحص نهائي للتيكت الرابعة

- [x] **فحص 1 — جدول `fcm_tokens` موجود:**
  ```bash
  php artisan tinker --execute="echo Schema::hasTable('fcm_tokens') ? 'EXISTS' : 'MISSING';"
  ```

- [x] **فحص 2 — Service Account JSON موجود:**
  ```bash
  php artisan tinker --execute="echo file_exists(config('services.fcm.credentials_path')) ? 'FILE FOUND' : 'FILE MISSING';"
  ```

- [x] **فحص 3 — FCM routes متسجلة:**
  ```bash
  php artisan route:list --path=fcm
  ```
  لازم يعرض `POST api/fcm/register` و `DELETE api/fcm/unregister`.

- [x] **فحص 4 — Service Worker يتحمّل:**
  افتح المتصفح → `http://localhost:8000/firebase-messaging-sw.js` → لازم يعرض كود JavaScript.

- [x] **فحص 5 — الداشبورد شغال:** `php artisan serve` → بدون أخطاء.

## 📝 توثيق التيكت الرابعة (إلزامي)
- [x] سجّل الـ migration بالمحتوى الكامل.
- [x] سجّل كل الملفات الجديدة (FcmToken, FcmNotificationService, FcmTokenController, firebase-messaging-sw.js, fcm-init.blade.php) بالمحتوى الكامل.
- [x] سجّل التعديلات على User.php, services.php, api.php, .env, .gitignore, AdminPanelProvider.php, OrderObserver.php بصيغة diff.

---
---

# 🎟️ التيكت الخامسة: خريطة التتبع الحي (Live Map)

> **الهدف:** بناء صفحة Filament تعرض مواقع المناديب لحظياً على خريطة Leaflet.js + OpenStreetMap.
>
> **الاعتماديات:** التيكتات 2, 3 (Broadcasting لازم يكون شغال).
> **التقدير الزمني:** 3 ساعات.

---

## 5.1 — إنشاء صفحة الخريطة في Filament

### 5.1.1 إنشاء `app/Filament/Pages/LiveMapPage.php`
- [x] **ملف جديد:** `app/Filament/Pages/LiveMapPage.php`
- [x] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Filament\Pages;

  use App\Enums\BatchStatus;
  use App\Models\Driver;
  use Filament\Pages\Page;
  use Illuminate\Support\Facades\Cache;

  class LiveMapPage extends Page
  {
      protected static ?string $navigationIcon = 'heroicon-o-map';
      protected static ?string $navigationGroup = 'المراقبة';
      protected static ?string $navigationLabel = 'خريطة التتبع';
      protected static ?string $title = 'خريطة التتبع الحي';
      protected static ?int $navigationSort = 1;

      protected static string $view = 'filament.pages.live-map';

      /**
       * جلب مواقع المناديب الأولية من Redis.
       */
      protected function getViewData(): array
      {
          $drivers = Driver::withTrashed(false)->get();
          $driverLocations = [];

          foreach ($drivers as $driver) {
              $locationData = Cache::get("driver:location:{$driver->id}");

              // لو الموقع مخزن كـ JSON string
              if (is_string($locationData)) {
                  $locationData = json_decode($locationData, true);
              }

              if (!$locationData || !isset($locationData['lat'] ?? $locationData['latitude'])) {
                  continue; // المندوب مش online
              }

              $lat = $locationData['lat'] ?? $locationData['latitude'] ?? null;
              $lng = $locationData['lng'] ?? $locationData['longitude'] ?? null;
              $timestamp = $locationData['timestamp'] ?? $locationData['updated_at'] ?? null;

              if (!$lat || !$lng) {
                  continue;
              }

              // هل المندوب عنده Batch نشطة؟
              $hasActiveBatch = $driver->deliveryBatches()
                  ->where('status', BatchStatus::Active)
                  ->exists();

              // عدد الطلبات اللي مسلّمهاش لسه
              $activeOrders = $driver->orders()
                  ->whereNotIn('status', ['delivered', 'cancelled'])
                  ->count();

              $driverLocations[] = [
                  'id' => $driver->id,
                  'name' => $driver->name ?? "Driver #{$driver->id}",
                  'lat' => (float) $lat,
                  'lng' => (float) $lng,
                  'status' => $hasActiveBatch ? 'busy' : 'available', // busy = في مشوار
                  'active_orders' => $activeOrders,
                  'last_seen' => $timestamp,
              ];
          }

          return [
              'driverLocations' => $driverLocations,
          ];
      }
  }
  ```

### 5.1.2 إنشاء View الخريطة
- [x] **أنشئ المجلد:** `resources/views/filament/pages/` (لو مش موجود)
- [x] **ملف جديد:** `resources/views/filament/pages/live-map.blade.php`
- [x] **المحتوى الكامل:**
  ```html
  <x-filament-panels::page>
      {{-- Leaflet CSS --}}
      <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />

      <div
          id="live-map"
          style="height: calc(100vh - 220px); width: 100%; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"
      ></div>

      <div style="margin-top: 12px; display: flex; gap: 24px; font-size: 14px;">
          <span>🟢 متاح</span>
          <span>🟠 في مشوار</span>
          <span>🔴 غير متصل (> 5 دقائق)</span>
          <span style="margin-right: auto; color: #888;" id="driver-count">المناديب المتصلين: {{ count($driverLocations) }}</span>
      </div>

      {{-- Leaflet JS --}}
      <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

      <script>
          document.addEventListener('DOMContentLoaded', function () {
              // Initialize map centered on Riyadh (default)
              const map = L.map('live-map').setView([24.7136, 46.6753], 12);

              L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                  attribution: '© OpenStreetMap contributors',
                  maxZoom: 19,
              }).addTo(map);

              // Marker storage: { driverId: L.marker }
              const markers = {};

              // Color icons
              function createIcon(color) {
                  const svgIcon = `
                      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">
                          <circle cx="12" cy="12" r="10" fill="${color}" stroke="white" stroke-width="2"/>
                          <circle cx="12" cy="12" r="4" fill="white"/>
                      </svg>
                  `;
                  return L.divIcon({
                      html: svgIcon,
                      className: '',
                      iconSize: [32, 32],
                      iconAnchor: [16, 16],
                  });
              }

              const icons = {
                  available: createIcon('#22c55e'),  // Green
                  busy: createIcon('#f97316'),        // Orange
                  offline: createIcon('#ef4444'),     // Red
              };

              // Load initial driver locations
              const initialDrivers = @json($driverLocations);

              initialDrivers.forEach(driver => {
                  addOrUpdateMarker(driver);
              });

              // Fit bounds if we have markers
              if (initialDrivers.length > 0) {
                  const group = L.featureGroup(Object.values(markers));
                  map.fitBounds(group.getBounds().pad(0.1));
              }

              function addOrUpdateMarker(driver) {
                  const icon = icons[driver.status] || icons.available;

                  if (markers[driver.id]) {
                      // Update existing marker
                      markers[driver.id].setLatLng([driver.lat, driver.lng]);
                      markers[driver.id].setIcon(icon);
                      markers[driver.id].setPopupContent(buildPopup(driver));
                  } else {
                      // Create new marker
                      const marker = L.marker([driver.lat, driver.lng], { icon: icon })
                          .addTo(map)
                          .bindPopup(buildPopup(driver));
                      markers[driver.id] = marker;
                  }
              }

              function buildPopup(driver) {
                  const statusLabel = driver.status === 'busy' ? '🟠 في مشوار' : '🟢 متاح';
                  return `
                      <div style="direction: rtl; text-align: right; min-width: 150px;">
                          <strong>${driver.name || 'مندوب #' + driver.id}</strong><br>
                          <span>${statusLabel}</span><br>
                          <span>الطلبات النشطة: ${driver.active_orders || 0}</span><br>
                          <small style="color: #888;">آخر تحديث: ${driver.last_seen ? new Date(driver.last_seen).toLocaleTimeString('ar-EG') : 'غير معروف'}</small>
                      </div>
                  `;
              }

              // Listen for real-time location updates from Echo (via Ticket 3)
              window.addEventListener('driver-location-updated', function (e) {
                  const data = e.detail;
                  addOrUpdateMarker({
                      id: data.driver_id,
                      name: data.driver_name,
                      lat: data.latitude,
                      lng: data.longitude,
                      status: data.active_batch_id ? 'busy' : 'available',
                      active_orders: data.active_orders_count,
                      last_seen: data.timestamp,
                  });

                  // Update counter
                  document.getElementById('driver-count').textContent =
                      'المناديب المتصلين: ' + Object.keys(markers).length;
              });
          });
      </script>
  </x-filament-panels::page>
  ```

---

## ✅ فحص نهائي للتيكت الخامسة

- [x] **فحص 1 — الصفحة موجودة في Navigation:**
  افتح الداشبورد → لازم تلاقي "خريطة التتبع" في القائمة تحت "المراقبة".

- [x] **فحص 2 — الخريطة تعرض:**
  افتح الصفحة → لازم تشوف خريطة OpenStreetMap.

- [x] **فحص 3 — لو في بيانات مواقع → Markers تظهر:**
  لو سبق وبعت location عبر الـ API → لازم المندوب يظهر على الخريطة.

- [x] **فحص 4 — الداشبورد شغال:** بدون أخطاء.

## 📝 توثيق التيكت الخامسة (إلزامي)
- [ ] سجّل ملف `LiveMapPage.php` بالمحتوى الكامل.
- [ ] سجّل ملف `live-map.blade.php` بالمحتوى الكامل.

---
---

# 🎟️ التيكت السادسة: محرك التوزيع الآلي (Auto-Dispatch Engine)

> **الهدف:** بناء منطق يعيّن الطلبات الجديدة أوتوماتيك لأقرب مندوب متاح.
>
> **الاعتماديات:** التيكتات 1, 2, 3.
> **التقدير الزمني:** 4 ساعات.

---

## 6.1 — إضافة إعداد `dispatch_mode` في Settings

> **المشكلة:** الملف [GeneralSettings.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Settings/GeneralSettings.php) حالياً فيه 6 خصائص بس. مفيهوش `dispatch_mode`. محتاجين نضيفه.

### 6.1.1 إنشاء Settings Migration
- [ ] **الأمر:**
  ```bash
  php artisan make:settings-migration AddDispatchModeToGeneralSettings
  ```
  لو الأمر مش شغال، أنشئ الملف يدوياً في `database/settings/`.
- [ ] **المحتوى:**
  ```php
  <?php

  use Spatie\LaravelSettings\Migrations\SettingsMigration;

  class AddDispatchModeToGeneralSettings extends SettingsMigration
  {
      public function up(): void
      {
          $this->migrator->add('general.dispatch_mode', 'manual');
          $this->migrator->add('general.delivery_fee_strategy', 'flat_fee');
          $this->migrator->add('general.extra_stop_fee', 500);
          $this->migrator->add('general.enable_batched_orders', false);
          $this->migrator->add('general.max_orders_per_driver', 3);
          $this->migrator->add('general.batch_routing_mode', 'same_zone');
          $this->migrator->add('general.batch_inactivity_timeout_minutes', 120);
          $this->migrator->add('general.batch_grace_period_minutes', 15);
          $this->migrator->add('general.driver_can_cancel_directly', false);
          $this->migrator->add('general.require_receipt_image', true);
      }
  }
  ```
  > **⚠️ ملاحظة مهمة (تصحيح):** دالة `$this->migrator->add()` في Spatie Settings **سترمي Exception لو القيمة موجودة بالفعل** — مش هتتجاهلها.
  > **قبل ما تشغل الـ migration:** افحص القيم الموجودة أولاً:
  > ```bash
  > php artisan tinker --execute="echo json_encode(DB::table('settings')->where('group', 'general')->pluck('name'));"
  > ```
  > لو أي من الإعدادات دي موجودة أصلاً → **احذفها من ملف الـ migration** (الأسطر المقابلة فقط) قبل ما تشغل `php artisan migrate`.

- [ ] **شغّل:**
  ```bash
  php artisan migrate
  ```

### 6.1.2 تحديث `app/Settings/GeneralSettings.php`
- [ ] **الملف الحالي** (21 سطر) فيه 6 خصائص.
- [ ] **أضف الخصائص الجديدة بعد `$available_payment_methods` (سطر 14) وقبل `public static function group()` (سطر 16):**
  ```php

      // ——— Phase 6 Settings ———
      public string $dispatch_mode;                   // 'manual' أو 'auto'
      public string $delivery_fee_strategy;           // 'flat_fee' أو 'base_plus_stop'
      public int $extra_stop_fee;                     // بالقرش
      public bool $enable_batched_orders;
      public int $max_orders_per_driver;
      public string $batch_routing_mode;              // 'same_zone' أو 'strict_nearby'
      public int $batch_inactivity_timeout_minutes;
      public int $batch_grace_period_minutes;
      public bool $driver_can_cancel_directly;
      public bool $require_receipt_image;
  ```

---

## 6.2 — إنشاء AutoDispatcher Service

### 6.2.1 إنشاء `app/Services/AutoDispatcher.php`
- [ ] **ملف جديد:** `app/Services/AutoDispatcher.php`
- [ ] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Services;

  use App\Enums\OrderStatus;
  use App\Models\Order;
  use App\Models\Driver;
  use App\Settings\GeneralSettings;
  use App\Services\OrderStateMachine;
  use Illuminate\Support\Facades\Cache;
  use Illuminate\Support\Facades\DB;
  use Illuminate\Support\Facades\Log;

  class AutoDispatcher
  {
      /**
       * حاول تعيين طلب أوتوماتيك لأقرب مندوب متاح.
       *
       * @param Order $order الطلب المطلوب تعيينه
       * @param int $attempt رقم المحاولة (max 3)
       * @return bool هل نجح التعيين؟
       */
      public function dispatch(Order $order, int $attempt = 1): bool
      {
          $settings = app(GeneralSettings::class);

          if ($settings->dispatch_mode !== 'auto') {
              return false; // التوزيع اليدوي مفعّل
          }

          if ($order->driver_id) {
              return false; // الطلب معيّن بالفعل
          }

          if ($attempt > 3) {
              // فشلت كل المحاولات — إشعار المشرفين
              $this->notifyAdminsDispatchFailed($order);
              return false;
          }

          // === جلب مواقع المناديب من Redis ===
          $drivers = Driver::where('is_active', true)
              ->whereNull('deleted_at')
              ->get();

          $candidates = [];

          foreach ($drivers as $driver) {
              // تخطي المناديب اللي عليهم lock (تم تعيينهم مؤخراً)
              if (Cache::has("dispatch:lock:{$driver->id}")) {
                  continue;
              }

              // جلب الموقع من Redis
              $locationData = Cache::get("driver:location:{$driver->id}");
              if (is_string($locationData)) {
                  $locationData = json_decode($locationData, true);
              }

              if (!$locationData) {
                  continue; // المندوب offline
              }

              $driverLat = $locationData['lat'] ?? $locationData['latitude'] ?? null;
              $driverLng = $locationData['lng'] ?? $locationData['longitude'] ?? null;

              if (!$driverLat || !$driverLng) {
                  continue;
              }

              // جلب موقع العميل
              $customerLocation = $order->customer?->locations()?->first();
              if (!$customerLocation || !$customerLocation->latitude) {
                  // مفيش موقع عميل — عيّن لأقرب مندوب بدون حساب مسافة
                  $candidates[] = [
                      'driver' => $driver,
                      'distance' => 0,
                  ];
                  continue;
              }

              // Haversine Distance
              $distance = $this->haversineDistance(
                  $driverLat, $driverLng,
                  $customerLocation->latitude, $customerLocation->longitude
              );

              $candidates[] = [
                  'driver' => $driver,
                  'distance' => $distance,
              ];
          }

          // ترتيب حسب المسافة (الأقرب أولاً)
          usort($candidates, fn($a, $b) => $a['distance'] <=> $b['distance']);

          // === محاولة التعيين ===
          foreach ($candidates as $candidate) {
              $driver = $candidate['driver'];

              try {
                  return DB::transaction(function () use ($order, $driver, $settings) {
                      // Lock الطلب
                      $lockedOrder = Order::where('id', $order->id)->lockForUpdate()->first();
                      if (!$lockedOrder || $lockedOrder->driver_id) {
                          return false; // الطلب اتعيّن بالفعل
                      }

                      // استخدام BatchDispatcher للتحقق من القيود
                      $batch = app(BatchDispatcher::class)->assignOrderToDriver($lockedOrder, $driver);

                      // State Machine transition
                      OrderStateMachine::transition($lockedOrder, OrderStatus::Assigned);
                      $lockedOrder->save();

                      // Lock المندوب 60 ثانية (منع التعيين المتكرر)
                      Cache::put("dispatch:lock:{$driver->id}", true, 60);

                      // FCM notification للمندوب
                      if ($driver->user) {
                          app(FcmNotificationService::class)->sendToUser(
                              $driver->user,
                              'طلب جديد #' . $lockedOrder->id,
                              'تم تعيين طلب جديد لك. يرجى المراجعة.',
                              ['order_id' => (string) $lockedOrder->id]
                          );
                      }

                      // Schedule acceptance check after 60 seconds
                      \App\Jobs\CheckDriverAcceptance::dispatch($lockedOrder->id, $driver->id, 1)
                          ->delay(now()->addSeconds(60));

                      Log::info("AutoDispatch: Order #{$lockedOrder->id} assigned to Driver #{$driver->id}");

                      return true;
                  });
              } catch (\Illuminate\Validation\ValidationException $e) {
                  // المندوب ده مش ينفع (وصل الحد، zone مختلف، إلخ) — جرّب اللي بعده
                  Log::info("AutoDispatch: Skipping Driver #{$driver->id}", ['reason' => $e->getMessage()]);
                  continue;
              } catch (\Throwable $e) {
                  Log::error("AutoDispatch: Unexpected error", ['message' => $e->getMessage()]);
                  continue;
              }
          }

          // مفيش مندوب اتعيّن
          $this->notifyAdminsDispatchFailed($order);
          return false;
      }

      /**
       * Haversine formula — حساب المسافة بين نقطتين جغرافيتين بالكيلومتر.
       */
      private function haversineDistance(float $lat1, float $lon1, float $lat2, float $lon2): float
      {
          $earthRadius = 6371; // km

          $dLat = deg2rad($lat2 - $lat1);
          $dLon = deg2rad($lon2 - $lon1);

          $a = sin($dLat / 2) * sin($dLat / 2) +
               cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
               sin($dLon / 2) * sin($dLon / 2);

          $c = 2 * atan2(sqrt($a), sqrt(1 - $a));

          return $earthRadius * $c;
      }

      /**
       * إشعار كل المشرفين بفشل التوزيع.
       */
      private function notifyAdminsDispatchFailed(Order $order): void
      {
          Log::warning("AutoDispatch: FAILED for Order #{$order->id} — no available drivers");

          // FCM Push لكل المشرفين
          app(FcmNotificationService::class)->sendToRole(
              'super_admin',
              '⚠️ فشل التوزيع الآلي',
              "الطلب #{$order->id} لم يتم تعيينه — لا يوجد مناديب متاحين.",
              ['order_id' => (string) $order->id]
          );

          // Filament Database Notification لكل المشرفين
          $admins = \App\Models\User::role('super_admin')->get();
          foreach ($admins as $admin) {
              \Filament\Notifications\Notification::make()
                  ->title('⚠️ فشل التوزيع الآلي')
                  ->body("الطلب #{$order->id} لم يتم تعيينه — لا يوجد مناديب متاحين. يرجى التعيين يدوياً.")
                  ->warning()
                  ->sendToDatabase($admin);
          }
      }
  }
  ```

---

## 6.3 — إنشاء Job فحص قبول المندوب

### 6.3.1 إنشاء `app/Jobs/CheckDriverAcceptance.php`
- [ ] **الأمر:**
  ```bash
  php artisan make:job CheckDriverAcceptance
  ```
- [ ] **استبدل المحتوى بالكامل:**
  ```php
  <?php

  namespace App\Jobs;

  use App\Enums\OrderStatus;
  use App\Models\Order;
  use App\Services\AutoDispatcher;
  use App\Services\OrderStateMachine;
  use Illuminate\Bus\Queueable;
  use Illuminate\Contracts\Queue\ShouldQueue;
  use Illuminate\Foundation\Bus\Dispatchable;
  use Illuminate\Queue\InteractsWithQueue;
  use Illuminate\Queue\SerializesModels;
  use Illuminate\Support\Facades\Cache;
  use Illuminate\Support\Facades\Log;

  /**
   * يتشغل بعد 60 ثانية من تعيين طلب أوتوماتيك.
   * لو المندوب مقبلش (مفيش نشاط) → الطلب يرجع pending ويتعاد التوزيع.
   */
  class CheckDriverAcceptance implements ShouldQueue
  {
      use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

      public function __construct(
          public int $orderId,
          public int $driverId,
          public int $attempt,
      ) {}

      public function handle(): void
      {
          $order = Order::find($this->orderId);

          if (!$order) {
              return; // الطلب اتحذف
          }

          // لو الحالة مش assigned → المندوب قبل أو الطلب اتلغى
          if ($order->status !== OrderStatus::Assigned) {
              Log::info("CheckDriverAcceptance: Order #{$this->orderId} is no longer 'assigned'. Skipping.");
              return;
          }

          // لو المندوب اتغير (مش هو اللي عيّناه)
          if ($order->driver_id !== $this->driverId) {
              return;
          }

          // === المندوب مقبلش — إرجاع الطلب لـ pending ===
          Log::info("CheckDriverAcceptance: Driver #{$this->driverId} didn't accept Order #{$this->orderId}. Reassigning...");

          try {
              OrderStateMachine::transition($order, OrderStatus::Pending);
              $order->driver_id = null;
              $order->batch_id = null;
              $order->version = ($order->version ?? 1) + 1;
              $order->saveQuietly(); // Quietly عشان الـ Observer ميعملش broadcast على pending

              // فك Lock المندوب
              Cache::forget("dispatch:lock:{$this->driverId}");

              // إعادة التوزيع
              app(AutoDispatcher::class)->dispatch($order, $this->attempt + 1);
          } catch (\Throwable $e) {
              Log::error("CheckDriverAcceptance: Failed to reassign Order #{$this->orderId}", [
                  'message' => $e->getMessage(),
              ]);
          }
      }
  }
  ```

---

## 6.4 — دمج AutoDispatcher في CreateOrder

### 6.4.1 تعديل `app/Filament/Resources/Orders/Pages/CreateOrder.php`
- [ ] **في دالة `afterCreate()`** — بعد الـ Broadcasting اللي ضفناه في التيكت 3:
- [ ] **أضف في آخر الدالة (قبل `}` بتاعها):**
  ```php

          // === Phase 6: Auto-Dispatch ===
          $settings = app(\App\Settings\GeneralSettings::class);
          if ($settings->dispatch_mode === 'auto') {
              app(\App\Services\AutoDispatcher::class)->dispatch($order);
          }
  ```
- [ ] **الدالة `afterCreate()` النهائية لازم تبقى:**
  ```php
  protected function afterCreate(): void
  {
      $order = $this->record;
      if ($order->order_type === \App\Enums\OrderType::Standard) {
          app(\App\Services\PricingManager::class)->snapshot($order);
          $order->save();
      }

      // === Phase 6: Broadcasting ===
      broadcast(new \App\Events\NewOrderCreated(
          orderId: $order->id,
          orderType: $order->order_type->value,
          customerName: $order->customer?->name ?? 'غير محدد',
      ));

      // === Phase 6: Auto-Dispatch ===
      $settings = app(\App\Settings\GeneralSettings::class);
      if ($settings->dispatch_mode === 'auto') {
          app(\App\Services\AutoDispatcher::class)->dispatch($order);
      }
  }
  ```

---

## ✅ فحص نهائي للتيكت السادسة

- [ ] **فحص 1 — إعداد `dispatch_mode` موجود:**
  ```bash
  php artisan tinker --execute="echo app(App\Settings\GeneralSettings::class)->dispatch_mode;"
  ```
  لازم يطبع `manual`.

- [ ] **فحص 2 — State Machine يسمح بـ assigned→pending:**
  ```bash
  php artisan tinker --execute="use App\Services\OrderStateMachine; use App\Enums\{OrderStatus, OrderType}; echo OrderStateMachine::canTransition(OrderStatus::Assigned, OrderStatus::Pending, OrderType::Standard) ? 'PASS' : 'FAIL';"
  ```
  لازم يطبع `PASS` (ده اتعمل أصلاً في Phase 5 — بس تأكد).

- [ ] **فحص 3 — الداشبورد شغال:** بدون أخطاء.

## 📝 توثيق التيكت السادسة (إلزامي)
- [ ] سجّل Settings Migration + التعديل على GeneralSettings.php.
- [ ] سجّل AutoDispatcher.php بالمحتوى الكامل.
- [ ] سجّل CheckDriverAcceptance.php بالمحتوى الكامل.
- [ ] سجّل التعديل على CreateOrder.php بصيغة diff.

---
---

# 🎟️ التيكت السابعة: حماية التوزيع والتكامل (Dispatch Protection)

> **الهدف:** ضمان نفس القيود تنطبق على التعيين اليدوي من EditOrder.
>
> **الاعتماديات:** التيكت السادسة.
> **التقدير الزمني:** ساعتين.

---

## 7.1 — تعديل EditOrder للتعيين اليدوي عبر BatchDispatcher

### 7.1.1 تعديل `app/Filament/Resources/Orders/Pages/EditOrder.php`
- [ ] **⚠️ اقرا الملف أولاً!** الدالة `handleRecordUpdate()` فيها logic معقد لتعديل Items والمخزون.
- [ ] **المطلوب:** في بداية `handleRecordUpdate()` — **داخل** الـ `DB::transaction` وقبل `if ($record->order_type === 'errand')` — أضف:
  > **⚠️ تصحيح مهم:** الكود الأصلي كان يستخدم `$record->isDirty('driver_id')` — وده **خطأ** لأن `$record` في هذه النقطة لم يتم تحديثه بعد بالداتا الجديدة من `$data`، يعني `isDirty()` هترجع `false` دائماً.
  > **الحل:** نقارن `$data['driver_id']` مع `$record->driver_id` مباشرة.
  ```php
              // === Phase 6: Manual Dispatch via BatchDispatcher ===
              $newDriverId = $data['driver_id'] ?? null;
              $oldDriverId = $record->driver_id;
              if ($newDriverId && !$oldDriverId && $newDriverId != $oldDriverId) {
                  $driver = \App\Models\Driver::findOrFail($newDriverId);
                  try {
                      app(\App\Services\BatchDispatcher::class)->assignOrderToDriver($record, $driver);
                      // BatchDispatcher هيربط الطلب بالـ Batch ويحدّث driver_id و batch_id
                  } catch (\Illuminate\Validation\ValidationException $e) {
                      \Filament\Notifications\Notification::make()
                          ->title('لا يمكن تعيين المندوب')
                          ->body($e->getMessage())
                          ->danger()
                          ->send();
                      return $record; // ارجع بدون حفظ
                  }
              }
  ```

---

## ✅ فحص نهائي للتيكت السابعة

- [ ] **فحص 1 — التعيين اليدوي يمر عبر BatchDispatcher:** عيّن مندوب وصل الحد الأقصى → لازم يظهر رسالة رفض.
- [ ] **فحص 2 — الداشبورد شغال:** بدون أخطاء.

## 📝 توثيق التيكت السابعة (إلزامي)
- [ ] سجّل التعديل على EditOrder.php بصيغة diff.

---
---

# 🎟️ التيكت الثامنة: Widgets وإحصائيات KPIs

> **الهدف:** بناء 5 Widgets في الصفحة الرئيسية للداشبورد.
>
> **ليه بنعمل ده؟** الصفحة الرئيسية حالياً فاضية. المشرف محتاج يشوف لمحة سريعة عن حالة الأعمال بدون ما يفتح كل صفحة لوحدها.
>
> **الاعتماديات:** التيكت الأولى (Redis لقراءة مواقع المناديب).
> **التقدير الزمني:** 3 ساعات.
>
> **⚠️ ملاحظة مهمة:** الـ `AdminPanelProvider` فيه `discoverWidgets` مفعّل. يعني أي Widget ننشئه في `app/Filament/Widgets/` هيتسجل أوتوماتيك بدون أي تعديل.

---

## 8.1 — Widget طلبات اليوم

### 8.1.1 إنشاء `app/Filament/Widgets/TodayOrdersWidget.php`
- [ ] **أنشئ المجلد:** `app/Filament/Widgets/` (لو مش موجود)
- [ ] **ملف جديد:** `app/Filament/Widgets/TodayOrdersWidget.php`
- [ ] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Filament\Widgets;

  use App\Enums\OrderType;
  use App\Models\Order;
  use Filament\Widgets\StatsOverviewWidget;
  use Filament\Widgets\StatsOverviewWidget\Stat;

  class TodayOrdersWidget extends StatsOverviewWidget
  {
      protected static ?int $sort = 1;
      protected int | string | array $columnSpan = 'full';
      protected static ?string $pollingInterval = '15s';

      protected function getStats(): array
      {
          $today = now()->startOfDay();

          $total = Order::where('created_at', '>=', $today)->count();
          $standard = Order::where('created_at', '>=', $today)
              ->where('order_type', OrderType::Standard)->count();
          $errand = Order::where('created_at', '>=', $today)
              ->where('order_type', OrderType::Errand)->count();

          return [
              Stat::make('إجمالي طلبات اليوم', $total)
                  ->description('كل الطلبات')
                  ->icon('heroicon-o-shopping-cart')
                  ->color('primary'),
              Stat::make('طلبات عادية', $standard)
                  ->description('Standard')
                  ->icon('heroicon-o-cube')
                  ->color('success'),
              Stat::make('مشاوير', $errand)
                  ->description('Errand')
                  ->icon('heroicon-o-truck')
                  ->color('warning'),
          ];
      }
  }
  ```

---

## 8.2 — Widget الإيرادات

### 8.2.1 إنشاء `app/Filament/Widgets/TodayRevenueWidget.php`
- [ ] **ملف جديد:** `app/Filament/Widgets/TodayRevenueWidget.php`
- [ ] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Filament\Widgets;

  use App\Enums\OrderStatus;
  use App\Models\Order;
  use App\Support\MoneyHelper;
  use Filament\Widgets\StatsOverviewWidget;
  use Filament\Widgets\StatsOverviewWidget\Stat;

  class TodayRevenueWidget extends StatsOverviewWidget
  {
      protected static ?int $sort = 2;
      protected int | string | array $columnSpan = 'full';
      protected static ?string $pollingInterval = '30s';

      protected function getStats(): array
      {
          $today = now()->startOfDay();

          $delivered = Order::where('created_at', '>=', $today)
              ->whereIn('status', [OrderStatus::Delivered, OrderStatus::PartiallyDelivered]);

          $totalRevenue = (clone $delivered)->sum('total_amount');
          $totalShipping = (clone $delivered)->sum('shipping');
          $totalTax = (clone $delivered)->sum('tax');

          return [
              Stat::make('إجمالي الإيرادات', MoneyHelper::display((int) $totalRevenue))
                  ->description('الطلبات المسلّمة اليوم')
                  ->icon('heroicon-o-banknotes')
                  ->color('success'),
              Stat::make('رسوم التوصيل', MoneyHelper::display((int) $totalShipping))
                  ->icon('heroicon-o-truck')
                  ->color('info'),
              Stat::make('الضرائب المحصّلة', MoneyHelper::display((int) $totalTax))
                  ->icon('heroicon-o-receipt-percent')
                  ->color('gray'),
          ];
      }
  }
  ```

---

## 8.3 — Widget المناديب النشطين

### 8.3.1 إنشاء `app/Filament/Widgets/ActiveDriversWidget.php`
- [ ] **ملف جديد:** `app/Filament/Widgets/ActiveDriversWidget.php`
- [ ] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Filament\Widgets;

  use App\Enums\BatchStatus;
  use App\Models\Driver;
  use Filament\Widgets\StatsOverviewWidget;
  use Filament\Widgets\StatsOverviewWidget\Stat;
  use Illuminate\Support\Facades\Cache;

  class ActiveDriversWidget extends StatsOverviewWidget
  {
      protected static ?int $sort = 3;
      protected int | string | array $columnSpan = 'full';
      protected static ?string $pollingInterval = '30s';

      protected function getStats(): array
      {
          $allDrivers = Driver::whereNull('deleted_at')->count();
          $online = 0;
          $busy = 0;

          // استخدم withCount لتجنب N+1 queries
          $drivers = Driver::whereNull('deleted_at')->withCount(['deliveryBatches' => function ($query) {
              $query->where('status', BatchStatus::Active);
          }])->get();

          foreach ($drivers as $driver) {
              $location = Cache::get("driver:location:{$driver->id}");
              if ($location) {
                  $online++;
                  if ($driver->delivery_batches_count > 0) {
                      $busy++;
                  }
              }
          }

          $offline = $allDrivers - $online;

          return [
              Stat::make('متصلين', $online)
                  ->description('أونلاين الآن')
                  ->icon('heroicon-o-signal')
                  ->color('success'),
              Stat::make('في مشوار', $busy)
                  ->description('عندهم طلبات نشطة')
                  ->icon('heroicon-o-truck')
                  ->color('warning'),
              Stat::make('غير متصلين', $offline)
                  ->description('أوفلاين')
                  ->icon('heroicon-o-signal-slash')
                  ->color('danger'),
          ];
      }
  }
  ```

---

## 8.4 — Widget الطلبات المعلقة (تحتاج تدخل)

### 8.4.1 إنشاء `app/Filament/Widgets/PendingAttentionWidget.php`
- [ ] **ملف جديد:** `app/Filament/Widgets/PendingAttentionWidget.php`
- [ ] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Filament\Widgets;

  use App\Enums\OrderStatus;
  use App\Models\Order;
  use Filament\Tables;
  use Filament\Tables\Table;
  use Filament\Widgets\TableWidget as BaseWidget;

  class PendingAttentionWidget extends BaseWidget
  {
      protected static ?int $sort = 5;
      protected int | string | array $columnSpan = 'full';
      protected static ?string $pollingInterval = '15s';
      protected static ?string $heading = '⚠️ طلبات تحتاج تدخل';

      public function table(Table $table): Table
      {
          return $table
              ->query(
                  Order::query()
                      ->where(function ($q) {
                          $q->where('status', OrderStatus::CancellationRequested)
                            ->orWhere('status', OrderStatus::Disputed)
                            ->orWhere(function ($q2) {
                                $q2->where('status', OrderStatus::PendingPricing)
                                   ->where('updated_at', '<=', now()->subMinutes(30));
                            });
                      })
                      ->orderBy('updated_at', 'asc')
              )
              ->columns([
                  Tables\Columns\TextColumn::make('id')->label('#'),
                  Tables\Columns\TextColumn::make('order_type')->label('النوع')
                      ->formatStateUsing(fn ($state) => $state->value ?? $state),
                  Tables\Columns\TextColumn::make('status')->label('الحالة')
                      ->badge()
                      ->formatStateUsing(fn ($state) => $state->value ?? $state),
                  Tables\Columns\TextColumn::make('updated_at')->label('منذ')
                      ->since(),
              ])
              ->actions([
                  Tables\Actions\Action::make('view')
                      ->label('فتح')
                      ->url(fn (Order $record) => route('filament.admin.resources.orders.edit', $record))
                      ->icon('heroicon-o-eye'),
              ])
              ->paginated(false);
      }
  }
  ```

---

## ✅ فحص نهائي للتيكت الثامنة

- [ ] **فحص 1 — الصفحة الرئيسية تعرض Widgets:**
  افتح `/admin` → لازم تشوف 3 صفوف Stats + جدول "طلبات تحتاج تدخل".

- [ ] **فحص 2 — الأرقام منطقية:** لو في طلبات في DB → لازم الأرقام تطلع صح.

- [ ] **فحص 3 — Polling شغال:** استنى 15 ثانية → الأرقام بتتحدث (شوف Network tab في DevTools).

## 📝 توثيق التيكت الثامنة (إلزامي)
- [ ] سجّل كل ملف Widget (4 ملفات) بالمحتوى الكامل.

---
---

# 🎟️ التيكت التاسعة: الرسوم البيانية (Charts) + الفحص النهائي الشامل

> **الهدف:** بناء Charts أسبوعية وشهرية + الفحص النهائي الشامل.
>
> **الاعتماديات:** التيكت الثامنة.
> **التقدير الزمني:** ساعتين.

---

## 9.1 — رسم بياني أسبوعي

### 9.1.1 إنشاء `app/Filament/Widgets/WeeklyOrdersChart.php`
- [ ] **ملف جديد:** `app/Filament/Widgets/WeeklyOrdersChart.php`
- [ ] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Filament\Widgets;

  use App\Enums\OrderType;
  use App\Models\Order;
  use Filament\Widgets\ChartWidget;
  use Illuminate\Support\Carbon;

  class WeeklyOrdersChart extends ChartWidget
  {
      protected static ?string $heading = 'الطلبات — آخر 7 أيام';
      protected static ?int $sort = 6;
      protected int | string | array $columnSpan = 'full';

      protected function getData(): array
      {
          $days = collect();
          $standardCounts = array_fill(0, 7, 0);
          $errandCounts = array_fill(0, 7, 0);
          $startDate = Carbon::today()->subDays(6);

          // استعلام واحد مجمع لتجنب N+1 queries
          $orders = Order::where('created_at', '>=', $startDate)
              ->selectRaw('DATE(created_at) as date, order_type, count(*) as count')
              ->groupBy('date', 'order_type')
              ->get();

          for ($i = 6; $i >= 0; $i--) {
              $dateObj = Carbon::today()->subDays($i);
              $dateString = $dateObj->toDateString();
              $days->push($dateObj->translatedFormat('l'));

              $index = 6 - $i;
              foreach ($orders as $order) {
                  if ($order->date === $dateString) {
                      $typeValue = $order->order_type instanceof \App\Enums\OrderType 
                          ? $order->order_type->value 
                          : (string) $order->order_type;

                      if ($typeValue === OrderType::Standard->value) {
                          $standardCounts[$index] = $order->count;
                      } else {
                          $errandCounts[$index] = $order->count;
                      }
                  }
              }
          }

          return [
              'datasets' => [
                  [
                      'label' => 'طلبات عادية',
                      'data' => $standardCounts->toArray(),
                      'borderColor' => '#3b82f6',
                      'backgroundColor' => 'rgba(59, 130, 246, 0.1)',
                  ],
                  [
                      'label' => 'مشاوير',
                      'data' => $errandCounts->toArray(),
                      'borderColor' => '#f97316',
                      'backgroundColor' => 'rgba(249, 115, 22, 0.1)',
                  ],
              ],
              'labels' => $days->toArray(),
          ];
      }

      protected function getType(): string
      {
          return 'line';
      }
  }
  ```

---

## 9.2 — رسم بياني شهري

### 9.2.1 إنشاء `app/Filament/Widgets/MonthlyRevenueChart.php`
- [ ] **ملف جديد:** `app/Filament/Widgets/MonthlyRevenueChart.php`
- [ ] **المحتوى الكامل:**
  ```php
  <?php

  namespace App\Filament\Widgets;

  use App\Enums\OrderStatus;
  use App\Models\Order;
  use Filament\Widgets\ChartWidget;
  use Illuminate\Support\Carbon;

  class MonthlyRevenueChart extends ChartWidget
  {
      protected static ?string $heading = 'الإيرادات — آخر 30 يوم';
      protected static ?int $sort = 7;
      protected int | string | array $columnSpan = 'full';

      protected function getData(): array
      {
          $labels = collect();
          $revenues = collect();

          for ($i = 29; $i >= 0; $i--) {
              $date = Carbon::today()->subDays($i);
              $labels->push($date->format('m/d'));

              $dayRevenue = Order::whereDate('created_at', $date)
                  ->whereIn('status', [OrderStatus::Delivered, OrderStatus::PartiallyDelivered])
                  ->sum('total_amount');

              // تحويل من القرش للريال
              $revenues->push(round($dayRevenue / 100, 2));
          }

          return [
              'datasets' => [
                  [
                      'label' => 'الإيرادات (ريال)',
                      'data' => $revenues->toArray(),
                      'backgroundColor' => 'rgba(34, 197, 94, 0.6)',
                      'borderColor' => '#22c55e',
                  ],
              ],
              'labels' => $labels->toArray(),
          ];
      }

      protected function getType(): string
      {
          return 'bar';
      }
  }
  ```

---

## 9.3 — الفحص النهائي الشامل (Final Acceptance Test)

> **⚠️ هذا الفحص لازم يتعمل بعد ما كل التيكتات الثمانية السابقة تكون مغلقة ومُوثّقة.**

### المتطلبات: 3 terminals مفتوحة
```
Terminal 1: php artisan serve
Terminal 2: php artisan reverb:start
Terminal 3: php artisan queue:work
```

- [ ] **1. Redis شغال:** `docker compose exec redis redis-cli ping` → `PONG`
- [ ] **2. الداشبورد يفتح:** `http://localhost:8000/admin` → بدون أخطاء.
- [ ] **3. Widgets ظاهرة:** الصفحة الرئيسية فيها Stats + Charts.
- [ ] **4. خريطة التتبع موجودة:** "خريطة التتبع" في Navigation → تعرض خريطة.
- [ ] **5. إنشاء طلب → Toast:** افتح tab تاني للداشبورد → أنشئ طلب من الأول → Toast يظهر في الثاني فوراً.
- [ ] **6. Browser Notification:** المتصفح بيسأل عن إذن الإشعارات → وافق → FCM token يتحفظ.
  ```bash
  php artisan tinker --execute="echo App\Models\FcmToken::count();"
  ```
  لازم يرجع 1 على الأقل.
- [ ] **7. التوزيع الآلي (لو dispatch_mode=auto):**
  ```bash
  php artisan tinker --execute="app(App\Settings\GeneralSettings::class)->dispatch_mode = 'auto'; app(App\Settings\GeneralSettings::class)->save();"
  ```
  ثم أنشئ طلب → لو في مندوب online → يتعين أوتوماتيك.
- [ ] **8. Charts تعرض بيانات:** الرسوم البيانية تعرض بيانات حقيقية (لو في طلبات في DB).
- [ ] **9. Polling شغال:** الأرقام بتتحدث كل 15-30 ثانية بدون Refresh.
- [ ] **10. php artisan serve — بدون أي Error أو Warning.**

---

## 📝 توثيق التيكت التاسعة (إلزامي)
- [ ] سجّل ملف `WeeklyOrdersChart.php` بالمحتوى الكامل.
- [ ] سجّل ملف `MonthlyRevenueChart.php` بالمحتوى الكامل.
- [ ] سجّل كل نتائج الفحص النهائي الشامل (كل سطر أعلاه ونتيجته).

---

## 📊 إحصائيات ملف التوثيق النهائي
- [ ] **تأكد أن `PHASE6_CHANGELOG.md` يحتوي على 9 أقسام** (قسم لكل تيكت).
- [ ] **تأكد أن كل قسم فيه:** ملفات جديدة + ملفات معدّلة + أوامر مُنفّذة + نتائج فحص.
- [ ] **تأكد أن الملف قابل للقراءة** (مُنسّق بـ Markdown صحيح مع syntax highlighting).

---

## 📈 ملخص الإنجاز النهائي

| الإحصائية | العدد |
|-----------|------|
| **تيكتات هندسية** | 9 |
| **ملفات جديدة** | ~25 |
| **ملفات معدّلة** | ~12 |
| **Migrations** | 2 (fcm_tokens + settings) |
| **حزم جديدة** | 3 (predis, reverb, echo+pusher) |
| **Events** | 3 |
| **Widgets** | 6 (4 Stats + 1 Table + 1 بعد Phase 6) |
| **Charts** | 2 |
| **وقت مقدّر** | ~24 ساعة |
