v10 — TAM DOKÜMANTASYON 24 Şubat 2026

Dinamik İçerik Sistemi — Eksiksiz Rehber

Bu doküman, projeyi hiç bilmeyen bir geliştiricinin veya AI'ın sıfırdan anlayıp devam edebileceği şekilde yazılmıştır. Tüm kararlar, hedefler, mimari, DB şeması, kod örnekleri, UX kuralları ve TODO listesi içerir.

1. Proje Nedir?

Muzibu Platformu

Muzibu, Türkiye merkezli bir B2B müzik platformudur. İşletmelere (kafe, restoran, mağaza, spor salonu, otel vb.) lisanslı arka plan müziği hizmeti sunar. Soundtrack Your Brand modeline benzer: bireysel kullanıcı yoktur, her kullanıcı bir işletme sahibi veya çalışanıdır.

Teknik Altyapı
  • Framework: Laravel 11 + stancl/tenancy (multi-tenant)
  • Tenant ID: 1001 (Muzibu)
  • Tenant DB: tenant_muzibu_1528d0
  • Central DB: tuufi_4ekim (tuufi.com)
  • PHP: 8.3 (/opt/plesk/php/8.3/bin/php)
  • Test domain: mztest.muzibu.com
  • Production: muzibu.com
  • Admin: Tabler.io + Bootstrap + Livewire
  • Frontend: Alpine.js + Tailwind CSS
  • Müzik: HLS şifreli stream (AES-128)
  • Cache: Redis (tenant prefix ayrı)
  • Arama: Meilisearch
Multi-Tenant Yapı
  • Central: tuufi.com (admin paneli, tenant yönetimi)
  • Tenant 1001: muzibu.com (müzik platformu)
  • Tenant 2: ixtif.com (endüstriyel — farklı sektör)
  • Her tenant kendi DB, kendi storage, kendi Redis prefix
  • Migration: hem central hem tenant klasöründe olmalı
  • Model connection: 'tenant'
  • Route: MuzibuServiceProvider domain-specific yükler
  • Tenant kontrolü: tenant()->id === 1001

İş Modeli

B2B Only: Guest/bireysel kullanıcı yok. Herkes bir işletmeye bağlı.

Abonelik: İşletmeler aylık/yıllık abonelik satın alır. Kurumsal hesaplar (MuzibuCorporateAccount) altında branch'ler olabilir.

Sektör: Her işletme bir sektöre aittir (Kafe, Spor Salonu, Otel vb.). İçerik bu sektöre göre kişiselleştirilir.

Cihaz profili: Kötü cihazlar (düşük RAM, eski tablet) yaygın — performans kritik. Müşteriler donma/takılma şikayeti yapıyor.

2. Hedefler ve Motivasyon

Ana Hedef

Muzibu anasayfasını dinamik, kişiselleştirilmiş ve zamana duyarlı bir içerik sistemiyle değiştirmek. Şu anda anasayfa statik: hep aynı playlistler, albümler, radyolar gösteriliyor. Yeni sistemde:

1
Saate Göre İçerik: Sabah 08:00'de "Günaydın Kafe Mix", akşam 20:00'de "Lounge & Chill" gösterilir. Admin her koleksiyon için başlangıç/bitiş saati belirler.
2
Sektöre Göre İçerik: Kafe sahibi farklı, spor salonu sahibi farklı koleksiyonlar görür. Sektör bazlı filtreleme.
3
Karışık İçerik Tipleri: Tek koleksiyonda Radyo + Playlist + Albüm + Genre + Sektör karışık olabilir (polimorfik).
4
Canlı Güncelleme: Kullanıcılar sayfayı gündüz açıp gece kadar yenilemeden bırakıyor. Her saat başı sayfa sessizce güncellenmeli, müzik kesilmemeli.
5
Kişisel Karşılama: "Günaydın, Ahmet! Kafe & Restoran için seçilmiş içerikler" — saate + kullanıcı adına + sektöre göre.

Kritik Kısıtlar

Kötü Cihazlar
Müşterilerin çoğu eski tablet, düşük RAM'li bilgisayar kullanıyor. Donma şikayetleri var. Frontend JS minimum olmalı — interval yok, polling yok, framework yok.
Müzik Kesilmemeli
Sayfa yenilemesi = müzik durur. Bu kabul edilemez. İçerik güncellemesi page reload olmadan yapılmalı (innerHTML swap).
SPA Davranışı
Kullanıcılar sabah sayfayı açıp gece kadar yenilemiyor. İçerik saate bağlı olduğu için "stale content" problemi var. Otomatik güncelleme şart.
Canli Sistem
Bu production sistemi. Migration'lar 3 aşamalı onay gerektirir. Service restart yasak. Redis FLUSHALL yasak.

Ne Değişecek?

Şu Anki Durum (Statik)

HomeController: Sabit sorgular (son 10 playlist, son 10 albüm, popüler şarkılar...)

Herkese aynı anasayfa gösteriliyor

Saat/sektör farkı yok

Sayfa yenilenmedikçe içerik değişmez

Greeting/karşılama yok

Yeni Sistem (Dinamik)

SmartFeedService: Saat + sektör + iş kurallarına göre filtreleme

Her kullanıcı kendi sektörüne uygun içerik görür

Sabah/öğle/akşam farklı koleksiyonlar

Her saat başı sessiz otomatik güncelleme

"Günaydın, Ahmet!" kişisel karşılama

3. Mimari Genel Bakış

Sistem Akışı

