# Özel Alan Ekleme

https://dev.wisecp.com/tr/ozel-alan-ekleme

Bir müşteriye ya da talebe kolon eklemeden ek bilgi iliştirin.

## Genel Bakış

"Özel alan" adını iki motor paylaşır. Müşteri profili alanları `users_custom_fields` içinde dil başına tanımlanır, değerleri anahtar/değer satırlarıdır. Talep alanları `tickets_custom_fields` içinde iki eksene bağlanır.

İkisinde de şema değişikliği gerekmez: bir tanım satırı eklersiniz, değer genel bir depoya düşer.

## Ön Koşullar

- Tanım ekranlarına erişen bir operatör hesabı ya da ayarlar ve talepler kapsamlı bir jeton.
- Kanca mekanizmasına aşinalık; dönüşüm iki filtre kancasıyla yapılır.
- Kurulu dil kodları: tanım **dil başına bir satırdır**, yani iki dil iki id demektir.

## Yapı

Tanım ile değer ayrıdır; değer satırının adı `field_` ile tanım id'sinin birleşimidir.

| Tablo | Ne tutar | Anahtarı |
| --- | --- | --- |
| `users_custom_fields` | Dil, tip, etiket, seçenekler, işaretler, sıra | `id`, dil başına bir satır |
| `users_informations` | Değer, metin olarak | `owner_id` ve `field_{id}` |
| `tickets_custom_fields` | Talep alanı ve departmanı | `id`; etiket ve tip `_lang` ikizinde |
| `tickets_access_groups` | Yanıt tarafındaki gruplama ve alan id'leri | `id`; ilişki grupta durur |

Depo tipsiz metindir: çok seçimli alan virgülle birleşir, boş cevap eksik satır değil boş dizedir.

## Adım Adım

### Alanı Tanımlayın

1. Tanımı dil başına bir kez oluşturun: Ayarlar altındaki müşteri alanları listesi ya da API kaynağı.
2. Tipi seçin: `text`, `textarea`, `select`, `checkbox`, `radio`; seçim tiplerinde `options` virgüllü liste alır.
3. Görünürlük işaretlerini ayarlayın; bağımsız mantıksal değerlerdir ve birlikte çalışırlar.
4. Satıra verilen id'yi not edin; kodunuz etiketi değil id'yi kullanır.

### Değeri Okuyun ve Yazın

1. Bilgi yardımcısıyla, `field_{id}` anahtarlarıyla yazın.
2. Bilgi okuyucusuyla okuyun: satırı olmayan ad eksik anahtar olarak değil `null` olarak döner.
3. Okuyucu istek boyunca hafızaya alır; yazdıktan sonra üçüncü argümanla atlayın.
4. "Cevaplanmadı" anlamlıysa boşaltmak yerine silin: yedek yol yalnız kaldırılmış satırı arar.

### Değeri Dönüştürün

1. Kaydetme filtresine dinleyici kaydedin: her `field_*` anahtarı için bir kez, yazmadan önce.
2. Okuma filtresine kaydedin: her okumada, satırlar toplandıktan sonra.
3. İkisini simetrik tutun; yoksa profil ekranında şifreli metin kalır.
4. Alan id'sini kontrol edin: iki filtre de **her** özel alan için çalışır.

## Referans

### Tanım İşaretleri

Altısının beşi 0 ya da 1 tutan tam sayı kolonudur; `status` bir dizedir ve onu diğerleri gibi ele alan kod her alanı kapalı okur.

- **status**: `active` ya da `inactive`; kapalı alan yalnız operatörün listesinde görünür.
- **required**: Boş cevap bütünlük kapısını besler; kapı müşteriyi profil ekranına yönlendirir.
- **uneditable**: Görünür ama yazılamaz; profil kaydetme döngüsü bu alanları atlar.
- **client_hidden**: Yalnız operatör ve **diğerlerinin üzerinde**: kayıt formundan, faturadan ve zorunlu kümeden düşer.
- **invoice**: Fatura görünümünde çıkar; boş değerler atılır.
- **signForm**: Kayıt sırasında sorulur; olmadan alana yalnız profil ekranından erişilir.

