> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flextell.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Örnek istemci

> JavaScript ile bağlanmayı, kanala abone olmayı ve olayları dinlemeyi örnekle anlatır.

Bu sayfa, `pusher-js` kütüphanesiyle Flextell'in gerçek zamanlı kanallarına bağlanan çalışır bir istemci kurar. İstemci kliniğin sohbet kanalına abone olur, olayları dinler ve bağlantı koptuğunda veriyi REST API'den yeniden okur. Kanalların ve olayların tam listesi için [Kanallar ve olaylar](/api/gercek-zamanli/kanallar-ve-olaylar) sayfasına bakın.

<Note>
  Gerçek zamanlı olaylar yalnızca "bir şey değişti" bilgisini taşır. Kaçan olaylar sonradan tekrar gönderilmez. REST API her zaman doğru veri kaynağıdır; olay aldığınızda ya da yeniden bağlandığınızda ilgili kaydı API'den okuyun.
</Note>

## Kurulum

Başlamadan önce şunlara ihtiyacınız var:

| Gereken | Nereden alınır |
| - | - |
| OAuth `access_token` | [Token'lar](/api/kimlik-dogrulama/tokenlar) sayfasındaki akışla alın. |
| Klinik kimliği (`X-Tenant` değeri) | [Klinikler ve X-Tenant](/api/klinikler-ve-x-tenant) |
| Pusher anahtarı (herkese açık anahtar) | Flextell destek ekibinden isteyin: [Destek](/api/destek) |
| Kümesi ve TLS | Küme `eu`, `forceTLS` açık |
| Yetki | Hesabın kliniğinde `View:Conversation` izni |

Kütüphaneyi kurun:

```bash theme={null}
npm install pusher-js
```

## Bağlanma ve yetkilendirme

Flextell kanalları `private-` önekiyle başlar. Pusher her abonelikten önce `socket_id` ve kanal adını sizin yetkilendirme uç noktanıza gönderir. Siz de bunu Flextell'e iletirsiniz:

`POST https://app.flextell.ai/api/broadcasting/auth`

Bu adres `/api/v1` önekini taşımaz. İsteğin gövdesi `socket_id` ve `channel_name` alanlarıdır. Form (`application/x-www-form-urlencoded`) ya da JSON olarak gönderebilirsiniz.

| Başlık | Değer |
| - | - |
| `Authorization` | `Bearer <access_token>` (zorunlu) |
| `Accept` | `application/json` |

Bu uç nokta scope istemez ve `X-Tenant` başlığına bakmaz. Klinik yetkisi kanal adından belirlenir.

```javascript theme={null}
import Pusher from 'pusher-js'

const API_ROOT = 'https://app.flextell.ai/api'
const AUTH_URL = `${API_ROOT}/broadcasting/auth`
const TENANT_ID = 123 // klinik kimliği

// Her yetkilendirmede güncel token'ı döndürün (süresi dolan token'ı yenileyin).
async function getAccessToken() {
  return '<access_token>'
}

const pusher = new Pusher('<PUSHER_KEY>', {
  cluster: 'eu',
  forceTLS: true,
  channelAuthorization: {
    endpoint: AUTH_URL,
    transport: 'ajax',
    customHandler: async ({ socketId, channelName }, callback) => {
      try {
        const res = await fetch(AUTH_URL, {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${await getAccessToken()}`,
            Accept: 'application/json',
            'Content-Type': 'application/x-www-form-urlencoded',
          },
          body: new URLSearchParams({
            socket_id: socketId,
            channel_name: channelName,
          }),
        })
        if (!res.ok) return callback(new Error(`Yetkilendirme başarısız (${res.status})`), null)
        callback(null, await res.json()) // { "auth": "<anahtar>:<imza>" }
      } catch (err) {
        callback(err, null)
      }
    },
  },
})
```

Yetkilendirme hataları şöyle döner:

| HTTP | Hata kodu | Ne zaman |
| - | - | - |
| 401 | `UNAUTHENTICATED` | Token eksik, geçersiz veya süresi dolmuş. |
| 403 | `FORBIDDEN` | Kullanıcı bu kanala katılamaz. |

<Tip>
  Token ömrü kısadır. `customHandler` içinde token'ı sabit bir değişkenden okumak yerine her çağrıda güncel değeri alın. Yeniden bağlanmada Pusher yetkilendirmeyi yeniden ister. Token yenileme için [Token'lar](/api/kimlik-dogrulama/tokenlar) sayfasına bakın.
</Tip>

## Kanala abone olma

Sohbet olayları için iki kanal vardır. Hangisine katılabileceğiniz izinlerinize bağlıdır.

| Kanal | Kim katılır | Ne taşır |
| - | - | - |
| `private-chat-log.{tenantId}` | `View:Conversation` iznine sahip ve tüm sohbetleri görebilen kullanıcı (`ViewAllConversations:Conversation` izni veya platform yöneticisi) | Mesaj içeriği dahil tam olay yükleri |
| `private-chat-log-activity.{tenantId}` | `View:Conversation` iznine sahip her klinik kullanıcısı | Yalnızca kimlikler (ör. `conversation_id`) |

Tüm sohbetleri göremeyen bir kullanıcı `private-chat-log` kanalında 403 alır. Örnek istemciyi her kullanıcıda çalışacak şekilde `private-chat-log-activity` kanalı üzerine kurmak en güvenli yoldur: olay gelince ilgili kaydı REST API'den okursunuz, API de yalnızca kullanıcının görebildiği sohbetleri döndürür.

```javascript theme={null}
const channel = pusher.subscribe(`private-chat-log-activity.${TENANT_ID}`)