┌──────────────────────────────────────────────────────────────────────┐
│                        KULLANICI TARAYICISI                          │
│                                                                      │
│  1. Sayfa yüklenir → PHP greeting + koleksiyonları render eder       │
│  2. setTimeout (saat başı +1dk) → fetch('/api/feed-partial')         │
│  3. Sunucu yeni saatle HTML döner → innerHTML swap                   │
│  4. Müzik kesilmez, sadece #feed-container güncellenir               │
│                                                                      │
└──────────────┬───────────────────────────────────┬───────────────────┘
               │ İlk yükleme                       │ Saat başı AJAX
               ▼                                   ▼
┌──────────────────────────────────────────────────────────────────────┐
│                         LARAVEL BACKEND                              │
│                                                                      │
│  HomeController@index          /api/feed-partial (closure)           │
│       │                              │                               │
│       └──────────┬───────────────────┘                               │
│                  ▼                                                    │
│         SmartFeedService                                             │
│         ├── getGreeting(User) → selamlama + isim + sektör            │
│         └── getFeed(User)                                            │
│              ├── matchesTimeRules(collection, hour)                   │
│              ├── matchesSectorRules(collection, user)                 │
│              └── matchesBusinessRules(collection, user)               │
│                                                                      │
│         Cache: smart_feed:{userId}:{sectorId}:{hour} (TTL: 300s)     │
│                                                                      │
└──────────────────────────────┬───────────────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────────────┐
│                       VERİTABANI (tenant_muzibu_1528d0)              │
│                                                                      │
│  users ──sector_id──▶ muzibu_sectors                                 │
│                                                                      │
│  muzibu_content_collections (koleksiyonlar)                           │
│    ├── display_rules JSON: {start_hour, end_hour, days, is_always}   │
│    ├── sector_rules JSON: {mode, sector_ids, show_to_all}            │
│    └── business_rules JSON: {subscription, min_months}               │
│                                                                      │
│  muzibu_collection_items (polimorfik öğeler)                         │
│    ├── itemable_type: Playlist | Album | Radio | Genre | Sector      │
│    └── itemable_id: ilgili tablonun PK'sı                            │
│                                                                      │
│  Mevcut tablolar: muzibu_playlists, muzibu_albums, muzibu_radios,    │
│                   muzibu_genres, muzibu_sectors, muzibu_songs...      │
└──────────────────────────────────────────────────────────────────────┘

Koleksiyon Kavramı

Koleksiyon = İçerik gruplarını organize eden bir kapsayıcı. Admin panelden oluşturulur. İçine 5 farklı tipte öğe eklenebilir. Hangi saatte, hangi sektöre, hangi abonelik seviyesine gösterileceği JSON kurallarla belirlenir.

Örnek Koleksiyonlar:
daypart Sabah Kafe Mix — 06:00-12:00, Kafe & Restoran sektörü, içinde: Jazz FM radyo + Sabah Akustik playlist + Kahve Molası playlist
curated Editörün Seçimi — 7/24 (is_always:true), tüm sektörler, içinde: 3 playlist + 2 albüm + 1 radyo + 1 genre
sector Spor Motivasyon — 7/24, sadece Spor Salonu sektörü, içinde: Workout playlist + EDM radyo + Hip-Hop genre
featured Bu Haftanın Yenileri — 7/24, tüm sektörler, içinde: yeni albümler + yeni playlistler

4. Kesinleşen Kararlar (14 Madde)

Kullanıcıyla 9 versiyon boyunca tartışılıp kesinleşen kararlar:

K1 — B2B Only: Her kullanıcı bir işletme sahibi. Guest/bireysel yok. Soundtrack Your Brand modeli.
K2 — 5 Polimorfik Tip: Playlist, Album, Radio, Genre, Sector. Aynı koleksiyonda karışık kullanılır. morphTo ilişki.
K3 — Users Sektör: users.sector_id FK → muzibu_sectors.sector_id. Kullanıcı profilinden seçer. NULL olabilir.
K4 — Esnek Saat Aralığı: Sabit slot isimleri (sabah/öğle/akşam) yok. Admin start_hour (int 0-23) ve end_hour (int 0-23) seçer. Farklı koleksiyonlar farklı saat aralıklarına sahip olabilir. is_always: true ise saat yoksayılır.
K5 — Koleksiyon Tipleri: curated (el seçimi), daypart (zamana göre), sector (sektöre göre), featured (öne çıkan). ENUM olarak DB'de saklanır.
K6 — Tıklama Davranışları: Radio → hemen çalar (sayfa değişmez, player bar güncellenir). Playlist/Album → şarkı listesi sayfası. Genre → genre sayfası. Sector → sektör sayfası.
K7 — "Tümü" Butonu: Koleksiyonda 6'dan fazla öğe varsa "Tümü" butonu görünür → /koleksiyon/{slug} grid sayfası açılır. 6 ve altı → buton gizli, yatay scroll yeterli.
K8 — Sıralama: Koleksiyon içi öğeler: position (sürükle-bırak, Sortable.js). Koleksiyonlar arası: priority (büyük sayı = önce gösterilir).
K9 — Cache: Key: smart_feed:{userId}:{sectorId}:{hour}, TTL: 300 saniye. Saat değişince key de değişir → otomatik invalidate.
K10 — Koleksiyon Sayfası: GET /koleksiyon/{slug} → Grid görünüm, aynı tıklama kuralları geçerli.
K11 — Zaman Güncelleme: Tek setTimeout, her saat başı +1 dakikada tetiklenir. setInterval yok, polling yok. Client RAM: ~8 byte. CPU: sıfır.
K12 — Otomatik Sessiz Yenileme: Sadece anasayfa #feed-container div'i güncellenir. fetch('/api/feed-partial') → sunucu HTML render → innerHTML swap. Page reload yok, müzik kesilmez. İç sayfalarda (playlist detay, albüm detay vb.) hiçbir şey yapılmaz.
K13 — Admin Saat Gösterimi: Dropdown'larda sadece düz saatler: 00:00, 01:00, 02:00 ... 23:00. Dakikalı/yarım saatli seçim yok.
K14 — Greeting Sistemi: Koleksiyonlardan bağımsız, hardcoded PHP. 4 zaman dilimi selamlaması (Günaydın/İyi günler/İyi akşamlar/İyi geceler) + kullanıcı adı + sektör adı. DB sorgusu yok, admin ayarı yok.

