# Chunk 1 — Müşteri organizasyonu, portal kabuğu, dashboard, personel

*Index: `musteri-portali-plan.md` · Spec: §2, §3, §9.1–9.2 · Tasarım: `11`, `12`*

Sonunda: müşteri panele girer, kendi birim ağacını ve lokasyonlarını kurar, personelini yönetir ve davet eder, iç onay ayarını belirler.

---

## 1. Migration'lar

### 1.1 `customer_units`
`id` · `tenant_id` · `parent_id` (nullable, self FK) · `name` · `is_active` · timestamps.
⛔ `TenantPolicy::apply('customer_units')`.
⚠ **Döngü koruması veritabanında değil action'da** — self-FK döngüyü engellemez; `CreateCustomerUnit`/`MoveCustomerUnit` ata zincirini yürür ve kendi kendine bağlamayı reddeder.

### 1.2 `customer_unit_memberships`
`id` · `tenant_id` · `customer_unit_id` · `customer_user_id` · `role` (`manager` \| `member`) · timestamps.
⛔ `TenantPolicy::apply`. Benzersiz: `(customer_unit_id, customer_user_id)`.
⚠ Bir kişi **birden çok birimde** olabilir; birim yöneticiliği kişinin değil **bu satırın** özelliğidir.

### 1.3 `customer_locations`
`id` · `tenant_id` · `name` · `city_option_value_id` (nullable FK `option_list_values`) · `address` · `is_active` · timestamps.
⛔ `TenantPolicy::apply`. ⚠ Eğitim yeri **değildir** (spec §2.2) — model docblock'u bunu yazar, yoksa ilk okuyan `trainings.address` ile karıştırır.

### 1.4 `customer_approval_settings`
`tenant_id` (PK) · `is_enabled` · `approver_customer_user_id` (nullable FK) · timestamps.
⛔ `TenantPolicy::apply('customer_approval_settings')`.
⚠ Tek satır per tenant. `approver` **nullable**: onaylayan pasifleşince satır silinmez, alan boşalır ve `is_enabled` etkisiz kalır (spec §2.3).

---

## 2. Domain

`app/Domain/Customers/ApprovalRoute.php` — saf sınıf.
```
needsApproval(bool $enabled, ?int $approverId, bool $approverIsActive): bool
```
⛔ Kuralın tamamı: onay **yalnız** açıkken, onaylayan tanımlıyken **ve aktifken** gerekir. Üçünden biri düşerse istek doğrudan 3E'ye gider (spec §2.3).
⚠ Framework'süz (AD-25); `isActive()` sonucunu **parametre olarak** alır, `AccountStatus`'u import etmez.

`app/Domain/Customers/UnitTree.php` — ata zinciri, döngü tespiti, bir birimin altındaki tüm birimler.

---

## 3. Action'lar

| Action | Yetki | Not |
|---|---|---|
| `CreateCustomerUnit`, `RenameCustomerUnit`, `MoveCustomerUnit`, `DeactivateCustomerUnit` | `customer.manage_organization` | Move döngü kontrolü yapar (`UnitTree`) |
| `AssignUnitManager`, `RemoveUnitManager` | `customer.manage_organization` | |
| `CreateCustomerLocation`, `UpdateCustomerLocation`, `DeactivateCustomerLocation` | `customer.manage_organization` | |
| `SetInternalApproval` | `customer.manage_organization` | `is_enabled` + `approver`; ⚠ onaylayan **aktif değilse** kaydetmeyi reddeder ve sebebini söyler |
| `AddPortalPerson` | `customer.manage_people` | `customer_users` üstüne; şifreleme modelde (AD-05) |
| `InvitePortalPerson` | `customer.manage_people` | ✓ mevcut `IssueMagicLink(purpose:'invitation')` yeniden kullanılır — yeni davet mekanizması **yazılmaz** |
| `SetPersonUnits` | `customer.manage_people` | kişinin birim üyelikleri |
| `ChangePortalPersonStatus` | `customer.manage_people` | ⛔ **silme yok** — pasifleştirme (spec M-34, AD-23) |

⛔ Birim/lokasyon **silinmez, pasifleştirilir**: geçmiş talep ve teklifler onlara işaret ediyor.

---

## 4. Query'ler

