Initial commit — DLS lab-app Flutter project
This commit is contained in:
+418
@@ -0,0 +1,418 @@
|
||||
# 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);
|
||||
```
|
||||
Reference in New Issue
Block a user