channel.bind('pusher:subscription_succeeded', () => {
  // İlk abonelikte ve her yeniden bağlanmada çalışır.
  refreshConversations()
})

channel.bind('pusher:subscription_error', (err) => {
  // 401/403 durumunda buraya düşer.
  console.warn('Kanala abone olunamadı', err)
})
```

Tam yüklere de ihtiyacınız varsa ve kullanıcı tüm sohbetleri görebiliyorsa `private-chat-log.${TENANT_ID}` kanalına da aynı şekilde abone olun.

Bir sohbetteki Flexy önerisini canlı izlemek için sohbet başına ayrı bir kanal vardır: `private-ai-suggestion.{conversationId}`. Kullanıcının `View:Conversation` izni olmalı ve sohbeti görebilmelidir. Sohbeti kapattığınızda `pusher.unsubscribe(...)` ile kanaldan çıkın.

## Olayları işleme

Olay adı, olay sınıfının tam adıdır. `pusher-js` ile adı tek ters eğik çizgiyle, kod içinde ise kaçışlı yazarak bağlayın.

```javascript theme={null}
channel.bind('Modules\\Chat\\Events\\ChatLogChanged', ({ conversation_id, chat_log_id }) => {
  // chat_log_id null olabilir. Mesajı REST API'den okuyun.
  refreshMessages(conversation_id)
})

channel.bind('Modules\\Chat\\Events\\ConversationRead', ({ conversation_id }) => {
  markAsRead(conversation_id)
})

channel.bind('Modules\\Chat\\Events\\ConversationCreated', () => refreshConversations())
channel.bind('Modules\\Chat\\Events\\ConversationDeleted', () => refreshConversations())
```

Aşağıdaki fonksiyonlar uygulamanıza özgüdür. Veriyi her zaman API'den okuyun:

```javascript theme={null}
async function api(path) {
  const res = await fetch(`${API_ROOT}/v1${path}`, {
    headers: {
      Authorization: `Bearer ${await getAccessToken()}`,
      'X-Tenant': String(TENANT_ID),
      Accept: 'application/json',
    },
  })
  if (!res.ok) throw new Error(`API hatası (${res.status})`)
  return res.json()
}

