# EGT-65 — Planlama paketi ve tek sayfa planlama ekranı

**Upstream:** `docs/specs/planlama-spec.md` **v1.2** §15 (paket), §16 (ekran), P-44…P-56 · AD-19 (referans kodu), AD-23 (yetki), AD-25 (kod yerleşimi), AD-27 (mevzuat kuralı gömülmez) · `design/01 Form Elemanlari.dc.html`, `design/21 Planlamaci Sihirbaz.dc.html`

**Baseline:** `main` @ CI #816 yeşil (1081 test)

---

## Sözleşme — spawn öncesi pinlenir

| Şey | Ad |
|---|---|
| Tablo | `training_packages`, `training_package_cost_items`, `training_instructor_offer_trainings` |
| Yeni kolon | `trainings.package_id` (nullable, `restrictOnDelete`) |
| Ön ek | `ReferencePrefix::Package = 'PR'` *(Sets, 31.07.2026: `PK` → `PR`; herkesin kullandığı kelime "program")* |
| Model | `TrainingPackage`, `TrainingPackageCostItem` |
| Domain | `PackageWindow`, `PackageCostShare` |
| Action | `PlanTrainings`, `UpdatePackageWindow`, `AddPackageCostItem`, `RecordActualPackageCost`, `DetachTrainingFromPackage` |
| Query | `RegulationOptions`, `PackageDetail`, `PackageCostTable`, `InstructorCapabilityMatrix` |
| Route adı | `office.packages.*`, `office.trainings.plan` |
| Ekran | `Office/Trainings/Plan.vue` (Create.vue'nun yerine), `Office/Packages/Show.vue` |
| Test grubu | `planlama` |

---

## Görev 1 — Şema ve kod öneki

**Dosyalar:**
- Değişir: `app/Domain/Shared/ReferencePrefix.php`
- Yeni: `database/migrations/2026_08_16_000100_create_training_packages.php`
- Yeni: `database/migrations/2026_08_16_000200_create_package_cost_items.php`
- Yeni: `database/migrations/2026_08_16_000300_offers_cover_many_trainings.php`
- Yeni: `app/Models/TrainingPackage.php`, `app/Models/TrainingPackageCostItem.php`
- Değişir: `config/tenancy.php` (yeni tablolar sınıflandırılır — `TenancyTest` tam sayı bekler)

**Kurallar:**
- `training_packages` tenant-scoped **değildir** — eğitim gibi 3E'nin kendi kaydı; açık paket kimseye ait değil. `exempt_tables`'a yazılır, gerekçesiyle.
- Silme tetikleyicisi: altında eğitim olan paket silinemez (AD-11).
- `training_package_cost_items` `training_cost_items` deseninin aynısı: planlanan tutar bir kez yazılır ve donar, gerçekleşen gerekçe ister.

**Adımlar:**
- [ ] Migration'ları yaz, `--database=pgsql_admin` ve `--env=testing` ile koş
- [ ] Model + factory
- [ ] `config/tenancy.php`'ye ekle
- [ ] `./vendor/bin/pest --group=omurga-rls` (TenancyTest, SchemaConformanceTest)

---

## Görev 2 — Domain kuralları

**Dosyalar:** `app/Domain/Trainings/PackageWindow.php`, `app/Domain/Trainings/PackageCostShare.php`

`PackageWindow`: bir eğitim tarihi pencerenin içinde mi (P-49). Framework import etmez.

`PackageCostShare`: ortak masrafı **kontenjan oranında** dağıtır (§15.3). Kesinleşen koltuğa göre değil — o sayı satış boyunca değişir ve geçmiş maliyet tablosunu oynatır.

**Adımlar:**
- [ ] İki sınıf + saf birim testleri (`tests/Unit/Planlama/`)

---

## Görev 3 — Çok eğitimli planlama

**Dosyalar:** `app/Data/Office/PlannedTraining.php`, `app/Data/Office/NewPlan.php`, `app/Actions/Office/PlanTrainings.php`

`PlanTrainings` bir **liste** alır. Bir eğitimse paket doğmaz (`package_id` null). İkiden fazlaysa önce paket açılır, `PK` kodu verilir, her eğitim ona bağlanır.

⚠ `PlanTraining` (tekil) **kalır** — `PlanTrainings` onu çağırır. Kontenjan tavanı, kapalı-eğitim-firma kontrolü ve mevzuat sürümü dondurma zaten orada; kopyalamak iki yerde ayrışacak bir kural üretirdi.

**Adımlar:**
- [ ] Data nesneleri
- [ ] `PlanTrainings` + tek transaction
- [ ] Testler: tek eğitim paketsiz · iki eğitim paketli · pencere dışı tarih reddedilir · biri düşerse hiçbiri yazılmaz

---

## Görev 4 — Paket masrafı ve iki mali tablo

**Dosyalar:** `app/Actions/Office/AddPackageCostItem.php`, `RecordActualPackageCost.php`, `app/Queries/Office/PackageCostTable.php`

**Adımlar:**
- [ ] Action'lar (audit'li, yetki `training.update.*`)
- [ ] `PackageCostTable`: paket toplamı + eğitim başına pay
- [ ] `TrainingDetail`'e paket payı eklenir, **türetilmiş olduğu işaretli**
- [ ] Testler: donmuş planlanan tutar · gerekçesiz gerçekleşen reddi · pay toplamı ortak masrafa eşit

