# Invoice Sale Cost Centers – Implementation Plan

## الهدف
إضافة زر **"مركز التكلفة"** في شاشة:

- `admin/invoice_sale/add`

بحيث يفتح Popup يعرض **كل قيود اليومية التي ستُنشأ عند حفظ فاتورة المبيعات** (`InvoiceSaleController::postAdd`) مع إمكانية توزيع مراكز التكلفة بنفس تجربة:

- `admin/journals/add`

مع شرط مهم:

1. إذا لم يدخل المستخدم على شاشة مركز التكلفة: يتم الحفظ تلقائياً من إعدادات الحساب الافتراضية (`account_cost_centers`) كما هو الحال الحالي.
2. إذا دخل المستخدم على شاشة مركز التكلفة: يجد التوزيع الافتراضي جاهزًا وقابلًا للتعديل، ويتم حفظ تعديله بدل التوزيع الافتراضي.

---

## الفكرة السليمة (معمارياً)

### 1) توحيد بناء سطور القيود قبل الحفظ
في `InvoiceSaleController` يتم إنشاء قيود متعددة داخل `postAdd` (حساب دفع، احتجاز، مبيعات، ضريبة، وقد تظهر قيود المخزون عند الجرد المستمر).

المطلوب: استخراج منطق "ما هي سطور اليومية" إلى دالة خاصة واحدة مثل:

- `buildInvoiceJournalLines(array $payload): array`

وترجع مصفوفة مرتبة ثابتة من السطور:

- `index` (ترتيب السطر)
- `account_id`
- `type` (1 دائن / 2 مدين)
- `amount`
- `label`

هذا مهم جدًا لأن نفس الترتيب سيستخدم في:

- عرض popup (قبل الحفظ)
- تطبيق وحفظ مراكز التكلفة (وقت الحفظ)

> بهذه الطريقة لن يحصل أي mismatch بين ما رآه المستخدم وما تم حفظه فعلياً.

---

### 2) Endpoint مخصص لفتح Popup
إضافة endpoint جديد (مثلاً):

- `POST admin/invoice_sale/cost-centers-preview-lines`

يأخذ مدخلات الشاشة الحالية (العميل، نوع الدفع، الإجماليات، عناصر الفاتورة…) ويرجع HTML/JSON لأسطر اليومية المتوقعة.

لكل سطر:

- يتم جلب مراكز التكلفة الافتراضية للحساب من `AccountCostCenters`
- يتم تجهيزها كنقطة بداية (prefill)
- تُعرض بنفس جزئية `journal_centers_tree` المستخدمة في `journals/add`، مع naming convention مطابق (center_id / camount / mamount / damount).

---

### 3) زر في `invoice_sale/add` + popup
في واجهة الفاتورة:

- إضافة زر: **مركز التكلفة**
- عند الضغط:
  - نعمل AJAX إلى endpoint السابق.
  - نعرض السطور في Modal.
  - كل سطر يحوي:
    - اسم الحساب
    - مبلغ السطر
    - نوعه (مدين/دائن)
    - توزيع مراكز التكلفة

> نفس تجربة `journals/add` بصريًا وسلوكيًا لتقليل التعلم على المستخدم.

---

### 4) إرسال بيانات مركز التكلفة مع الحفظ
في `public/assets/js/invoice.js` عند حفظ الفاتورة:

- إضافة كل حقول popup إلى `FormData` بنفس naming المتداخلة المستخدمة في القيود اليدوية.
- مثال مفاهيمي:
  - `center_id[lineIndex][parentId][]`
  - `camount[lineIndex][parentId][]`
  - `mamount[...]`
  - `damount[...]`

---

### 5) أولوية الحفظ داخل `postAdd`
وقت إنشاء `JournalItems` لكل سطر قيد:

- إذا يوجد مدخلات مراكز تكلفة لهذا `lineIndex`: نحفظها كما أدخلها المستخدم.
- إذا لا يوجد: نرجع لسلوك النظام الحالي (التوزيع الافتراضي من `AccountCostCenters`).

يعني منطق الحفظ يصبح:

1. **Manual override** (من popup)
2. **Default fallback** (من إعداد الحساب)

وهذا يحقق شرطك: حتى لو المستخدم ما دخل popup، الحفظ يظل صحيح وجاهز.

---

### 6) Validation المطلوب إضافته
قبل الحفظ (server-side) لكل سطر تم إدخال مراكز تكلفة له:

- منع التكرار لنفس المركز داخل نفس Parent.
- التأكد أن مجموع النسب = 100 لكل Parent مستخدم.
- التأكد أن المبالغ (`mamount/damount`) توافق نوع السطر (مدين/دائن).

في حال فشل التحقق:

- إرجاع رسالة واضحة للمستخدم وعدم إنشاء القيد.

---

## لماذا هذه الطريقة هي الأفضل؟

- **لا تكسر السلوك الحالي**: fallback موجود دائمًا.
- **قابلة للصيانة**: مصدر واحد لبناء سطور اليومية.
- **قابلة للتوسع**: أي تغيير لاحق في قيود الفاتورة سينعكس تلقائيًا على popup.
- **تجربة مستخدم متسقة**: نفس UX الموجود في `journals/add`.
- **دقة محاسبية أعلى**: المستخدم يعدّل توزيع التكلفة قبل الحفظ الفعلي مباشرة.

---

## ترتيب التنفيذ المقترح (Sprint صغير)

1. Refactor: استخراج دالة بناء سطور اليومية من `postAdd`.
2. Backend endpoint للمعاينة + تجهيز default cost centers.
3. Frontend: زر + Modal + تحميل السطور بالـ AJAX.
4. Frontend: إرسال حقول المراكز مع حفظ الفاتورة.
5. Backend: تطبيق manual override داخل `postAdd` مع fallback.
6. Validation + رسائل الخطأ.
7. اختبار السيناريوهات الأساسية.

---

## سيناريوهات اختبار إلزامية

1. حفظ بدون فتح Popup → يشتغل fallback الافتراضي.
2. فتح Popup بدون تعديل ثم حفظ → نفس fallback لكن عبر بيانات popup.
3. تعديل التوزيع ثم حفظ → يتم حفظ التعديل فعليًا.
4. حالة `retention_value > 0`.
5. حالة `payment_types_id = -1 / -2`.
6. حالة الجرد المستمر (قيود تكلفة المبيعات/المخزون).
7. خطأ تحقق (النسب لا تساوي 100) → منع الحفظ.