5. Mevcut Yapı (Değiştirilmeyecek)

Yeni sistem bu mevcut modellerin üzerine inşa edilecek. Bu tablolar/modeller zaten var ve değiştirilmeyecek:

Mevcut Muzibu Modelleri

ModelTabloPKTranslatableAçıklama
Playlistmuzibu_playlistsplaylist_idtitle, slug, descriptionÇalma listesi. is_system, is_public, is_radio, is_featured bayrakları var.
Albummuzibu_albumsalbum_idtitle, slug, descriptionAlbüm. artist_id FK. songs() ilişkisi.
Radiomuzibu_radiosradio_idtitle, slug, descriptionRadyo istasyonu. is_featured. HasPlaylistDistribution trait.
Genremuzibu_genresgenre_idtitle, slug, descriptionMüzik türü. 12 adet seeder'da. songs() ilişkisi.
Sectormuzibu_sectorssector_idtitle, slug, descriptionİş sektörü. 8 adet seeder'da. radios() M2M ilişkisi.
Songmuzibu_songssong_idtitle, slug, lyricsŞarkı. album_id, genre_id FK. HLS encrypted stream.
Artistmuzibu_artistsartist_idtitle, slug, bioSanatçı. albums() ilişkisi.

Mevcut Sektörler (Seeder)

Günün Enerjisi Çalışma & Odaklanma Spor & Antrenman Rahatlatıcı & Meditasyon Parti & Eğlence Romantik Anlar Yolculuk & Seyahat Nostalji

Not: Bu sektörler "mood" gibi. Gerçek işletme sektörleri (Kafe, Otel, Spor Salonu) admin tarafından eklenecek.

Mevcut HomeController (Değiştirilecek)

Dosya: Modules/Muzibu/App/Http/Controllers/Front/HomeController.php

// Şu anki statik sorgular — YENİ SİSTEMDE KALDIRILACAK:
$homePlaylists  = Playlist::active()->system()...limit(10);  // cache 5dk
$newReleases    = Album::active()...limit(10);               // cache 5dk
$popularSongs   = Song::active()...orderBy('play_count')...limit(10);
$newSongs       = Song::active()...orderBy('created_at')...limit(15);
$genres         = Genre::active()...limit(15);
$radios         = Radio::active()...limit(10);
$sectors        = Sector::active()...limit(10);
$userPlaylists  = // kullanıcıya özel, 60s cache

// YENİ SİSTEM:
$smartFeed = app(SmartFeedService::class);
$greeting  = $smartFeed->getGreeting($user);
$feed      = $smartFeed->getFeed($user);
$msUntilNextHour = ((60 - now()->minute) * 60 - now()->second + 60) * 1000;

User Model (Mevcut Durum)

Dosya: app/Models/User.php

Fillable (mevcut): name, surname, email, password, is_active, phone, bio, device_limit, corporate_account_id, subscription_expires_at, audio_preference...

Eksik olan: sector_id — bu kolon henüz yok, migration ile eklenecek.

Eklenecek relationship:

// User.php'ye eklenecek:
public function sector()
{
    if (!$this->isMuzibuTenant()) {
        return $this->belongsTo(self::class, 'id')->whereRaw('1=0');
    }
    return $this->belongsTo(
        \Modules\Muzibu\App\Models\Sector::class,
        'sector_id',
        'sector_id'
    );
}

6. Veritabanı Şeması (3 Migration)

Migration Kuralı
Her migration hem Modules/Muzibu/database/migrations/ (central) hem Modules/Muzibu/database/migrations/tenant/ (tenant) altında olmalı. 3 aşamalı onay gerekli. AŞAMA 1 onayı henüz alınmadı.
Migration 1 users tablosuna sector_id ekleme
KolonTipDefaultIndexKonumAçıklama
sector_id BIGINT UNSIGNED NULL INDEX + FK after corporate_account_id FK → muzibu_sectors.sector_id ON DELETE SET NULL
Schema::table('users', function (Blueprint $table) {
    $table->unsignedBigInteger('sector_id')->nullable()->after('corporate_account_id');
    $table->index('sector_id');
    $table->foreign('sector_id')
          ->references('sector_id')
          ->on('muzibu_sectors')
          ->onDelete('set null');
});
Migration 2 muzibu_content_collections (Yeni Tablo)
KolonTipDefault/IndexAçıklama
collection_idBIGINT UNSIGNED AUTOPKBirincil anahtar
titleJSON NOT NULL{"tr": "Sabah Kafe Mix", "en": "Morning Cafe Mix"}
slugJSON NOT NULL{"tr": "sabah-kafe-mix", "en": "morning-cafe-mix"}
descriptionJSON NULLÇoklu dil açıklama
typeENUM'curated', 'daypart', 'sector', 'featured'
iconVARCHAR(50) NULLFontAwesome sınıfı: "fas fa-coffee"
colorVARCHAR(20) NULLHex renk: "#f59e0b"
media_idBIGINT UNSIGNED NULLKapak görseli (Spatie Media Library FK)
display_rulesJSON NULLAşağıda detaylı açıklandı
sector_rulesJSON NULLAşağıda detaylı açıklandı
business_rulesJSON NULLAşağıda detaylı açıklandı
priorityINT DEFAULT 0INDEXBüyük = önce gösterilir
is_activeBOOLEAN DEFAULT 1INDEXAktif/Pasif toggle
is_featuredBOOLEAN DEFAULT 0Öne çıkan koleksiyon
cache_ttlINT DEFAULT 300Özel cache süresi (saniye)
created_atTIMESTAMPOluşturma
updated_atTIMESTAMPGüncelleme

