# Admin Sayfası Ekleme

https://dev.wisecp.com/tr/admin-sayfasi-ekleme

Bir modüle admin panelde kendi sayfasını iki dosyayla açarsınız. HTML'i bir alan sınıfı döndürür; `router.php` içindeki tek satır onu kaydeder.

## Genel Bakış

Kendi kayıtlarını yöneten ya da toplu iş çalıştıran bir modülün gerçek bir sayfaya ihtiyacı vardır. O sayfa çekirdeğe ait değildir: sınıfı bildirir, kaydedersiniz; rota, menü öğesi, yetki kontrolü, künye çizgisi ve tema kabuğu kendiliğinden gelir. Yeniyseniz: [İlk Modülünüz](https://dev.wisecp.com/tr/ilk-modulunuz).

Dağıtıcı `coremio/controllers/admin/module-page.php` dosyasıdır; slug rotaları oraya çözülür ve sonuç eklenti tema sarmalayıcısına gider.

## Yapı

- **classes/ModuleAdminArea.php**: Taban sınıf ve kayıt defteri: `register()`, rota kablolaması, menü kancası, örnek yardımcıları.
- **admin/module-page.php**: İç dağıtıcı: `page_*` ve `op_*` metotlarını çözer, URL'de görünmez.
- **tools/addons-area.php**: Tema sarmalayıcısı: başlık şeridi, butonlar, eklentiler, stiller, script'ler, modaller.
- **{modül}/AdminArea.php + router.php**: Yazdığınız iki dosya. `Router::loadModuleRouters()` router dosyasını rotalardan önce okur.

## Adım Adım

### Alan Sınıfını Bildirin

1. Modül dizininizde `AdminArea.php` oluşturun, ad alanı `WISECP\Modules\{Tip}\{Ad}` olsun.
2. `\ModuleAdminArea` sınıfını genişletin ve statik `manifest()` metodundan manifest'i döndürün.
3. Sayfa yalnız bir koşulda anlamlıysa `available()` metodunu ezin; menü öğesini de kapılar.

### Kaydedin

1. Yanına `router.php` oluşturun: sınıf dosyasını include edip `\ModuleAdminArea::register(AdminArea::class)` çağırın.
2. Oraya başka bir şey yazılmaz. Kayıt üç rota ekler, manifest'i saklar ve menü öğesini kancalar.

### Sayfa Metodu Yazın

1. `page_home()` alanın kökünü yanıtlar; her metot URL'in kalan parçalarını dizi olarak alır.
2. HTML'i heredoc ile kurun; metinlerinizi `$this->lang` içinden okuyun.
3. Kendinize verdiğiniz bağlantıları `$this->link()` ile kurun, elle yazılmış bir yolla değil.

### Operation Yazın

1. Metodu `op_{ad}` olarak adlandırın; yalnız bu önek çağrılabilir.
2. Alan URL'ine bir `operation` parametresiyle POST edin; fırlatılan exception standart JSON hatasına dönüşür.
3. `$operation->demo()` ile başlayın, girdiyi `Filter::init()` ile okuyun, `$operation->output()` ile bitirin.

### Alan Ayarlarını Saklayın

1. Kendi dosyanız değil, `settings()`, `setting()` ve `save_settings()`.
2. `save_settings()` mevcut değerlerle birleştirir, yani kısmi bir kayıt gerisini korur, sonra `action:module.area_settings_saved` kancasını çalıştırır.

## Referans

### Taban Sınıf API'si

```php
class ModuleAdminArea
{
    public string $module_type = '';   // ad alanından çözülür
    public string $module_name = '';   // ad alanından çözülür
    public array  $lang        = [];   // {module}/lang/{selected}.php, en.php'ye düşerek

    public function __construct();

    // Ezeceğiniz metot budur. Varsayılanı olan diğer her şey isteğe bağlıdır.
    public static function manifest(): array;

    public static function register(string $areaClass): void;
    public static function get(string $type, string $name): ?array;

    public function available(): bool;                                  // varsayılan true
    public function slug(): string;
    public function link(array $params = []): string;
    public function module_dir(): string;                               // dosya sistemi yolu, sonda ayraçla
    public function module_url(): string;                               // modül dizininin genel URL'i

    public function settings(): array;
    public function setting(string $key, mixed $default = null): mixed;
    public function save_settings(array $values): void;

    protected function license_state_badge(string $slug): string;
}
```

> **On altı değil, on bir modül tipi**
> 
> Kabul edilen tipler: Servers, Payment, Registrars, Product, Addons, SMS, Mail, Authentication, Pipe, Imports ve Fraud. Diğerleri için `register()` sessizce hiçbir şey yapmaz.

### Manifest Neyi Kabul Eder

- **title**: Sayfa ve tarayıcı başlığı; `menu.name` yoksa menü etiketi.
- **slug**: URL parçası, varsayılanı modül adının küçük harfli hâli. `Filter::route()` yalnız `a-zA-Z0-9`, tire, alt çizgi ve noktayı korur.
- **privileges**: Yetki anahtarları, alanın tamamı için bir kez denetlenir. Boş bırakmak kontrolü kaldırır.
- **menu**: `['path' => ['PRODUCTS', 'GROUP_HOSTING_SERVER'], 'name' => 'Hetzner Cloud']`. Yol ağaçta aşağı yürür; yazmazsanız öğe oluşmaz.
- **type, name, class**: `register()` bunları ad alanından ve argümandan yazar. Siz atamayın.

### URL Şeması ve Metot Çözümlemesi

```php
// Üç rota, en özgülü başta. $target değeri module-page/{Type}/{Name} olur.
$router->add($slug . '-2', $slug . '/(?)/(?)', $target . '/(1)/(2)');
$router->add($slug . '-1', $slug . '/(?)',     $target . '/(1)');
$router->add($slug,        $slug,              $target);

// link() metodunun rota anahtarını parametre sayısından türetmesinin sebebi budur:
//   $this->link()                     -> /{admin}/{slug}
//   $this->link(['configuration'])    -> /{admin}/{slug}/configuration
//   $this->link(['logs', 'archive'])  -> /{admin}/{slug}/logs/archive
```

| İstek | Çağrılan metot | Argüman |
| --- | --- | --- |
| `GET /{admin}/{slug}` | `page_home()` | boş dizi |
| `GET /{admin}/{slug}/configuration` | `page_configuration()` | `['configuration']` |
| `GET /{admin}/{slug}/logs/archive` | `page_logs_archive()` / `page_logs()` | `['logs', 'archive']` |
| `POST` + `operation=sync_prices` | `op_sync_prices(Operation $operation)` | `Operation` |
| eşleşen metot yok | 404 sayfası | hiçbir şey görünmez |

Bir parçadaki tire, nokta ve virgül alt çizgiye dönüşür, yani `/{slug}/price-list` adresi `page_price_list()` metoduna çözülür. Operation adı da aynı dönüşümden geçer.

### Sayfa Metodu Ne Döndürebilir

Dize sayfa gövdesi olur. Dizi, aşağıdaki her anahtarı sarmalayıcıya geçirir; yazmadığınız anahtar gösterilen varsayılana düşer.

| Anahtar | Tip | Etkisi |
| --- | --- | --- |
| `content` | string | Sayfa gövdesi. Boşsa yerine `No module area content is available.` uyarısı görünür. |
| `page_title` | string | Manifest başlığını ezer. |
| `page_title_buttons` | array | Buton tanımı listesi; düz HTML işe yaramaz. |
| `page_title_after` | string | Başlık ve butonlardan sonra serbest HTML. |
| `page_title_logo` | string | Bir görsel adresi, ya da `<` içeriyorsa ham HTML. |
| `content_layout` | string | Varsayılan `panel`; `plain` paneli kaldırır. |
| `content_data_class` | string | İçerik sarmalayıcısına ek sınıf (plain yerleşimi). |
| `breadcrumbs` | array | Pano + alan başlığı izinin sonuna eklenir. |
| `plugins` | array | Sarmalayıcı varsayılanlarıyla birleşir. |
| `page_styles`, `page_scripts`, `modals` | string | Head, footer ve modal alanı. |

### Oturumdaki Yönetici

```php
public static function LoginData($type = 'member', $isRemembered = false, $recheck = false);
```

```php
$adminId = (int) (\UserManager::LoginData('admin')['id'] ?? 0);   // panel dışında 0 (CLI, cron)

// Bir atama alanı için operatör seçici.
$staff = \Admin::list();          // [id => ['id' => 1, 'full_name' => 'Jane Doe'], ...]
```

## Örnek

Hayali bir `Acme` modülü için iki dosyalık alan.

```php
<?php
namespace WISECP\Modules\Servers\Acme;

use AdminFormBuilder;
use Exception;
use Filter;
use Operation;
use User;
use UserManager;
use WDB;

class AdminArea extends \ModuleAdminArea
{
    public static function manifest(): array
    {
        return [
            'title'      => 'Acme',
            // 'slug'    => 'acme',                  // varsayılan: strtolower(modül adı)
            'privileges' => ['PRODUCTS_OPERATION'],
            'menu'       => ['path' => ['PRODUCTS', 'GROUP_HOSTING_SERVER'], 'name' => 'Acme'],
        ];
    }

    /** Acme sunucusu yokken menü öğesini gizler ve sayfayı 404 yapar. */
    public function available(): bool
    {
        static $available = null;

        if ($available === null)
            $available = (bool) WDB::select('id')->from('servers')
                ->where('type', '=', 'Acme', '&&')
                ->where('status', '=', 'active')
                ->build();

        return $available;
    }

    public function page_home(array $params): array
    {
        $L    = $this->lang;
        $conf = $this->settings();

        $form = new AdminFormBuilder('acmeAreaForm', $this->link(), ['disableStickySubmit' => true]);
        $form->addHidden('operation', 'save_settings');
        $form->addAmount('profit_rate', $L['profit-rate'] ?? '', (string) ($conf['profit_rate'] ?? 25));
        $form->addSwitch('auto_sync', $L['auto-sync'] ?? '', $L['auto-sync-desc'] ?? '', '1', (int) ($conf['auto_sync'] ?? 0) === 1);

        return [
            'content'          => $form->render(),
            'page_title'       => $L['area-title'] ?? 'Acme',
            'page_title_after' => $this->license_state_badge('acme'),
        ];
    }

    public function op_save_settings(Operation $operation): bool
    {
        $operation->demo();

        $values = [
            'profit_rate' => (float) Filter::init('POST/profit_rate', 'amount'),
            'auto_sync'   => (int) Filter::init('POST/auto_sync', 'rnumbers') === 1,
        ];

        if ($values['profit_rate'] < 0)
            throw new Exception($this->lang['err-negative-rate'] ?? 'The profit rate cannot be negative.');

        $this->save_settings($values);

        $adminId = (int) (UserManager::LoginData('admin')['id'] ?? 0);
        User::addAction($adminId, 'update', 'acme-settings-updated', ['changes' => $values]);

        return $operation->output([
            'status'   => 'successful',
            'redirect' => 'reload',
        ]);
    }
}
```

```php
<?php
namespace WISECP\Modules\Servers\Acme;

include_once __DIR__ . DS . 'AdminArea.php';

\ModuleAdminArea::register(AdminArea::class);
```

Okuma tarafı dağıtıcıdır; metodunuza ulaşılıp ulaşılmayacağına o karar verir:

```php
$area_info = ModuleAdminArea::get($type, $name);
if (!$area_info) return $this->page_404();

if (($area_info['privileges'] ?? []) && !Admin::isPrivilege($area_info['privileges'])) return 'Access Denied';

$area = new $area_info['class']();
if (!$area->available()) return $this->page_404();

if ($operation = Filter::init('REQUEST/operation')) return $this->area_operation($area, $operation);

$page    = Filter::route($this->params[2] ?? '') ?: 'home';
$subpage = Filter::route($this->params[3] ?? '');

$filter_method = fn ($param) => str_replace(['-', '.', ','], '_', $param);
$method        = $filter_method('page_' . $page);            // page_logs
$method2       = $filter_method($method . '_' . $subpage);   // page_logs_archive

$page_params = array_slice($this->params, 2);

if ($subpage && method_exists($area, $method2)) $result = $area->$method2($page_params);
elseif (method_exists($area, $method))          $result = $area->$method($page_params);
else return $this->page_404();

if (!is_array($result)) $result = ['content' => (string) $result];
```

## Tuzaklar

> **Sessizce sonlanan sayfa metodu yönlendirme değil ölümcül hata sorunudur**
> 
> Tanımsız bir property ya da unutulmuş bir import hiç çıktı üretmez ve `Exception` üzerine kurulu `try/catch` bunu yakalamaz, çünkü `Error` bir `Exception` değildir. En sık vaka `Admin::$data['id']`'dir: öyle bir statik property yoktur, operatörü `UserManager::LoginData('admin')` ile okuyun.

> **Çekirdek rotasıyla çakışan slug bağlantı üretimini bozar**
> 
> Gerçek controller dosyaları dağıtımda kazanır, yani `services` olarak kaydedilen alana ulaşılamaz ve o anahtarın bağlantıları ezilir. Çekirdeğin kullanmadığı bir slug seçin.

> **Eksik bir ebeveyn menü öğesini tek kelime etmeden gizler**
> 
> Menü kancası ağaçta aşağı yürür ve bir adım yoksa döner. Ebeveyn dal yetkilerle budanmışsa öğeniz eklenmez, sayfa URL ile erişilebilir kalır.

> **Sistem lisansı etkin değilken operation'lar bloklanır**
> 
> Alan dağıtıcısı metodunuzu aramadan önce lisansı denetler ve JSON hatasıyla yanıtlar. Sayfalar view katmanında etkilenir.

> **Alan ayarları modül adı altında yaşar**
> 
> Configurations tablosundaki satır anahtarı, dizinin yazdığı hâliyle modül adıdır; dizini yeniden adlandırmak ayarları öksüz bırakır. Taşımayı `settings()` ezerek yapın.

## İlgili Makaleler

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Modülden Kanca Kaydetme](https://dev.wisecp.com/tr/modulden-kanca-kaydetme)
- [API Ucu Açma](https://dev.wisecp.com/tr/api-ucu-acma)
- [Admin Form Oluşturucu](https://dev.wisecp.com/tr/admin-form-olusturucu)
- [Operation'lar](https://dev.wisecp.com/tr/operationlar)
- [Bağlantı ve Rota Kurma](https://dev.wisecp.com/tr/baglanti-ve-rota-kurma)
