# Tasarım `17` — Müşteri: Açık Eğitimler · kapsama matrisi

*03.08.2026 · `design/17 Musteri Acik Egitimler.dc.html` yeniden açıldı ve baştan sona okundu; matris bellekten değil dosyadan üretildi (Tasarım Uygulama Protokolü §3).*

Adres: `/portal/acik-egitimler` · Ekran: `resources/js/pages/Portal/OpenTrainings.vue` · Okuyucu: `app/Queries/Customer/OpenTrainings.php` · Eylem: `app/Actions/Customer/RequestSeats.php`

## Başlık ve dört kırmızı kural

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| Başlık "Açık eğitimler" + alt cümle (fiyat teklifle gelir) | ✓ | `OpenTrainings.vue:164-168`, `lang/tr/portal.php` `open_trainings.title/subtitle` |
| Sağ üstte uyum cümlesi ("9 personelin süresi 90 gün içinde doluyor") | ✓ | `OpenTrainings.vue:171-184`; sayı `ComplianceSummary`, gün sayısı firmanın kendi ayarı (`OpenTrainings.php:59-70`) |
| ⛔ fiyat yok | ✓ | Payload'da hiçbir fiyat alanı yok — test: `OpenTrainingTest.php` "lists an open upcoming training…" son satırı |
| ⛔ doluluk yüzdesi yok — kalan yer | ✓ | `seats_left = capacity − confirmed_seats` (`OpenTrainings.php:135-136`), ekranda `seats_left` (`OpenTrainings.vue:349-355`) |
| ⛔ doğrudan kayıt yok | ✓ | Tek POST teklif isteği doğurur (`routes/web.php` `open_trainings.request` → `RequestSeats` → `RequestQuote`) |
| ⛔ süzgeçte kişi adı yok | ✓ | `needs()` yalnız sayar (`OpenTrainings.php:214-247`); test: "counts the people who need a training and never names them" |

