# Müşteri paneli turu — 08.08.2026

Panelde firma yöneticisi olarak gezildi (Arkas Konteyner, `yonetici2@firma2.test`); on ekran açıldı, teklif isteği ve iç onay akışı uçtan uca koşuldu. **Her bulgunun yanında geçen bir test duruyordu.**

## Kırmızı çizgiye yakın olan

⛔ **Ray rozeti başka firmaların tekliflerini sayıyordu.** `quotes` tablosu bilerek RLS taşımıyor (model bunu yazıyor: kapsam sorguda `customer_id` ile kurulur) ve `PortalRailCounts` bunu unutmuştu: `TenantContext::run()` içinde `Quote::where('status','sent')->count()` **veritabanındaki her gönderilmiş teklifi** sayıyor. Kaptan Denizcilik'in rayında "Teklifler 2" yazıyordu; tenant'ın tek bir teklifi yok. Tıklayınca liste boş.

⚠ Testi vardı ve **düşemezdi**: fixture tek firma yaratıyordu, dolayısıyla "hepsi" ile "benimkiler" aynı sayıydı. Teste ikinci bir firmanın teklifi eklendi; düzeltme geri alındığında test düşüyor (doğrulandı).

## Panelin kendisi

⛔ **İki teklif kartı sabit `null` döndürüyordu.** Panonun "Onay bekleyen teklif" ve "Bekleyen iç onay" kartları, bekledikleri dikeyler aylar önce yazılmışken hâlâ *"Veri henüz yok"* diyordu — rayda "Teklifler 2" yazarken aynı ekranda. Uyum kartı ise kayıt varken **hiçbir şey çizmiyordu** (yalnız `notYet()` dalı yazılmıştı).

⛔ **Selamlama satırı her firmaya aynı cümleyi yazıyordu** — *"Uyum kayıtları henüz oluşmadı"* — altındaki kartta 17 kişinin süresi geçmiş görünürken. Tasarım `11` burada özet cümlesini yazar.

⛔ **Tasarım `11`'in üç öğesi eksikti:** bildirim zili (portal `unread_notifications` prop'unu hiç paylaşmıyordu), **"Teklif İste"** birincil düğmesi ve **"İç onay kuyruğum"** panelinin içeriği (cümle + "Onayla ve 3E'ye İlet" + "İncele").

⛔ **Personel listesinin üç kolonu boştu** (son eğitim, sertifika bitiş, uyum) — aynı satır iki kolon solda "3 ay kuralı · süre doldu" derken. Dört durumun kuralı `ComplianceSummary`'nin döngüsünden çıkarılıp `Domain\Customers\ComplianceState`'e alındı; kart ile liste artık aynı cümleyi kuruyor.

⛔ **Eğitimler ekranı yalnız katılımcısı olan eğitimleri listeliyordu.** Pano "yaklaşan eğitim: 2" diyor, liste onları göstermiyordu — ve firmanın personelini o ekrandan eklemesi gerekiyor. Liste artık `training_customers`'tan çıkıyor.

## İç onay akışı — hiç çalışmıyordu

⛔ **Anahtar açılamıyordu:** `SetInternalApproval` "onaylayacak kişi seçilmedi" reddini `InvalidArgumentException` ile atıyordu → ilk tıklamada **500**. Reddediş `ValidationException`'a çevrildi; ekran da onaylayıcı seçilene kadar kaydetmiyor ve bunu söylüyor.

⛔ **Onay verilemiyordu:** `TrainingRequest` rotada bağlanıyor ama `ResolvedWithinTenant` taşımıyordu — bağlama, tenant kapsamı olmadan okuyup patlıyordu. Hem panel kartı hem teklifler ekranı aynı adrese POST ediyor; ikisi de 500 alıyordu.

⛔ **İsteyenin adı her yerde "—" idi:** `morphTo('opened_by')` ilişkinin ADINI `opened_by` yapıyor, dolayısıyla `with('openedBy')` başka bir kovaya yüklüyor ve okuma null dönüyordu. Lazy yükleme çalışıyordu — bu yüzden kimse fark etmemiş.

⛔ **Sihirbaz kendisiyle çelişiyordu:** 2. adımda "önce çatı yönetici onayına düşer" derken 3. adımda "talep 3E satış ekibine düşer" diyordu. Bir de Türkçe ek hatası vardı (*"Ayşe Kaya'in"*).

## Sözlük

⛔ **`portal.permissions.customer.view_invoices` yoktu** — yetki matrisinin kolon başlığında ham anahtar. Kaynağı iki sözlüğün varlığı: `permission.names` (ofis/backoffice kataloğu, satır zaten vardı) ve `portal.permissions` (müşteriye dönük kısa metin, geride kalmıştı).

## Adresler — token'a geçiş *(Sets kararı)*

Panelin bağlı adresleri artık **oturuma bağlı token** taşıyor (AD-23): teklif, eğitim, teklif isteği, personel, grup, lokasyon, kütüphane dokümanı. Çözüm `PortalScreen::fromToken()` üzerinde tek yerde; modele binding **konmadı**, çünkü teklif ve eğitim 3E'nin kendi kayıtları ve ofis onları bugün id ile adresliyor — model üzerinden yapılan bir değişiklik iki tarafı birden çevirirdi.

