# 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` | | 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); ```