const refreshConversations = () => api('/conversations')
const refreshMessages = (id) => api(`/conversations/${id}/messages`)
```

Dikkat edilecek noktalar:

* Olayların sırası garanti değildir. Örneğin `ConversationRead`, sonradan oluşan bir mesaj olayından sonra gelebilir.
* Sizin işleminizin olayı size de gelir. Aynı kaydı iki kez işlememek için kimliğe göre tekilleştirin.
* `ChatLogCreated` içinde medya mesajının dosyası henüz hazır olmayabilir. Dosya hazır olunca `ChatLogUpdated` gelir.
* `ConversationCreated`, `ConversationDeleted`, `ConversationRead` gibi olaylar yalnızca kimlik taşır; ayrıntı için API'ye gidin.

<Info>
  Laravel Echo kullanıyorsanız kanal adını `private-` öneki olmadan yazın ve olay adının başına nokta koyun: `Echo.private('chat-log-activity.123').listen('.Modules\\Chat\\Events\\ChatLogChanged', cb)`. Nokta olmazsa Echo olay adının başına kendi ad alanını ekler ve olay eşleşmez.
</Info>

### Flexy önerisi olayları

`private-ai-suggestion.{conversationId}` kanalı iki olay yayınlar. Yük 9000 bayttan büyükse `requires_hydration: true` gelir; bu durumda öneriyi `GET /api/v1/conversations/{id}/ai-suggestion` ile okuyun.

```javascript theme={null}
const conversationId = 987
const ai = pusher.subscribe(`private-ai-suggestion.${conversationId}`)

const onSuggestion = (payload) => {
  if (payload.requires_hydration) {
    api(`/conversations/${conversationId}/ai-suggestion`)
  } else {
    // payload öneri alanlarını zaten içerir
  }
}

ai.bind('Modules\\Ai\\Events\\AiSuggestionCreated', onSuggestion)
ai.bind('Modules\\Ai\\Events\\AiSuggestionUpdated', onSuggestion)
```

## Yeniden bağlanma

`pusher-js` bağlantı koptuğunda kendiliğinden yeniden bağlanır ve kanallara yeniden abone olur. Koptuğu sürede gelen olaylar tekrar gönderilmez. Bu yüzden her başarılı abonelikte veriyi API'den yeniden okuyun.

<Steps>
  <Step title="Aboneliği dinleyin">
    `pusher:subscription_succeeded` olayına bağlanın. Bu olay hem ilk abonelikte hem de her yeniden bağlanmada çalışır.
  </Step>

  <Step title="Durumu yeniden okuyun">
    Olayın içinde sohbet listesini ve açık sohbetin mesajlarını `GET /api/v1/conversations` ve `GET /api/v1/conversations/{id}/messages` ile yenileyin.
  </Step>

  <Step title="Bağlantı durumunu izleyin">
    `pusher.connection.bind('state_change', ...)` ile durumu izleyin. Durum `unavailable` veya `failed` olduğunda kullanıcıya canlı bağlantının kesildiğini gösterin ve elle yenileme seçeneği sunun.
  </Step>

  <Step title="Klinik değişince kanaldan çıkın">
    Kullanıcı başka bir kliniğe geçtiğinde `pusher.unsubscribe(...)` ile eski kanalı bırakın ve yeni klinik kimliğiyle abone olun. Oturum kapanınca `pusher.disconnect()` çağırın.
  </Step>
</Steps>

```javascript theme={null}
pusher.connection.bind('state_change', ({ current }) => {
  if (current === 'unavailable' || current === 'failed') {
    showOfflineNotice()
  }
})
```

<Warning>
  Yetkilendirme isteği 401 dönerse token'ın süresi dolmuştur. Token'ı yenileyip aboneliği yeniden kurun. 403 dönerse kullanıcının o kanal için izni yoktur; yeniden denemek sonucu değiştirmez.
</Warning>

## İlgili sayfalar

<CardGroup cols={2}>
  <Card title="Genel bakış" icon="signal" href="/api/gercek-zamanli/genel-bakis">
    Gerçek zamanlı mimari ve yetkilendirme akışı.
  </Card>

  <Card title="Kanallar ve olaylar" icon="list" href="/api/gercek-zamanli/kanallar-ve-olaylar">
    Tüm kanallar, olay adları ve yükler.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.