# DLS — Veritabanı Referansı (Directus)

> **Backend:** Appwrite → **Directus** migrasyonu. Kaynak gerçek bu dokümandır.
> Oluşturulma: 2026-06-05 (Directus MCP ile canlıdan inşa edildi).

## 1. Bağlantı bilgileri

| | Değer |
|---|---|
| Directus URL | (Coolify'da set edilecek) |
| Auth | Directus built-in JWT (email + şifre) |
| Storage | Directus Files (directus_files) |
| Tenant izolasyonu | `tenants` + `tenant_members` tabloları |

> **Appwrite → Directus eşlemesi:**
> - `Appwrite Team` → `tenants` koleksiyonu
> - `Appwrite Team membership` → `tenant_members` koleksiyonu
> - `Appwrite Auth` → `directus_users` (Directus built-in)
> - `Appwrite Storage buckets` → `directus_files` (tek bucket, `kind` alanıyla ayrım)
> - Row-level security → Directus policies + `$CURRENT_USER` + Flutter tarafında `tenant_id` filtresi

## 2. Uygulama özeti (DLS — Dental Lab System)

**Diş klinikleri ↔ diş laboratuvarları** arasındaki protez iş alışverişini dijitalleştirir.
- Klinik bir hasta için protez işi açar → bağlı laboratuvara yollar.
- Lab gelen kutusundan görür, durum adımlarını işler: **Ölçü → Alt Yapı Prova → Üst Yapı Prova → Cila/Bitim**.
- Tamamlanınca iş `sent` → klinik teslim alınca `delivered`.
- Her iki taraf finansal akışı kendi defterinde izler.

## 3. Multi-tenancy & yetki modeli

- **Tenant = `tenants` satırı.** Her tenant'ın bir `kind`'ı var: `clinic` veya `lab`.
- Kullanıcı → `tenant_members` üzerinden bir veya birden fazla tenant'a bağlanır.
- **`member_number`** (12 hane, unique): tenant'ın **bağlantı kodu**. Login'de kullanılmaz; sadece iki tenant'ı eşlemek için.
- Flutter tarafında her sorguda `tenant_id` (veya `clinic_tenant_id`/`lab_tenant_id`) filtresi **zorunlu**.
- Cross-tenant tablolar (`connections`, `jobs`, `job_files`, `job_status_history`): hem `clinic_tenant_id` hem `lab_tenant_id` taşır — iki taraf da erişir.

## 4. Koleksiyonlar (17)

> Notasyon: `IDX`=index, `UNQ`=unique, `FK`=foreign key, `DEF`=default.
> Tüm tablolarda sistem alanları: `id` (uuid PK), `date_created`, `date_updated` (varsa).

### `tenants` — tenant profili (Appwrite Team karşılığı)
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| kind | enum[lab\|clinic] | tenant türü · **IDX** |
| member_number | string(12) | bağlantı kodu · **UNQ** |
| company_name | string(255) | |
| company_tax_id | string(50) | |
| company_address | text | |
| company_email | string(255) | |
| company_phone | string(30) | |
| logo | uuid | → `directus_files` |
| default_currency | string(8) | DEF: `TRY` |
| status | enum[active\|suspended] | DEF: `active` |

### `tenant_members` — kullanıcı ↔ tenant üyeliği
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` CASCADE · **IDX** |
| user_id | uuid | → `directus_users` CASCADE |
| role | enum[owner\|admin\|member] | DEF: `member` |

### `profiles` — kullanıcı başına ek bilgi
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` |
| user_id | uuid | → `directus_users` |
| display_name | string(255) | |
| phone | string(30) | |
| title | string(100) | |

### `connections` — iki tenant arası bağlantı
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| clinic_tenant_id | uuid | → `tenants` · **IDX** |
| lab_tenant_id | uuid | → `tenants` · **IDX** |
| status | enum[pending\|approved\|rejected] | DEF: `pending` · **IDX** |
| requested_by | uuid | → `directus_users` |
| requested_at | timestamp | |
| approved_at | timestamp | |
| rejected_at | timestamp | |

> **Unique constraint:** `(clinic_tenant_id, lab_tenant_id)` — aynı çift iki kez bağlanamaz. Flutter tarafında kontrol edilmeli; DB constraint Directus MCP ile eklenemez, migration SQL ile ekle.

### `patients` — klinik hasta kayıtları
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| clinic_tenant_id | uuid | → `tenants` · **IDX** |
| created_by | uuid | → `directus_users` |
| patient_code | string(50) | klinik içinde unique (soft) |
| first_name | string(100) | |
| last_name | string(100) | |
| phone | string(30) | |
| date_of_birth | date | |
| notes | text | |
| archived | boolean | DEF: false |

### `jobs` — protez işi (çekirdek tablo — en ağır)
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| clinic_tenant_id | uuid | → `tenants` · **IDX** |
| lab_tenant_id | uuid | → `tenants` · **IDX** |
| created_by | uuid | → `directus_users` |
| patient_id | uuid | → `patients` SET NULL · **IDX** |
| patient_code | string(50) | |
| prosthetic_id | uuid | → `prosthetics` SET NULL |
| prosthetic_type | enum[metal_porselen\|zirkonyum\|implant_ustu_zirkonyum\|gecici\|e_max\|diger] | |
| member_count | integer | üye (diş) sayısı |
| teeth | json | diş numaraları dizisi `List<String>` |
| color | string(20) | Vita renk kodu |
| description | text | |
| price | decimal(10,2) | |
| currency | string(8) | |
| status | enum[pending\|in_progress\|sent\|delivered\|cancelled] | DEF: `pending` · **IDX** |
| current_step | enum[olcu\|alt_yapi_prova\|ust_yapi_prova\|cila_bitim] | |
| location | enum[at_clinic\|at_lab] | DEF: `at_clinic` |
| due_date | timestamp | |

**Query pattern (Flutter):**
```
// Lab gelen kutusu
GET /items/jobs?filter[lab_tenant_id][_eq]=$tenantId&filter[status][_eq]=pending
  &sort=-date_created&limit=50&page=1

// Klinik giden işler
GET /items/jobs?filter[clinic_tenant_id][_eq]=$tenantId
  &sort=-date_created&limit=50&page=1

// Filtre + arama
&filter[status][_in]=pending,in_progress
&search=hasta_kodu
```

### `job_files` — işe bağlı dosyalar
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| job_id | uuid | → `jobs` CASCADE · **IDX** |
| clinic_tenant_id | uuid | → `tenants` CASCADE · **IDX** |
| lab_tenant_id | uuid | → `tenants` CASCADE · **IDX** |
| uploaded_by | uuid | → `directus_users` |
| file_id | uuid | → `directus_files` SET NULL |
| kind | enum[scan\|image\|document] | |
| name | string(255) | |
| size | integer | bayt |
| mime_type | string(100) | |
| archived_at | timestamp | soft-delete; set → download disabled |

### `job_status_history` — stepper denetim izi
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| job_id | uuid | → `jobs` CASCADE · **IDX** |
| clinic_tenant_id | uuid | → `tenants` |
| lab_tenant_id | uuid | → `tenants` |
| step | enum[olcu\|alt_yapi_prova\|ust_yapi_prova\|cila_bitim] | |
| completed_by | uuid | → `directus_users` |
| completed_at | timestamp | |
| note | text | |

### `prosthetics` — lab ürün kataloğu
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` · **IDX** |
| created_by | uuid | → `directus_users` |
| name | string(255) | |
| type | enum[...prosthetic_types...] | |
| unit_price | decimal(10,2) | |
| currency | string(8) | DEF: `TRY` |
| archived | boolean | DEF: false · **IDX** |

### `finance_entries` — tek-taraflı defter
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` · **IDX** |
| created_by | uuid | → `directus_users` |
| job_id | uuid | → `jobs` SET NULL |
| counterpart_tenant_id | uuid | → `tenants` SET NULL |
| type | enum[income\|expense\|receivable\|payable] | |
| amount | decimal(12,2) | |
| currency | string(8) | |
| status | enum[pending\|paid\|cancelled] | DEF: `pending` · **IDX** |
| date | timestamp | **IDX** |
| description | text | |

> **Cross-tenant sync (Directus Flow):** İş `sent`/`delivered` olunca:
> - Lab tarafı → `receivable/pending` satır
> - Klinik tarafı → `payable/pending` satır
> Idempotent: `(job_id, tenant_id, type)` kombinasyonu varsa atla.

### `payments` — ödeme kayıtları
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` |
| counterpart_tenant_id | uuid | → `tenants` |
| direction | enum[inflow\|outflow] | |
| amount | decimal(12,2) | |
| currency | string(8) | |
| payment_date | timestamp | |
| method | string(30) | |
| notes | text | |
| recorded_by | uuid | → `directus_users` |
| status | enum[pending\|confirmed\|rejected] | DEF: `confirmed` |

### `clinic_pricing` — kliniğe özel lab fiyatı
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| lab_tenant_id | uuid | → `tenants` CASCADE |
| clinic_tenant_id | uuid | → `tenants` CASCADE |
| prosthetic_type | enum[...] | |
| custom_unit_price | decimal(10,2) | |
| discount_percent | decimal(5,2) | |
| currency | string(8) | |
| created_by | uuid | → `directus_users` |

> **Unique:** `(lab_tenant_id, clinic_tenant_id, prosthetic_type)` — Flutter tarafında kontrol et.

### `notifications`
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` · **IDX** |
| user_id | uuid | → `directus_users` CASCADE |
| job_id | uuid | → `jobs` SET NULL |
| connection_id | uuid | → `connections` SET NULL |
| message | string(500) | |
| read | boolean | DEF: false · **IDX** |
| severity | enum[info\|warning] | DEF: `info` |

### `audit_logs`
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` |
| user_id | uuid | → `directus_users` |
| action | enum[create\|update\|delete] | |
| entity_type | string(50) | |
| entity_id | string(36) | |
| changes | json | |
| ip_address | string(50) | |
| user_agent | string(500) | |

### `invite_links`
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| tenant_id | uuid | → `tenants` CASCADE |
| code | string(32) | **UNQ** |
| email | string(255) | |
| role | enum[admin\|member] | |
| status | enum[pending\|accepted\|cancelled\|expired] | DEF: `pending` |
| invited_by | uuid | → `directus_users` |
| expires_at | timestamp | |
| accepted_at | timestamp | |
| accepted_by | uuid | → `directus_users` |

### `user_preferences`
| Alan | Tip | Not |
|---|---|---|
| id | uuid | PK |
| user_id | uuid | → `directus_users` CASCADE |
| theme | enum[light\|dark\|system] | DEF: `system` |
| color_theme | string(50) | |

## 5. İş akışı (jobs lifecycle)

```
Klinik açar (pending)
  → Lab "İşleme Al" (in_progress, currentStep: alt_yapi_prova, location: at_lab)
    → Lab "Kliniğe Gönder" (location: at_clinic) — prova için
      → Klinik "Provayı Onayla" (currentStep++, location: at_lab)
      → Klinik "Düzeltme İste" (location: at_lab, step aynı)
    → [Cila/Bitim sonrası] Lab "Kliniğe Gönder" (status: sent, location: at_clinic)
      → Klinik "Teslim Al" (status: delivered)
        → finance-sync tetiklenir
        → job_files arşivlenir (archived_at set)
```

**Adım sırası:** `olcu → alt_yapi_prova → ust_yapi_prova → cila_bitim`

> Not: `acceptJob` = olcu tamamlandı anlamına gelir; `currentStep` direkt `alt_yapi_prova`'ya atlar.

## 6. Query Optimizasyon Kuralları (Flutter)

### Zorunlu filtreler — hiç ihmal etme
```dart
// Her jobs sorgusunda tenant filtresi ZORUNLU
filter: {
  'lab_tenant_id': {'_eq': tenantId},   // lab tarafı
  // veya
  'clinic_tenant_id': {'_eq': tenantId}, // klinik tarafı
}
```

### Sayfalama — sonsuz liste / cursor
```dart
// İlk yükleme
limit: 30, page: 1

// Sonraki sayfa (offset bazlı)
limit: 30, offset: 30

// Toplam sayı için ayrı istek (pahalı — sadece gerektiğinde)
meta: 'filter_count'
```

### Field projection — sadece gerekeni çek
```dart
// Jobs listesi — detay alanlarını çekme
fields: ['id', 'patient_code', 'prosthetic_type', 'status',
         'current_step', 'location', 'date_created', 'due_date',
         'clinic_tenant_id.company_name', 'lab_tenant_id.company_name']

// Jobs detay — tam veri
fields: ['*', 'patient_id.*', 'job_files.*']
```

### Relation join — N+1 önleme
```dart
// Kötü: jobs çek → her iş için ayrı tenant sorgusu
// İyi: nested fields ile tek sorguda
fields: ['*', 'clinic_tenant_id.company_name', 'lab_tenant_id.company_name']
```

### Finance listesi — tarih aralığı ile kes
```dart
filter: {
  'tenant_id': {'_eq': tenantId},
  'date': {'_gte': '2026-01-01'},        // son 6 ay gibi
  'status': {'_neq': 'cancelled'},
}
sort: ['-date']
limit: 50
```

## 7. Klinik vs Lab — Ayrı Akışlar

| Ekran | Klinik | Lab |
|---|---|---|
| Ana sayfa | Gönderilen işler özeti, bekleyen ödemeler | Gelen işler özeti, işlemdeki işler |
| İşler ana liste | Giden işler (`clinic_tenant_id`) | Gelen işler (`lab_tenant_id`) |
| İş aksiyon butonu | Provayı Onayla / Düzeltme İste / Teslim Al | İşleme Al / Kliniğe Gönder |
| Ürünler | — | Katalog CRUD |
| Hastalar | Hasta kayıtları | — |
| Bağlantı kur | Lab'ı `member_number` ile ara | Gelen talepleri onayla/reddet |
| Finans | Borçlar (payable) | Alacaklar (receivable) |

## 8. Flutter → Directus API pattern

```dart
// Auth
POST /auth/login
  body: { email, password }
  returns: { access_token, refresh_token, expires }

// Token yenileme
POST /auth/refresh
  body: { refresh_token }

// Koleksiyon okuma
GET /items/{collection}?filter[field][_eq]=value&limit=30&sort=-date_created

// Tekil kayıt
GET /items/{collection}/{id}?fields=*,relation.*

// Oluşturma
POST /items/{collection}
  body: { field: value, ... }

// Güncelleme
PATCH /items/{collection}/{id}
  body: { field: value }

// Dosya yükleme
POST /files
  Content-Type: multipart/form-data
  field: file (binary)
  returns: { id, filename_download, ... }
```

## 9. Directus Flows (server-side otomasyonlar)

Aşağıdaki iş mantıkları Flutter client'tan değil Directus Flow'dan tetiklenmeli:

| Tetikleyici | Flow | Açıklama |
|---|---|---|
| `jobs.status → sent\|delivered` | finance-sync | Lab receivable + klinik payable oluştur (idempotent) |
| `jobs.status → delivered` | archive-job-files | `job_files.archived_at` set et |
| `jobs` UPDATE | audit-log | `audit_logs` satır yaz |
| `connections.status → approved\|rejected` | notify-connection | İlgili tarafa bildirim gönder |

> Flows henüz oluşturulmadı — bir sonraki adım.

## 10. Eksik kısıtlamalar (SQL ile eklenecek)

Directus MCP composite unique constraint desteklemiyor; veritabanına direkt SQL ile eklenecek:

```sql
-- connections çifti unique
ALTER TABLE connections
  ADD CONSTRAINT connections_pair_unique UNIQUE (clinic_tenant_id, lab_tenant_id);

-- clinic_pricing üçlüsü unique
ALTER TABLE clinic_pricing
  ADD CONSTRAINT clinic_pricing_triple_unique
  UNIQUE (lab_tenant_id, clinic_tenant_id, prosthetic_type);
```