JSON Kolon Detayları

// display_rules — Zaman kuralları
{
  "start_hour": 9,              // int 0-23 — 09:00
  "end_hour": 14,               // int 0-23 — 14:00
  "days": ["mon","tue","wed","thu","fri","sat","sun"],
  "is_always": false            // true → start/end yoksayılır, 7/24 gösterilir
}

// sector_rules — Sektör filtreleme
{
  "mode": "include",            // "include" veya "exclude"
  "sector_ids": [1, 3, 5],     // hangi sektörler
  "show_to_all": false          // true → tüm sektörlere göster
}

// business_rules — İş kuralları
{
  "subscription": "premium",    // "free", "premium", "corporate", "all"
  "min_months": 0               // minimum üyelik süresi (ay)
}
Migration 3 muzibu_collection_items (Yeni Tablo — Polimorfik)
KolonTipDefault/IndexAçıklama
idBIGINT UNSIGNED AUTOPKBirincil anahtar
collection_idBIGINT UNSIGNEDFK + INDEX→ content_collections.collection_id CASCADE
itemable_typeVARCHAR(255)COMPOSITEMorph map: 'playlist', 'album', 'radio', 'genre', 'sector'
itemable_idBIGINT UNSIGNEDINDEXİlgili tablonun PK'sı (playlist_id, album_id, radio_id...)
positionINT DEFAULT 0INDEXSürükle-bırak sıralama (küçük = önce)
is_activeBOOLEAN DEFAULT 1Aktif/Pasif
created_atTIMESTAMP
updated_atTIMESTAMP
UNIQUE constraint: (collection_id, itemable_type, itemable_id) — aynı öğe aynı koleksiyona iki kez eklenemez.