### Değer Yardımcısı

```php
// Upsert. $values bir ad => değer haritasıdır; özel alanın adı "field_{id}" biçimindedir.
// En az bir satır yazıldığında true döner.
public static function AddInfo($owner_id = 0, $values = []);
public static function setInfo($owner_id = 0, $values = []);   // AddInfo takma adı

// Okuma. $names bir dizidir (virgüllü dize de kabul edilir ve bölünür).
// İSTENEN HER ad dönen haritada bulunur; satırı olmayan ad null okunur.
// $noCache = true istek-içi hafızayı atlar; yazdıktan hemen sonra buna ihtiyacınız olur.
public static function getInfo($owner_id = 0, $names = [], $noCache = false): array;

// Satırları tamamen kaldırır. $name tek bir ad ya da ad dizisidir.
public static function deleteInfo($owner_id = 0, $name = ''): int|bool;

// Var olan bir değerin satır id'si, yoksa 0.
public static function isInfo($owner_id = 0, $name = ''): int;

// Ters arama: bu değeri hangi müşteriler taşıyor. $first = true tek satır döndürür.
public static function findInfo($name = '', $value = '', $first = true): array;

// Bütünlük kapısı. Hâlâ boş olan her şey için [alan anahtarı => etiket] döner;
// boş dizi, hesabın müşteri panelini kullanabileceği anlamına gelir.
public static function missing_required_fields($id = 0): array;
```

- **User::AddInfo()**: Girdi başına bir satır. `phone` ve `company_name` müşteri kaydına da yansır.
- **User::getInfo()**: İstek başına hafızalanır ve hafıza **ham** değeri tutar, yani okuma filtresi yine çalışır.
- **User::missing_required_fields()**: Operatörün zorunlu alanları ile yerleşik kayıt zorunluluklarını, müşterinin dili için birleştirir.
- **Sıra tuzağı: findInfo()**: **Önce adı, sonra değeri** alır, çünkü müşteriler arasında arar. İlk yuvaya müşteri id'si geçmek boş dizi döndürür.
- **Değerin biçimi**: Her şey metindir. `checkbox` virgülle birleşir; diğer dört tip düz dizedir.

### İki Değer Filtresi

İkisi de referansla ateşlenir ve `field_` ile başlayan anahtarlarla sınırlıdır; `timezone` onlara hiç ulaşmaz.

```php
// User::AddInfo() içinden, insert ya da update'ten hemen önce ateşlenir.
// $value      mixed, referansla. Burada bıraktığınız şey saklanır.
// $field_id   int, "field_5" içinden ayrıştırılan tanım id'si.
// $owner_id   int, değerin ait olduğu müşteri.
Hook::add('filter:custom_field.save_value', 10, function (&$value, $field_id, $owner_id) {
    // ...
});

// User::getInfo() içinden, satırlar toplandıktan sonra ve dönüşten önce ateşlenir.
// Aynı üç argüman, aynı sıra.
Hook::add('filter:custom_field.load_value', 10, function (&$value, $field_id, $owner_id) {
    // ...
});
```

- **filter:custom_field.save_value**: Kalıcılaşma sınırı; aşağıdaki hiçbir katman değeri yeniden yazmaz. `$value`'yu yerinde değiştirin; dönüş kullanılmaz.
- **filter:custom_field.load_value**: Okuma sınırı: şifre çözün, biçimlendirin ya da saklanmayan bir değeri enjekte edin. Dönüş kullanılmaz.
- **action:client.profile_updated**: Profil kaydından sonra müşteri id'si, değişen kolonlar ve bilgi haritasıyla ateşlenir. Haritadaki anahtar kaydedildiğini gösterir, değiştiğini değil. Dönüş yoksayılır.

### Tanımlar Nerelerde Okunur

Müşteriye bakan her sayfa kendi filtresini uygular ve bunlar bilerek farklıdır.