## Süzgeç şeridi

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| "Personelimin ihtiyacı olanlar" — **varsayılan açık**, işaretli hap | ✓ | `OpenTrainingController.php` `needed_only => ! $request->has('tumu')`; `OpenTrainings.vue:192-206` |
| Yanında durumu anlatan cümle ("Açık geldi — 4 eğitim türünde…") | ✓ | `OpenTrainings.vue:208-212`, `needed_filter_on/off` |
| İl süzgeci, lokasyon illeriyle ön dolu, ✕ ile kaldırılabilir | ✓ | Ön dolgu `homeProvinces()` (`OpenTrainings.php:262-289`), ekranda `MultiSelect` (kendi ✕'i var) |
| Eğitim türü ▾ | ✓ | `OpenTrainings.vue` tür `SelectInput` + `definitionOptions()` |
| Tarih ▾ | ✓ | Handoff'un kendi **date-range** kontrolü (`01 Form Elemanlari` → Date-range), kapalı hâlde: `DateRangePicker :inline="false"`. ⛔ İlk uygulamada iki ayrı `DatePicker` konmuştu — düzeltildi (Sets, 03.08.2026) |
| Sırala ▾ (en yakın tarih) | ✓ | `sort` filtresi, iki seçenek: en yakın tarih (varsayılan), en çok yer (`OpenTrainings.php:178-190`) |
| "İl süzgeci … ön dolu geldi (Hatay · Kocaeli) — değiştirilebilir. **4 eğitim** listeleniyor." | ✓ | `OpenTrainings.vue:255-261` (`prefilled` / `prefilled_everywhere` + `listing`) |

## Eğitim kartı (2 kolonlu ızgara)

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| `grid-template-columns: repeat(2,1fr)` | ✓ | `OpenTrainings.vue` `grid gap-3.5 lg:grid-cols-2` |
| Statü rozeti: BAKANLIK ONAYLI / PLANLANDI / YER KALMADI | ✓ | `statusKey()`/`statusTone()`, `lang` `statuses.*` (dört durum, bildirildi dahil) |
| "İHTİYACINIZ VAR · N KİŞİ" rozeti | ✓ | `needed_badge`, `row.needed_by` |
| Sağ üstte referans kodu, mono | ✓ | `font-mono` + `row.reference_code` |
| Eğitim adı | ✓ | `row.definition` |
| "Sertifikada Görev 5 ve 6 kapsanır · 2 yıl geçerli" | ✓ | `covers` (training_coverage) + `valid_months` → `valid_years` cümleye çevriliyor |
| Tarih satırı: tarih · saat · oturum sayısı | ✓ | `starts_on/ends_on`, `hours` (oturumlardan), `sessions` |
| Yer satırı: il/ilçe **kalın** + altında adres | ✓ | `row.province` + `row.address` |
| Eğitmen satırı — atanmamışsa **yazılmaz** | ✓ | Yalnız `confirmed` atama okunur; `v-if="row.instructor"` |
| Bakanlık onayı bekleyen kartta mavi not | ✓ | `awaiting_ministry` şeridi |
| Alt şerit: büyük "16 kişilik yer" + "kalan kontenjan" + CTA | ✓ | `OpenTrainings.vue:346-380`; 3 ve altındaki kalan `caution` tonunda |
| Dolmuş kart listede **kalır**, soluk, "Kontenjan doldu" + "Aynı eğitim tekrar açılabilir" | ✓ | `opacity-90` + `full_title`/`full_body`; test: "keeps a training whose seats have gone, marked full" |
| Dolmuş kartın eylemi **"Yeni Tarih İste"** (ikincil) | ✓ | `row.full ? request_new_date : request_seats`, `variant="secondary"` |

## Fiyat açıklaması ve paket şeridi

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| "Fiyat neden yazmıyor?" şeridi | ✓ | `why_no_price_title` + `why_no_price` |
| "Listede olmayan bir eğitim mi lazım? →" → teklif isteği | ✓ | `/portal/teklif-istegi` bağlantısı |
| Paket bloğu **mor kenarlıkla** ayrışır | ✓ | `border-role-border` + `bg-role-bg` (mor token seti `--color-role-*`) |
| Başlık + "fiyat yine yazmaz" notu + "Tüm paketler (6) →" | ✓ | `packages_title/note/all`, `packageCount` gerçek sayı |
| 3 kart, `repeat(3,1fr)` | ✓ | `grid md:grid-cols-3`, controller `array_slice($packages, 0, 3)` |
| Kart rozetleri: tür · SAHİPSİNİZ / N ÖRNEK | ✓ | `content_package.kinds.*`, `packages_held`, `packages_samples` |
| Sahip olunanda "Kütüphaneyi aç →", diğerlerinde İçindekiler + Teklif iste | ✓ | `one.held` dallanması |

## "Kontenjan iste" modalı

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| Başlık "Kontenjan iste" + "Açık eğitim · teklif isteği olarak gönderilir" | ✓ | `ConfirmModal` heading/description |
| **SEÇİLİ EĞİTİM — DEĞİŞTİRİLEMEZ** kilit kutusu (ad, tarih, il, kod, kalan) | ✓ | `OpenTrainings.vue` kilit bloğu; sunucu da formdan değil eğitimden okur (`RequestSeats.php`) |
| "Kaç kişi *" — stepper | ✓ | `NumberStepper` (hazır atom; ham `type=number` yok) |
| "16 yerden 6 — yeterli ✓" yardım satırı | ✓ | `modal.enough`; yetmediğinde `modal.more_than_left` — **engel değil**, talep yine gider |
| "Not (opsiyonel)" | ✓ | `TextareaInput`, 2000 karakter |
| KVKK şeridi: "Kişi adı gerekmez — sayı yeterlidir" | ✓ | `modal.kvkk` (+ `kvkk_tag`) |
| "Gönderince… **Yer ayrılmaz**… firma içi onay açıksa önce X'e gider" | ✓ | `modal.footer` + `footer_approver`; onaylayan `PortalScreen::approverName()` |
| Vazgeç / Talebi Gönder | ✓ | `ConfirmModal` cancel/confirm |
| İki kolonlu form ızgarası (150px + 1fr) | ✓ | `sm:grid-cols-3`, not iki kolon (1:2). Piksel değeri token dışı olduğu için ölçeğe oturtuldu |
| Modal genişliği 520px | ✓ | `ConfirmModal`'a `wide` varyantı eklendi (`max-w-lg`, 512px) — Sets onayı, 03.08.2026. Ayrı modal yazılmadı: kaçış tuşu, arka plan ve alt şerit tek yerde kalır |

## Boş hâl

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| "Şu an açık eğitim yok" + açıklama | ✓ | `empty_title` / `empty_body` |
| "İhtiyacımı Bildir" birincil butonu | ✓ | `/portal/teklif-istegi` |
| "İl süzgecini genişlet" ve "Personelimin ihtiyacı süzgecini kapat" bağlantıları | ✓ | `widenProvinces()` / `toggleNeeded()`; yalnız ilgili süzgeç açıkken görünür |
| İkon kutusu | ⚠ | Ortak `EmptyState` ikon taşımıyor; başlık + açıklama + eylem aynı |

## 375px

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| Kart listesi tek kolon | ✓ | `lg:grid-cols-2` — küçük ekranda tek kolon |
| Kalan yer ve tarih ilk iki bilgi | ✓ | Kart `flex` sıralaması: mobilde kalan yer + tarih üstte (`lg:hidden` şerit), sonra ad, detay, rozetler, tam genişlikte buton; `lg:order-*` masaüstü kompozisyonunu geri verir |
| Buton tam genişlik (48px) | ✓ | `w-full lg:w-auto` |
| Süzgeç hapları yatay şerit | ✓ | Şerit `flex-wrap`, dar ekranda alt alta sarar |

## Bağlanan yerler

| Tasarım öğesi | ✓/⚠/❌ | Kanıt |
|---|---|---|
| Menü: "EĞİTİM & TEKLİF" grubunda, "Eğitimler"in hemen altında | ✓ | `PortalNavigation::ITEMS` `open_trainings` (trainings'in hemen ardında); test: "offers the open trainings in the portal menu" |
| Portal panosundaki "bölgemdeki açık eğitimler" alanı | ✓ | `PortalDashboard::for()` → `OpenTrainings::nearby()`; `Dashboard.vue` şeridi + "Tümü →"; test: "feeds the portal dashboard strip from the same rule" |
| Satışçı duyurusunun **ilgili eğitim kartına** düşmesi (çapa) | ⚠ | Kart `id="egitim-{id}"` çapası **var**; duyurunun kendisi ticket #78'de, henüz yazılmadı |
| Uyum sinyalinden bu sayfaya bağlanma | ⚠ | Sayfa hazır ve süzgeç zaten açık geliyor; uyum kartındaki eylem bağlantısı henüz `11`'de değiştirilmedi |
| ⛔ "Hiçbir yerden doğrudan kayıt yok" | ✓ | Portalda kayıt yalnız onaylı teklif sonrası (`EnrolOwnParticipant`); bu ekranın tek yazma yolu teklif isteği |

## Duyuru (#78) — bu turda yazıldı

| Öğe | ✓/⚠/❌ | Kanıt |
|---|---|---|
| Tasarım `30`'daki **"Duyuru Gönder"** butonu | ✓ | `Office/Sales/Agenda.vue` düşük doluluk sinyalinde; yalnız `training.announce` yetkisi varsa çizilir (`SalesAgenda::for` → `can_announce`) |
| Hedef kitle: seçili müşteriler / bölge / o türde süresi yaklaşanlar | ✓ | `AnnouncementAudience` üç kitle; `Office/Sales/Announcement.vue` segmented |
| **Önizleme** (kaç firma, kaç kişi, kim neden atlandı) | ✓ | GET `/satis/acik-egitim/{training}/duyuru`; gönderim ayrı POST |
| Firma başına N gün üst sınırı, varsayılan 14, **admin-tanımlı** | ✓ | `business.training.announcement_gap_days` + `Settings::announcementGapDays()`; test: "skips a company that was told within the administrator gap" |
| Kişi adı gitmez, sayı gider | ✓ | `recipient_ids` tarayıcıya hiç gitmez (controller siler); kayıtta `people_told` sayısı |
| Yalnız **teklif isteyebilen** kişiye gider | ✓ | `whoWouldBeTold()`; test: "skips a company where nobody may ask for a quote" |
| Duyuru **kaydı** ve atlananlar | ✓ | `training_announcements` + `training_announcement_recipients` (atlama gerekçesiyle), audit `training.announced` |
| Müşteri bildirim tercihleri (uygulama içi / e-posta) | ✓ | `/portal/bildirimler` — `PortalNotificationController`, `Portal/Notifications.vue`, hesap menüsünde bağlantı; `training.open_announced` **zorunlu değil** |

⚠ **Tasarımı olmayan tek ekran:** `/portal/bildirimler`. Tasarımcıdan sayfa gelmedi; ofis bildirim ekranının kompozisyonu birebir taşındı (aynı liste + anahtar deseni). Brief istenirse ayrı iş.

## Açık kalan tek satır (Onay Kuralı 6)

⛔ **Kapandı (Sets, 03.08.2026): duyuru yalnız uygulama içi.** E-posta yok — *"in-app bildirimde sorumluluk yok"*. Müşterinin kendi açtığı paneldeki satır yükümlülük doğurmaz; aynı cümle posta kutusuna gidince 6563/İYS'ye tabi ticari elektronik ileti olur. Rıza sorusu cevaplanmıyor, **sorulmuyor**. Kanıt: `NotificationEvent::channels()` bu olay için `['in_app']`; test: "never sends the announcement as mail".