⚠ **Kalan:** form DEĞERİ olarak taşınan id'ler (paket seçimi, birim/lokasyon çoklu atama, `parent_id`). Adres değil, gövde; ayrı bir tur.

## Üç arch testi

1. Matrislerin sunduğu her yetkinin sözlükte satırı var (`EnumLabelTest`).
2. Polimorfik ilişki, kendisini okuyan metotla aynı adı taşır (`LayerTest`).
3. Rotada bağlanan her tenant modeli kendi tenant'ı içinde çözülür (`LayerTest`).

Üçü de düzeltme geri alındığında düşüyor — tek tek doğrulandı.


## Ek — teklif isteği bildirimleri *(Sets kararı, 08.08.2026)*

⛔ **Bir teklif isteği hiç bildirim üretmiyordu.** `TrainingRequestOpened` olayı kataloğa yazılmış (hedef kitle: ofis) ve **hiçbir yerden tetiklenmiyordu**; iç onay kuyruğuna düşen istek için ise olay hiç yoktu.

Kurulan iki haber:

| Ne zaman | Kime |
|---|---|
| İstek doğrudan 3E'ye gittiğinde · iç onay verildiğinde | Firmanın **satışçısı** (`salesperson`) |
| İstek firma içi onaya takıldığında | **Adı geçen onaylayıcı** (`customer_approver`) |

⚠ `customer_approver` yeni bir alıcı türü ve `customer_manager` bilerek seçilmedi: bekleyen istek **tek bir kişiyi** bekler; dördüncü kişiye "senin yapamayacağın iş bekliyor" demek panelin görmezden gelinmesini öğretir. Tür veritabanı CHECK kısıtına yazılı olduğu için migration ile açıldı.

⚠ Reddedilen iç onayda 3E'ye hiçbir şey gitmez — istek firmadan çıkmadı, firmanın iç kararını 3E'ye bildirmek olurdu.

**Yanında çıkan iki kusur:**

1. Bildirim satırının adresi **yalnız ofis ekranlarını** biliyordu; firma kullanıcısı kendi isteğine dair satırda hiç bağlantı görmüyordu. Panel için ayrı bir adres haritası eklendi (`/teklifler?sekme=internal`).
2. Bildirim ekranı, satır tenant'lı bir kayda işaret ettiğinde **500** veriyordu: kayıtlar tenant kapsamı olmadan okunuyordu. Okuma artık okuyucunun kapsamında (firma kullanıcısı kendi tenant'ında, ofis `forOffice`).

⚠ Metin yok: bildirim metinleri backoffice'te yazılıp yayınlanır (`notification_templates`) ve bilerek seed edilmez; metin yayınlanana kadar panel satırı olayın adını gösterir, e-posta ise *"yayınlanmış metni yok"* diyerek atlanır.


## Ek 2 — katılımcı alanı turu *(08.08.2026)*

⛔ **Katılımcı için ayrı bir panel yok** *(Sets, 08.08.2026)*: *"ofisteki ile aynı mantık. Nasıl operasyoncu her şeyi görüyor, satışçı sadece kendi menüsünü görüyor — katılımcı da sadece katılımcıysa kendi menüsünü görür; hem şirket yetkilisi hem katılımcıysa ikisini görür."* Kabuk tek; menüyü kişinin taşıdığı şey belirler.

Bulunanlar:

| Bulgu | Neydi |
|---|---|
| **U-3 hiç uygulanmamış** | Hem yetkili hem katılımcı olan kişi (demo verisinde firma yöneticisi tam olarak böyle) kendi eğitim ekranına **panelin hiçbir yerinden ulaşamıyordu** — rayda karşılığı yoktu |
| **Katılımcıya kapalı kapılar** | Yalnız katılımcı olan kişiye rayda dört giriş görünüyordu (Panel, Teklifler, Eğitimler, Açık Eğitimler); dördü de izin taşımıyor, dolayısıyla filtreye takılmıyor ve dördü de 403 veriyordu |
| **Giriş yanlış yere indiriyordu** | Şifreyle giriş ve ikinci faktör herkesi panelin köküne indiriyordu; katılımcı 403 alıyor, o sayfadaki tek düğme onu **yine aynı 403'e** götürüyordu. Magic link akışı bu soruyu zaten soruyordu, şifre akışı hiç sormamış |
| **Davet ekranı stub kalmış** | Blade sayfasının kendi notu *"portal turu bu görünümü değiştirir"* diyordu; tur aylar önce indi, stub kaldı — yeni bir firma kullanıcısının gördüğü **ilk ekran** logosuz, kabuksuz ve devamsızdı |
| **Demo oturum saati** | Yoklaması işlenmiş eğitimin oturumu (`ScheduleSessions` haklı olarak dokunmaz) eski UTC saatini taşıyordu: katılımcı ekranında `12:00–20:00` görünüyordu |

Kurulan: katılımcı ekranları portal kabuğuna alındı (`ParticipantLayout` kaldırıldı), ray girişi **veriden** türüyor (`training_enrolments`), şirket girişleri panel kapısı + kendi izinleriyle; hesap satırı artık kişinin ne olduğunu söylüyor (*Katılımcı* / *Firma paneli*).

⚠ `ParticipantStatus` sorusunu artık **kişinin kendi tenant'ında** soruyor: ray bunu her portal sayfasında soruyor, ambient kapsam çözülmeyen adreslerde okuma cevap vermek yerine patlıyordu.