| Sayfa | Okuyucu | Uyguladığı filtre |
| --- | --- | --- |
| Hesap profili | Hesap modelinde `get_custom_fields()` | aktif, gizli değil, müşterinin dili, rank sırası |
| Kayıt formu | Kayıt modelinde `get_custom_fields()` | aynısı, artı `signForm = 1` |
| Fatura görünümü | `invoice_custom_fields()` | `invoice = 1`; boş değerler atılır |
| Tek alan doğrulaması | `get_custom_field()` | liste filtresi, tek id'ye |
| Operatör sayfaları | Kullanıcı modelinde `get_custom_fields()` | yalnız dil; gizli alanlar dahil |

### Talep Alanları ve İki Ekseni

Aynı tanım tablosunu iki oluşturucu tüketir; iki üyelik iki ayrı yerde saklanır.

```php
// Departman ekseni, talep OLUŞTURMA formu kullanır. $did = 0 her alanı döndürür.
// Satırlar yerelleştirilmiş name, description, type, properties, options ve
// department_name taşır. Sıra: did DESC, sonra rank ASC.
public static function custom_fields($lang = '', $did = 0, $status = '');

// Erişim grubu ekseni, YANIT oluşturucunun erişim bilgisi satırları kullanır. Grubun
// kendi virgüllü alan id listesini okur ve alanları TAM O SIRAYLA döndürür; bu, operatörün
// sürükle bırakla belirlediği sıradır. Boş grup [] döndürür.
public static function custom_fields_by_group($lang = '', $groupId = 0, $status = 'active');

// Seçiciyi kurmak için grupların kendisi.
public static function access_groups($lang = '', $status = 'active');
public static function get_access_group($id = 0, $lang = '', $select = '');
```

- **Tickets::custom_fields()**: Departman ekseni. `$status` boş geçilirse kapalı tanımlar da döner.
- **Tickets::custom_fields_by_group()**: Erişim grubu ekseni; değerler kendi anahtar alanıyla şifrelenir ve maskeli gösterilir.
- **filter:ticket.custom_fields**: Departman ekseni listesini filtreler (dil, departman, durum). Referansla değişir; dönüş kullanılmaz.
- **filter:ticket.access_group_fields**: Erişim grubu listesi için aynısı; dönüş kullanılmaz.
- **Talep alanı tipleri**: text, textarea, number, password, select, radio, checkbox. `password` temizleyiciden geçmez.

### API Üzerinden

| Uç | Ne yapar | Not |
| --- | --- | --- |
| `GET /settings/client-fields` | Tanımları listeler | Dil bir parametredir |
| `POST /settings/client-fields` | Tek bir tanım oluşturur | İki dil, iki çağrı |
| `PATCH /settings/client-fields/{id}` | Tek bir tanımı günceller | İşaretlerde kısmi değil: gönderilmeyen işaret 0 olur |
| `PUT /settings/client-fields/order` | Listeyi sıralar | Rank gösterim sırasını belirler |
| `GET /tickets/custom-fields` | Talep tanımlarını listeler | Ayrı motor ve kapsam |

## Örnek

Değeri veritabanında asla açık durmaması gereken bir alan.

```php
// Aynı mantıksal alanın, kurulu her dildeki tanım id'leri. Bir müşteri alanı dil başına
// bir satırdır, yani tek bir etiket birden çok id'ye karşılık gelir.
const ACME_TAX_FIELD_IDS = [11, 12];

Hook::add('filter:custom_field.save_value', 10, function (&$value, $field_id, $owner_id) {
    if (!in_array($field_id, ACME_TAX_FIELD_IDS, true)) return;

    $value = trim((string) $value);
    if ($value === '') return;                      // boş cevap boş kalır

    $value = Crypt::encode($value, Config::get('crypt/system') . '_ACME_TAX');
});

Hook::add('filter:custom_field.load_value', 10, function (&$value, $field_id, $owner_id) {
    if (!in_array($field_id, ACME_TAX_FIELD_IDS, true)) return;
    if ((string) $value === '') return;

    // decode(), bu dinleyici var olmadan önce yazılmış bir değerde false döner; bu yüzden
    // üyenin cevabını boşaltmak yerine özgün metin korunur.
    $plain = Crypt::decode((string) $value, Config::get('crypt/system') . '_ACME_TAX');
    if ($plain !== false) $value = $plain;
});
```