Morph Map (AppServiceProvider'da tanımlanacak)

Relation::enforceMorphMap([
    'playlist' => \Modules\Muzibu\App\Models\Playlist::class,
    'album'    => \Modules\Muzibu\App\Models\Album::class,
    'radio'    => \Modules\Muzibu\App\Models\Radio::class,
    'genre'    => \Modules\Muzibu\App\Models\Genre::class,
    'sector'   => \Modules\Muzibu\App\Models\Sector::class,
]);

Örnek Veri

-- muzibu_content_collections
INSERT INTO muzibu_content_collections
  (collection_id, title, slug, type, icon, priority, display_rules, sector_rules, is_active) VALUES
(1, '{"tr":"Sabah Kafe Mix","en":"Morning Cafe Mix"}',
    '{"tr":"sabah-kafe-mix"}',
    'daypart', 'fas fa-coffee', 100,
    '{"start_hour":6,"end_hour":12,"days":["mon","tue","wed","thu","fri","sat","sun"],"is_always":false}',
    '{"mode":"include","sector_ids":[1,2],"show_to_all":false}',
    1),
(2, '{"tr":"Editörün Seçimi","en":"Editor Picks"}',
    '{"tr":"editorun-secimi"}',
    'curated', 'fas fa-star', 200,
    '{"is_always":true}',
    '{"show_to_all":true}',
    1);

-- muzibu_collection_items (koleksiyon 1 için)
INSERT INTO muzibu_collection_items
  (collection_id, itemable_type, itemable_id, position) VALUES
(1, 'radio',    5, 1),   -- Jazz FM
(1, 'playlist', 12, 2),  -- Sabah Akustik
(1, 'playlist', 45, 3),  -- Kahve Molası
(1, 'album',    8, 4);   -- Cafe Classics albümü

7. Greeting (Karşılama) Sistemi

Koleksiyonlardan tamamen bağımsız. DB'de saklanmaz, admin ayarı yok. Sadece PHP kodu.

Saat AralığıSelamlamaÖrnek Çıktı
06:00 — 12:00GünaydınGünaydın, Ahmet!
Kafe & Restoran için seçilmiş içerikler
12:00 — 18:00İyi günlerİyi günler, Zeynep!
Spor Salonu & Fitness için seçilmiş içerikler
18:00 — 22:00İyi akşamlarİyi akşamlar, Mehmet!
Otel & Spa için seçilmiş içerikler
22:00 — 06:00İyi gecelerİyi geceler, Can!
Senin için seçilmiş içerikler

Sektörü NULL olan kullanıcılara: "Senin için seçilmiş içerikler" gösterilir.

Sector eager load edilir (User::with('sector')), ekstra sorgu yok.

Saat başı yenilemede greeting de otomatik güncellenir (partial'ın içinde).

// SmartFeedService::getGreeting(User $user): array
public function getGreeting(User $user): array
{
    $hour = now()->hour;

    if ($hour >= 6 && $hour < 12)       $greet = 'Günaydın';
    elseif ($hour >= 12 && $hour < 18)  $greet = 'İyi günler';
    elseif ($hour >= 18 && $hour < 22)  $greet = 'İyi akşamlar';
    else                                 $greet = 'İyi geceler';

    $sector = $user->sector?->title;  // eager loaded, sorgu yok

    return [
        'title'    => "$greet, {$user->name}!",
        'subtitle' => $sector
            ? "{$sector} için seçilmiş içerikler"
            : 'Senin için seçilmiş içerikler',
    ];
}

8. Zaman Yönetimi (Otomatik Sessiz Yenileme)

Problem

Kullanıcılar sabah siteyi açıp gece kadar yenilemeden bırakıyor. İçerik saate bağlı olduğu için "stale content" problemi var. Çözüm: her saat başı otomatik sessiz güncelleme.

Akış Adımları

1
Sayfa yüklenir (örn: 08:29)
PHP hesaplar: (60-29)*60 - saniye + 60 = ~32 dakika$msUntilNextHour = 1920000 (ms)
Bu değer Blade'e {{ $msUntilNextHour }} olarak basılır.
2
09:01'de setTimeout tetiklenir
fetch('/api/feed-partial') → sunucu saat 9 ile koleksiyonları filtreler + greeting günceller → HTML partial döner
3
Sessiz güncelleme
document.getElementById('feed-container').innerHTML = yeniHTML
Müzik kesilmez (player bar feed-container dışında). Sayfa reload yok.
4
Yeni setTimeout kurulur → 10:01 için
schedule() fonksiyonu: (60 - dakika) * 60000 - saniye*1000 + 60000 ms sonra tekrar tetikle.
Bu döngü sayfa açık kaldığı sürece devam eder. setInterval kullanılmaz.

Neden Bu Yöntem?

YöntemRAMCPUSeçildi mi?Neden
setInterval (1dk polling)SürekliSürekliHayırKötü cihazları yorar
Banner + tıklaAzAzHayırKullanıcı tıklamayı unutur/geciktirir
Cron + Push (WebSocket)YüksekYüksekHayırSunucu kaynağı + eski cihaz desteği zayıf
Meta refreshSıfırSıfırHayırSayfa reload = müzik durur
Tek setTimeout + fetch~8 byteSıfırEVETMinimum kaynak, müzik kesilmez, otomatik

Frontend JS Kodu (Tamamı ~15 satır)

// Sadece anasayfada çalışır. Framework yok. Interval yok.
(function(){
    var el = document.getElementById('feed-container');
    if (!el) return; // iç sayfalarda #feed-container yok → hiçbir şey yapma

    function refresh(){
        fetch('/api/feed-partial')
            .then(function(r){ return r.text() })
            .then(function(html){
                el.innerHTML = html;
                schedule();
            });
    }

    function schedule(){
        var d = new Date();
        var ms = (60 - d.getMinutes()) * 60000 - d.getSeconds() * 1000 + 60000;
        setTimeout(refresh, ms); // saat başı +1dk
    }

    setTimeout(refresh, {{ $msUntilNextHour }}); // ilk timeout (PHP hesaplar)
})();
{{ $msUntilNextHour }} = Blade değişkeni. PHP'de: $ms = ((60 - now()->minute) * 60 - now()->second + 60) * 1000;
Yenileme ÇALIŞIR
Sadece anasayfa (/) — #feed-container div'i varsa
Yenileme ÇALIŞMAZ
İç sayfalar (koleksiyon detay, playlist detay, albüm detay, genre, sektör)

9. UX Kuralları

Tıklama Davranışları

İçerik TipiTıklanınca Ne OlurSayfa Değişir mi?Route
Radyo Hemen çalar Hayır — player bar güncellenir JS: playerCore.playRadio(id)
Playlist Şarkı listesi açılır Evet /playlists/{slug}
Albüm Şarkı listesi açılır Evet /albums/{slug}
Genre Genre sayfası Evet /genres/{slug}
Sektör Sektör sayfası Evet /sectors/{slug}

"Tümü" Butonu Kuralı

6 ve altı öğe
"Tümü" butonu GİZLİ. Yatay scroll yeterli.
7+ öğe
"Tümü" butonu GÖRÜNÜR → /koleksiyon/{slug} grid sayfası açılır.

Anasayfa Wireframe

┌─────────────────────────────────────────────┐
│ Header  |  Arama  |  Profil                  │ ← dokunulmaz
├─────────────────────────────────────────────┤
│ Player Bar  ▶ Şarkı Adı — Sanatçı           │ ← dokunulmaz
├═════════════════════════════════════════════┤
│                                              │
│  ☀️ Günaydın, Ahmet!                       │ ← greeting
│  Kafe & Restoran için seçilmiş içerikler    │ ← sektör
│                                              │
│  ★ Editörün Seçimi            Tümü ›        │ ← priority:200
│  [Radio][Playl][Album][Playl][Genre]→    │ ← yatay scroll
│                                              │
│  ☕ Sabah Kafe Mix                          │ ← priority:100
│  [Radio][Playl][Playl][Album] →             │ ← 4 öğe, Tümü yok
│                                              │
│  🎵 Türkçe Pop Klasikler      Tümü ›        │ ← 9 öğe, Tümü var
│  [Radio][Playl][Album][Playl][Genre]→    │
│                                              │
├═════════════════════════════════════════════┤
│ Footer                                       │ ← dokunulmaz
└─────────────────────────────────────────────┘
     ↑
  #feed-container — sadece bu bölge güncellenir
  (greeting + koleksiyonlar)
  Header, Player Bar, Footer sabit kalır

10. Backend Kod Mimarisi

ContentCollection Model

Dosya: Modules/Muzibu/App/Models/ContentCollection.php

class ContentCollection extends BaseModel
{
    use HasTranslations, Sluggable, HasMediaManagement, SoftDeletes;

    protected $connection = 'tenant';
    protected $table = 'muzibu_content_collections';
    protected $primaryKey = 'collection_id';

    public $translatable = ['title', 'slug', 'description'];

    protected $fillable = [
        'title', 'slug', 'description', 'type', 'icon', 'color',
        'media_id', 'display_rules', 'sector_rules', 'business_rules',
        'priority', 'is_active', 'is_featured', 'cache_ttl',
    ];

    protected $casts = [
        'display_rules'  => 'array',
        'sector_rules'   => 'array',
        'business_rules' => 'array',
        'priority'       => 'integer',
        'is_active'      => 'boolean',
        'is_featured'    => 'boolean',
        'cache_ttl'      => 'integer',
    ];

    // Koleksiyondaki öğeler (polimorfik)
    public function items(): HasMany
    {
        return $this->hasMany(CollectionItem::class, 'collection_id', 'collection_id')
                    ->orderBy('position');
    }

    // Aktif öğeler
    public function activeItems(): HasMany
    {
        return $this->items()->where('is_active', true);
    }

    // Scope: aktif koleksiyonlar
    public function scopeActive($q) { return $q->where('is_active', true); }
}

CollectionItem Model

Dosya: Modules/Muzibu/App/Models/CollectionItem.php

class CollectionItem extends Model
{
    protected $connection = 'tenant';
    protected $table = 'muzibu_collection_items';

    protected $fillable = [
        'collection_id', 'itemable_type', 'itemable_id', 'position', 'is_active',
    ];

    protected $casts = [
        'position'  => 'integer',
        'is_active' => 'boolean',
    ];

    // Polimorfik ilişki — Playlist, Album, Radio, Genre veya Sector
    public function itemable(): MorphTo
    {
        return $this->morphTo();
    }

    public function collection(): BelongsTo
    {
        return $this->belongsTo(ContentCollection::class, 'collection_id', 'collection_id');
    }
}

SmartFeedService

Dosya: Modules/Muzibu/App/Services/SmartFeedService.php

class SmartFeedService
{
    // Greeting (yukarıda detaylı açıklandı)
    public function getGreeting(User $user): array { /* ... */ }

    // Ana feed — filtrelenmiş koleksiyonlar
    public function getFeed(User $user): Collection
    {
        $hour     = now()->hour;
        $sectorId = $user->sector_id;
        $cacheKey = "smart_feed:{$user->id}:{$sectorId}:{$hour}";

        return Cache::remember($cacheKey, 300, function() use ($user, $hour) {
            return ContentCollection::active()
                ->with(['activeItems.itemable'])   // eager load
                ->orderByDesc('priority')
                ->get()
                ->filter(fn($c) => $this->matchesTimeRules($c, $hour))
                ->filter(fn($c) => $this->matchesSectorRules($c, $user))
                ->filter(fn($c) => $this->matchesBusinessRules($c, $user));
        });
    }

    // Filtre 1: Zaman kuralı
    private function matchesTimeRules(ContentCollection $c, int $hour): bool
    {
        $rules = $c->display_rules;
        if (empty($rules) || ($rules['is_always'] ?? true)) return true;

        $start = $rules['start_hour'] ?? 0;
        $end   = $rules['end_hour'] ?? 23;

        // Gece geçişi: start=22, end=6 → 22,23,0,1,2,3,4,5
        if ($start <= $end) {
            return $hour >= $start && $hour < $end;
        }
        return $hour >= $start || $hour < $end;
    }

    // Filtre 2: Sektör kuralı
    private function matchesSectorRules(ContentCollection $c, User $user): bool
    {
        $rules = $c->sector_rules;
        if (empty($rules) || ($rules['show_to_all'] ?? true)) return true;

        $sectorId  = $user->sector_id;
        $sectorIds = $rules['sector_ids'] ?? [];
        $mode      = $rules['mode'] ?? 'include';

        if ($mode === 'include') return in_array($sectorId, $sectorIds);
        if ($mode === 'exclude') return !in_array($sectorId, $sectorIds);
        return true;
    }

    // Filtre 3: İş kuralı
    private function matchesBusinessRules(ContentCollection $c, User $user): bool
    {
        $rules = $c->business_rules;
        if (empty($rules)) return true;

        $sub = $rules['subscription'] ?? 'all';
        if ($sub !== 'all') {
            // premium/corporate kontrol
            if ($sub === 'premium' && !$user->isPremium()) return false;
            if ($sub === 'corporate' && !$user->isCorporateMember()) return false;
        }
        return true;
    }
}

/api/feed-partial Endpoint

// routes/web.php (Muzibu domain group içinde)
Route::get('/api/feed-partial', function () {
    $user = auth()->user();
    if (!$user) abort(401);

    $service  = app(SmartFeedService::class);
    $greeting = $service->getGreeting($user);
    $feed     = $service->getFeed($user);

    return view('themes.muzibu.partials.feed-collections', compact('greeting', 'feed'));
})->middleware(['auth'])->name('muzibu.feed.partial');

11. Admin Paneli

Liste Sayfası (CollectionComponent)

Route: /admin/koleksiyonlar — Livewire component

Tablo kolonları:

Başlık Tip (badge) Saat Aralığı (08:00-14:00) Sektörler Öğe Sayısı Öncelik Durum (toggle)

Filtreler: Tip, sektör, durum. Toplu işlemler: sil, aktif/pasif. Mevcut pattern: Modules/Page/.../page-component.blade.php referans.

Form Sayfası (CollectionManageComponent — 5 Tab)

Route: /admin/koleksiyonlar/manage/{id?}

Tab 1: Temel Bilgiler
Başlık (multilang), slug (auto), açıklama (multilang), tip (ENUM dropdown), ikon (FA picker), renk (color picker), kapak görseli (media), öncelik (number), is_active, is_featured.
Tab 2: Zaman Kuralları
is_always checkbox → true ise diğerleri gizlenir.
start_hour dropdown: 00:00, 01:00, 02:00 ... 23:00 (24 seçenek, sadece düz saat)
end_hour dropdown: aynı format
Günler: 7 checkbox (Pzt-Paz), hepsi seçili varsayılan.
Tab 3: Sektör Kuralları
show_to_all checkbox → true ise diğerleri gizlenir.
mode radio: include / exclude
Sektör seçimi: dual listbox (mevcut admin pattern'i).
Tab 4: İş Kuralları
subscription dropdown: Tümü, Free, Premium, Corporate.
min_months number input: minimum üyelik süresi.
Tab 5: İçerikler (Polimorfik Öğe Yönetimi)
5 tipte öğe ekleme: Playlist, Album, Radio, Genre, Sector. Her tip için arama/seçim. Sürükle-bırak sıralama (Sortable.js). Öğe silme. Position otomatik güncelleme.
Her öğenin yanında: tip badge + başlık + sil butonu.

12. Frontend

feed-collections.blade.php (AJAX Partial)

Dosya: resources/views/themes/muzibu/partials/feed-collections.blade.php

Bu dosya hem ilk sayfa yüklemesinde hem saat başı AJAX ile yüklenir. Bağımsız partial — layout/header/footer içermez.

<!-- Greeting -->
<div class="mb-6">
    <h2 class="text-2xl font-bold text-white">{{ $greeting['title'] }}</h2>
    <p class="text-slate-400">{{ $greeting['subtitle'] }}</p>
</div>

<!-- Koleksiyonlar -->
@foreach($feed as $collection)
    <div class="mb-8">
        <div class="flex justify-between items-center mb-3">
            <h3 class="text-lg font-bold text-white">
                @if($collection->icon)
                    <i class="{{ $collection->icon }} mr-2"></i>
                @endif
                {{ $collection->title }}
            </h3>
            @if($collection->activeItems->count() > 6)
                <a href="/koleksiyon/{{ $collection->slug }}" class="text-blue-400">
                    Tümü &rsaquo;
                </a>
            @endif
        </div>
        <div class="flex gap-4 overflow-x-auto pb-2">
            @foreach($collection->activeItems as $item)
                {{-- Her itemable_type'a göre farklı kart --}}
                @include('themes.muzibu.partials.collection-item-card', [
                    'item' => $item->itemable,
                    'type' => $item->itemable_type
                ])
            @endforeach
        </div>
    </div>
@endforeach

index.blade.php (Anasayfa) Güncelleme

Dosya: resources/views/themes/muzibu/index.blade.php

<!-- Mevcut statik içerik KALDIRILACAK -->
<!-- Yerine: -->

<div id="feed-container">
    @include('themes.muzibu.partials.feed-collections', [
        'greeting' => $greeting,
        'feed'     => $feed,
    ])
</div>

@push('scripts')
<script>
(function(){
    var el = document.getElementById('feed-container');
    if (!el) return;
    function refresh(){
        fetch('/api/feed-partial')
            .then(function(r){ return r.text() })
            .then(function(html){ el.innerHTML = html; schedule(); });
    }
    function schedule(){
        var d = new Date();
        var ms = (60 - d.getMinutes()) * 60000 - d.getSeconds() * 1000 + 60000;
        setTimeout(refresh, ms);
    }
    setTimeout(refresh, {{ $msUntilNextHour }});
})();
</script>
@endpush

collection/show.blade.php (Grid Sayfa)

"Tümü" tıklanınca açılan sayfa. Route: /koleksiyon/{slug}

Koleksiyonun tüm öğelerini grid layout ile gösterir. Aynı tıklama kuralları geçerli (radio = hemen çal, diğerleri = sayfa aç).

13. Dosya & Route Haritası

Oluşturulacak / Güncellenecek Dosyalar

YENİ DOSYALAR:

Modules/Muzibu/
├── database/migrations/
│   ├── 2026_02_24_000001_add_sector_id_to_users.php          ← M1 central
│   ├── 2026_02_24_000002_create_content_collections.php       ← M2 central
│   ├── 2026_02_24_000003_create_collection_items.php          ← M3 central
│   └── tenant/
│       ├── 2026_02_24_000001_add_sector_id_to_users.php      ← M1 tenant
│       ├── 2026_02_24_000002_create_content_collections.php   ← M2 tenant
│       └── 2026_02_24_000003_create_collection_items.php      ← M3 tenant
├── App/Models/
│   ├── ContentCollection.php                                  ← YENİ
│   └── CollectionItem.php                                     ← YENİ
├── App/Services/
│   └── SmartFeedService.php                                   ← YENİ
├── App/Http/Controllers/Front/
│   └── CollectionController.php                               ← YENİ
├── App/Http/Livewire/Admin/
│   ├── CollectionComponent.php                                ← YENİ
│   └── CollectionManageComponent.php                          ← YENİ
└── resources/views/livewire/admin/
    ├── collection-component.blade.php                         ← YENİ
    └── collection-manage-component.blade.php                  ← YENİ

resources/views/themes/muzibu/
├── partials/
│   ├── feed-collections.blade.php                             ← YENİ (AJAX partial)
│   └── collection-item-card.blade.php                         ← YENİ (kart component)
└── collection/
    └── show.blade.php                                         ← YENİ (Tümü sayfası)

GÜNCELLENECEK DOSYALAR:

app/Models/User.php                                            ← sector() relationship + $fillable
Modules/Muzibu/App/Http/Controllers/Front/HomeController.php    ← SmartFeedService entegrasyonu
Modules/Muzibu/routes/web.php                                  ← /koleksiyon/{slug} + /api/feed-partial
Modules/Muzibu/Providers/MuzibuServiceProvider.php             ← Livewire component register + morph map
resources/views/themes/muzibu/index.blade.php                  ← #feed-container + setTimeout JS

Route Tablosu

MethodURIHandlerMiddlewareAçıklama
GET/koleksiyon/{slug}CollectionController@showauthKoleksiyon grid sayfası
GET/api/feed-partialClosureauthHTML partial (saat başı AJAX)
GET/admin/koleksiyonlarLivewire: CollectionComponentadminAdmin liste
GET/admin/koleksiyonlar/manage/{id?}Livewire: CollectionManageComponentadminAdmin form (5 tab)

14. TODO — 4 Faz, 18 Madde

Durum
Henüz hiçbir TODO başlanmadı. Migration için 3 aşamalı onay gerekiyor (CLAUDE.md kuralı). AŞAMA 1 onayı henüz alınmadı.
Faz 1 — Migration & Modeller (6 TODO)
1
Migration: users.sector_id
Central + Tenant dosyaları. BIGINT UNSIGNED NULL. FK → muzibu_sectors.sector_id ON DELETE SET NULL. After corporate_account_id.
2
Migration: muzibu_content_collections
Central + Tenant. 15 kolon. display_rules, sector_rules, business_rules JSON. type ENUM. priority INDEX.
3
Migration: muzibu_collection_items
Central + Tenant. Polimorfik. UNIQUE(collection_id, itemable_type, itemable_id). FK cascade.
4
Model: ContentCollection
$translatable, $casts JSON, items() hasMany, activeItems() scope, active() scope.
5
Model: CollectionItem
itemable() morphTo. Morph map: playlist, album, radio, genre, sector.
6
User.php güncelle
sector() relationship ekleme. $fillable'a sector_id ekleme. isMuzibuTenant() guard.
Faz 2 — SmartFeedService & Backend (5 TODO)
7
SmartFeedService oluştur
getGreeting(User): saat→selamlama + isim + sektör. getFeed(User): koleksiyonlar. 3 filtre: matchesTimeRules (gece geçişi dahil), matchesSectorRules (include/exclude), matchesBusinessRules. Cache: smart_feed:{userId}:{sectorId}:{hour}, TTL: 300s.
8
CollectionController@show
GET /koleksiyon/{slug}. items eager load + morphTo. "Tümü" grid sayfası.
9
HomeController güncelle
Statik sorguları kaldır. SmartFeedService entegrasyonu. $msUntilNextHour hesaplama: ((60-minute)*60-second+60)*1000. feed-collections partial.
10
API endpoint: /api/feed-partial
Auth middleware. HTML partial döner (greeting + koleksiyonlar blade render). Saat başı sessiz yenileme için.
11
Route'lar ekle
/koleksiyon/{slug} + /api/feed-partial → Modules/Muzibu/routes/web.php. Morph map → ServiceProvider.
Faz 3 — Admin Panel / Livewire (3 TODO)
12
CollectionComponent (Liste)
Tablo: başlık, tip badge, saat aralığı (00:00 formatında), sektörler, öğe sayısı, öncelik, durum toggle. Filtreler + bulk. WithPagination, WithBulkActions traits. Pattern: page-component.blade.php referans.
13
CollectionManageComponent (Form — 5 Tab)
Tab 1: Temel (multilang, slug, tip, ikon, renk, media, priority). Tab 2: Zaman (is_always toggle, start_hour/end_hour dropdown 00:00-23:00 düz saat, günler 7 checkbox). Tab 3: Sektör (show_to_all, mode radio, dual listbox). Tab 4: İş kuralları (subscription dropdown, min_months). Tab 5: İçerikler (5 tip ekleme, Sortable.js sürükle-bırak, position güncelleme). GlobalTabService tabs, form-floating Bootstrap.
14
Admin route + menü + Livewire kayıt
/admin/koleksiyonlar, /admin/koleksiyonlar/manage/{id?}. MuzibuServiceProvider'da Livewire::component() kayıt. Sidebar menüsüne "Koleksiyonlar" ekleme.
Faz 4 — Frontend (4 TODO)
15
feed-collections.blade.php (partial)
Greeting (selamlama + sektör adı) + koleksiyon döngüsü + yatay scroll kartlar. AJAX swap için bağımsız partial. @include('collection-item-card') ile her tip için kart.
16
index.blade.php güncelle
Statik bölümleri kaldır. #feed-container div + @include partial + setTimeout JS (~15 satır vanilla). "Tümü": activeItems count > 6.
17
collection/show.blade.php (grid)
"Tümü" tıklanınca açılan grid sayfa. Koleksiyon başlığı + tüm öğeler grid layout. Aynı tıklama kuralları.
18
Radio tıklama: player entegrasyonu
Radio kartına tıklanınca sayfa değişmeden player-core.js üzerinden radyo çalma. Mevcut playRadio() fonksiyonu kullanılır.
Versiyon Geçmişi (v1-v10)
v1 — İlk analiz (Spotify + Apple Music + Soundtrack Your Brand benchmark)
v2 — B2B pivot — bireysel kullanıcı kaldırıldı, işletme odaklı
v3 — Görsel simülasyon
v4 — 5 polimorfik tip kesinleşti (Playlist, Album, Radio, Genre, Sector)
v5 — Örnek veri tabloları + draft admin panel tasarımı
v6 — Telefon mockup: kullanıcı deneyimi + tıklama davranışları detayı
v7 — İlk kesin plan (6 sabit slot: sabah/öğle/akşam...)
v8 — Esnek saat sistemi (start_hour/end_hour) + otomatik sessiz yenileme kararı
v9 — Greeting sistemi eklendi + admin saat dropdown formatı (düz saatler)
v10 — TAM DOKÜMANTASYON: Başka bir AI'ın sıfırdan anlayabileceği eksiksiz rehber. Proje tanımı, hedefler, mimari, mevcut yapı, tüm kararlar, DB şeması + örnek veri, backend kod örnekleri, admin panel detayları, frontend partial + JS, dosya haritası, 18 TODO.