Files
Emre Emir 8bbc9dbff2 Initial commit: DLS - Dental Lab System
- Flutter + PocketBase dental lab management system
- Clinic & lab dashboards, job tracking, patient management
- Product catalog, finance tracking, multi-language support
- AI assistant integration, realtime notifications
- Windows installer (Inno Setup) included
- Developed by kovakyazilim.com
2026-06-11 15:57:31 +03:00

419 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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);
```