Aynı sözleşmenin okuma tarafı; sonraki hiçbir kod değerin şifreli olduğunu bilmez.

```php
$uid = 42;

// İstediğiniz adları isteyin. Her ad geri döner, satırı olmayanlar null olarak.
$info = User::getInfo($uid, ['field_11', 'gsm']);
$tax  = (string) ($info['field_11'] ?? '');

// Çok seçimli bir alan, virgülle birleştirilmiş tek bir dizedir; ayırmayı siz yapın.
$picked = array_values(array_filter(explode(',', (string) ($info['field_12'] ?? ''))));

// Yazma. Aynı adlar, aynı "field_{id}" kuralı.
User::setInfo($uid, ['field_11' => 'GB123456789']);

// Okuyucu istek başına hafızaya alır; yazdıktan sonra üçüncü argümanla yeniden okuyun.
$fresh = User::getInfo($uid, ['field_11'], true);

// Bir satırı kaldırmak, boş dize yazmakla aynı şey değildir: hesabı hiç cevap vermemiş
// gibi gösteren yalnızca kaldırmadır.
if ($tax === '') User::deleteInfo($uid, ['field_11']);
```

Operatör tarafı, modülünüz kendi alanını getiriyorsa: kurulumda dil başına bir tanım, id'ler saklanır.

```php
$ids = [];

foreach (['en' => 'VAT Number', 'tr' => 'Vergi Numarası'] as $lang => $label) {
    $ids[$lang] = WDB::insert('users_custom_fields', [
        'lang'          => $lang,
        'type'          => 'text',       // text | textarea | select | checkbox | radio
        'name'          => $label,
        'status'        => 'active',     // DİZE, aşağıdaki her bayrağın aksine
        'required'      => 0,
        'uneditable'    => 0,
        'client_hidden' => 0,
        'invoice'       => 1,
        'signForm'      => 1,
        'options'       => '',           // virgülle ayrılmış; select/checkbox/radio için
        'rank'          => 90,
    ]) ? WDB::lastID() : 0;
}

// Id'leri saklayın: alana giden tek kararlı tutamaktır ve dil başına farklıdır.
Config::setd('acme_vat_field_ids', Utility::jencode($ids));
```

## Tuzaklar

> **Alan dil başına bir satırdır ve id'ler farklıdır**
> 
> Dil sürümlerini birleştiren ortak bir üst id yoktur; birinde gizlemek diğerini görünür bırakır. Id'leri küme olarak toplayın.

> **Gizli artı zorunlu, müşteriyi kilitler**
> 
> Kapı her zorunlu alan cevaplanana kadar profile yönlendirir, o yüzden zorunluluk sorguları gizli alanları dışlar. Kendi kuralınızı da oraya uygulayın.

> **Yazımı reddeden tekil aramadır**
> 
> Listeden çıkarmak, sahte bir gönderimin yok saydığı bir arayüz kararıdır; yazımı asıl durduran tekil okuyucudur. Kural iki okuyucuya birden yazılır.

> **İşaret eklemek bir yeri değil sekiz yeri ilgilendirir**
> 
> Yeni bir işaret şunları ister: kolon, yazma yolu, form ve JavaScript dalları, liste kolonu, dil anahtarları, API'nin iki yönü ve tüm müşteri sayfaları. Kolonu koddan önce uygulayın.

> **Fatura okuyucusu id döndürmez**
> 
> Ad ve değer çiftleri verir, boş değerleri atar; id ile arayan bir test her zaman "yok" der.

## İlgili Makaleler

- [Kanca Dinleyicisi Yazma](https://dev.wisecp.com/tr/kanca-dinleyicisi-yazma)
- [Veritabanı Şemasını Değiştirme](https://dev.wisecp.com/tr/veritabani-semasini-degistirme)
- [Alan Yardımcıları](https://dev.wisecp.com/tr/alan-yardimcilari)
- [Özel Operation Ekleme](https://dev.wisecp.com/tr/ozel-operation-ekleme)
- [Çekirdeğe Dokunmadan Çalışma](https://dev.wisecp.com/tr/cekirdege-dokunmadan-calisma)