---

## Görev 5 — Eğitmen: kabiliyet matrisi ve çoklu teklif

**Dosyalar:** `app/Queries/Office/InstructorCapabilityMatrix.php`, `app/Actions/Office/SendInstructorOffers.php` (değişir), `AcceptInstructorOffer.php` (değişir)

Matris: paketteki her eğitim için eğitmenin verip veremediği (§15.5).
Teklif bir eğitim **kümesine** gider; kabul atomiktir, kümedeki her eğitim için atama doğar.

⚠ Küme kesişimdir — teklif yalnız eğitmenin verebildiği eğitimleri içerir.

**Adımlar:**
- [ ] Matris sorgusu
- [ ] Pivot üzerinden teklif gönderme ve kabul
- [ ] Testler: 3'ünü biri 4.'sünü başkası · şartı taşımayana teklif gitmez · kabul atomik

---

## Görev 6 — Ekranlar

**Dosyalar:** `resources/js/pages/Office/Trainings/Plan.vue` (yeni), `Office/Packages/Show.vue` (yeni), `Create.vue` (silinir), `app/Http/Controllers/Office/TrainingController.php`, `PackageController.php`, `routes/office.php`

`Plan.vue`: tek sayfa, altı bölüm, adım yok (P-44). İlk alan mevzuat (`SearchableSelect`, süzgeç — kilit değil), sonra eğitim satırları (her biri kendi kontenjanı ve tarihi), yer ve pencere (`DateRangePicker`), görünürlük, eğitmen, ortak masraf.

⚠ `WizardSteps.vue` **silinmez** — tasarımda tanımlı, katılımcı kaydı ve teklif akışlarında kullanılacak.

**Adımlar:**
- [ ] `RegulationOptions` sorgusu; `TrainingDefinitionOptions` mevzuata göre süzülür
- [ ] `Plan.vue`, `Packages/Show.vue`
- [ ] Controller + rota + `FormRequest`
- [ ] Çeviriler (tr **ve** en — `LocaleTest` eşitlik ister)
- [ ] Ekran testleri

---

## Görev 7 — Kapanış

- [ ] Wire-up: her yeni public symbol'ün production caller'ı reverse-grep ile doğrulanır
- [ ] `proje-haritasi.md` §4.3 güncellenir
- [ ] Pint · PHPStan · gruplar · `--parallel` koşusu
- [ ] TeamCity yeşil, sonra `Fixed`

---

## Kabul kriteri izlemesi

| # | Görev |
|---|---|
| P-44, P-45, P-46 | 6 |
| P-47, P-48, P-49 | 1, 3 |
| P-50, P-54 | 2, 4 |
| P-51, P-52 | 5 |
| P-53 | 3 |
| P-55, P-56 | değişiklik yok — bildirim eğitim başına kalır; pencere değişimi 4'te KUMS bayrağı doğurur |