| Query | Döndürdüğü |
|---|---|
| `Customer\MyOrganisation` | birim ağacı + yöneticiler + üye sayıları |
| `Customer\MyLocations` | lokasyonlar |
| `Customer\MyPortalPeople` | personel listesi; ⚠ TCKN/telefon **maskeli** (AD-16), açma ayrı eylem ve loglanır (AD-21) |
| `Customer\PortalDashboard` | yaklaşan eğitimler (`training_customers` ✓) · iç onay kuyruğum (chunk 3'ten sonra dolar) · uyum kartları |

⛔ **Uyum kartları chunk 2'ye kadar BOŞ DURUM döndürür, sıfır değil** (spec M-28). Query bunu `null` ile ifade eder; ekran `null` görünce *"veri henüz yok"* yazar. Sıfır dönmek *"kimsenin belgesi dolmuyor"* demektir ve yanlıştır.

---

## 5. HTTP

`app/Http/Controllers/Customer/` altında: `PortalController` (dashboard), `OrganisationController`, `LocationController`, `PortalPeopleController`.

Rotalar `routes/web.php`'nin mevcut `auth:customer` + `EnsureMagicLinkChallengeAnswered` + `RequireStrongAuth` grubuna:
```
/portal · /portal/organizasyon · /portal/lokasyonlar · /portal/personel
```
⛔ `CustomerPanelController`'ın JSON stub'ı `/portal`'a devredilir; panel izni kontrolü **korunur** (spec §3.1).

⛔ **Yetki adları YENİ DEĞİL — aile zaten var** (`config/authorization.php:326`): `customer.manage_organization` · `customer.manage_people` · `customer.request_quote` · `customer.approve_quote` · `customer.view_documents` · `customer.upload_documents` · `customer.view_special_category_documents`.

⚠ v1.0 `portal.*` diye yeni adlar uydurmuştu; kod okunmadan yazılmıştı. Mevcut aile kullanılır — ikinci bir isimlendirme, aynı yetkiyi iki adla taşıyan bir sistem demek olurdu. Fatura görüntüleme için ad **yok**; chunk4'te eklenir (`customer.view_invoices`).

---

## 6. Frontend

⛔ **Yeni panel girişi** (P-1): `resources/js/portal.js` + `resources/css/portal.css`, `vite.config.js` input'una eklenir, glob `./pages/Portal/**/*.vue`.

| Dosya | Tasarım |
|---|---|
| `pages/Portal/Dashboard.vue` | `11` sol |
| `pages/Portal/Organisation.vue` | `11` sağ (birim ağacı, yönetici, iç onay, yetki matrisi) |
| `pages/Portal/Locations.vue` | `12` lokasyonlar |
| `pages/Portal/People.vue` | `12` liste + ekleme drawer |
| `organisms/PortalSidebar.vue`, `layouts` | `11` rail |

⛔ Bileşenler **mevcut atom/molekül setinden** gelir: `PhoneInput`, `EmailInput`, `SensitiveInput`, `MultiSelect`, `SegmentedControl`, `DataTable`, `SlideOver`. Yeni bileşen dili icat edilmez; eksik atom çıkarsa **atom yazılır** (Tasarım Uygulama Protokolü adım 2).

---

## 7. Test planı

| Grup | Senaryo |
|---|---|
| `omurga` (unit) | `ApprovalRoute`: üç düşme yolu (kapalı · onaylayan yok · onaylayan pasif) · `UnitTree` döngü tespiti |
| `musteri` | Panele giriş: izinsiz kullanıcı 403, magic link oturumu **panele giremez** · birim aç/taşı/pasifleştir · lokasyon · personel ekle + davet · iç onay ayarı · onaylayan pasifken kaydetme reddi |
| `musteri` | Dashboard: uyum kartları **boş durum** döndürür (M-28), sıfır değil |
| `omurga-rls` | Dört yeni tablo için negatif test: B tenant'ı A'nın birimini/lokasyonunu/üyeliğini/onay ayarını **göremez** ve **yazamaz** |
| `omurga` (arch) | `portal.js` ofis sayfalarını glob'lamıyor; `app/Domain/Customers/` framework'süz |
| `omurga` | ⛔ **audit** (AD-06/AD-21): personel ekleme/pasifleştirme `audit_writes` üretir ve aynı transaction'da yazılır · **TCKN açma eylemi `audit_access`'e düşer** |

---

## 8. Kapsama

| Spec | Karşılığı |
|---|---|
| M-1 | §1.1 + RLS testi |
| M-2 | `ApprovalRoute` + `SetInternalApproval` reddi |
| M-3 | rota middleware (mevcut) + `musteri` grubu testi |
| M-28 | `PortalDashboard` null sözleşmesi |
| M-34 | `ChangePortalPersonStatus` + birim/lokasyon pasifleştirme |

⚠ M-44/M-45 (satışçı devri) bu chunk'ta **değil** — chunk 4, çünkü uyarının sayacağı işler (teklif, talep) orada doğuyor.
