Initial commit — DLS lab-app Flutter project

This commit is contained in:
egecankomur
2026-06-10 23:22:15 +03:00
commit d1acc1d367
225 changed files with 31294 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# MCP server config — contains API keys and credentials
.mcp.json
# Claude Code project settings — local dev tooling only
.claude/
+45
View File
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: android
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: ios
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: linux
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: macos
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: web
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: windows
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+16
View File
@@ -0,0 +1,16 @@
# lab_app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+45
View File
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
id("kotlin-android")
id("com.google.gms.google-services")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.kovakyazilim.labapp"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.kovakyazilim.labapp"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
+29
View File
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "751114036897",
"project_id": "dlslabapp",
"storage_bucket": "dlslabapp.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:751114036897:android:5da96f1a9691458099f2e0",
"android_client_info": {
"package_name": "com.kovakyazilim.labapp"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBjRvRur8zmczbDNOad0PGuFy19XRGS8QE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+45
View File
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.kovakyazilim.labapp
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DLS</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+27
View File
@@ -0,0 +1,27 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
include(":app")
+418
View File
@@ -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);
```
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+46
View File
@@ -0,0 +1,46 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf-with-dsym' if config.name == 'Release'
end
end
end
+147
View File
@@ -0,0 +1,147 @@
PODS:
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- onesignal_flutter (5.5.8):
- Flutter
- OneSignalXCFramework (= 5.5.2)
- OneSignalXCFramework (5.5.2):
- OneSignalXCFramework/OneSignalComplete (= 5.5.2)
- OneSignalXCFramework/OneSignal (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalExtension
- OneSignalXCFramework/OneSignalLiveActivities
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalComplete (5.5.2):
- OneSignalXCFramework/OneSignal
- OneSignalXCFramework/OneSignalInAppMessages
- OneSignalXCFramework/OneSignalLocation
- OneSignalXCFramework/OneSignalCore (5.5.2)
- OneSignalXCFramework/OneSignalExtension (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalInAppMessages (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalLiveActivities (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalLocation (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalNotifications (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalExtension
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalOSCore (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalOutcomes (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalUser (5.5.2):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- onesignal_flutter (from `.symlinks/plugins/onesignal_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- OneSignalXCFramework
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
onesignal_flutter:
:path: ".symlinks/plugins/onesignal_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
onesignal_flutter: 75c70a45a8d97e685273a14f04521ec121611458
OneSignalXCFramework: 2f46ff87ccefd9afe8e3b5f9fe357072191205ff
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
PODFILE CHECKSUM: d106a30630b099793eceddfd6e3c64af8abe90f1
COCOAPODS: 1.16.2
+746
View File
@@ -0,0 +1,746 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
604C3E861E9F106B3AA6CCEF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9221FFF6D01C1D6BF22E5ADA /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C9B4075C0256AC482F1CDD77 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFD5D57E8CDD24E2D84F3E9B /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0E5C1D2C3B5E98A6A4BF3DE7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
468E8F6599C9CD0A40663335 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
58AF9370CD56571E93F6935E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
5EDEE6A633E332DED57944E4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7A46E2402004ACC00F45B6B1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9221FFF6D01C1D6BF22E5ADA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AFD5D57E8CDD24E2D84F3E9B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F2CF6120D4E22FEE6FB14E1F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
F766D9502FD64D820017E914 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
F766D9512FD64D820017E916 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
604C3E861E9F106B3AA6CCEF /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D66E69B2ECA4A5318EDFF043 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C9B4075C0256AC482F1CDD77 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
61E03C992BD404E58CDC96E8 /* Pods */ = {
isa = PBXGroup;
children = (
468E8F6599C9CD0A40663335 /* Pods-Runner.debug.xcconfig */,
0E5C1D2C3B5E98A6A4BF3DE7 /* Pods-Runner.release.xcconfig */,
5EDEE6A633E332DED57944E4 /* Pods-Runner.profile.xcconfig */,
7A46E2402004ACC00F45B6B1 /* Pods-RunnerTests.debug.xcconfig */,
F2CF6120D4E22FEE6FB14E1F /* Pods-RunnerTests.release.xcconfig */,
58AF9370CD56571E93F6935E /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
6F5B57133C9496CF920C3E25 /* Frameworks */ = {
isa = PBXGroup;
children = (
9221FFF6D01C1D6BF22E5ADA /* Pods_Runner.framework */,
AFD5D57E8CDD24E2D84F3E9B /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
61E03C992BD404E58CDC96E8 /* Pods */,
6F5B57133C9496CF920C3E25 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
F766D9502FD64D820017E914 /* RunnerDebug.entitlements */,
F766D9512FD64D820017E916 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
416DB929131C2E55EE26F9D1 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
D66E69B2ECA4A5318EDFF043 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
42D030382DFE9994547FE992 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
7B78FAA6FA9F3F82D81A2E70 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
416DB929131C2E55EE26F9D1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
42D030382DFE9994547FE992 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
7B78FAA6FA9F3F82D81A2E70 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = XP74C5889V;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kovakyazilim.labapp;
PRODUCT_NAME = DLS;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7A46E2402004ACC00F45B6B1 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kovakyazilim.labapp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F2CF6120D4E22FEE6FB14E1F /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kovakyazilim.labapp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 58AF9370CD56571E93F6935E /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kovakyazilim.labapp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = XP74C5889V;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kovakyazilim.labapp;
PRODUCT_NAME = DLS;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = XP74C5889V;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kovakyazilim.labapp;
PRODUCT_NAME = DLS;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "DLS.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "DLS.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "DLS.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "DLS.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBoMcoZAInKHTvsmg-4jakJxzP_YnRfQ6k</string>
<key>GCM_SENDER_ID</key>
<string>751114036897</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.kovakyazilim.labapp</string>
<key>PROJECT_ID</key>
<string>dlslabapp</string>
<key>STORAGE_BUCKET</key>
<string>dlslabapp.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:751114036897:ios:aae969759ebc15a199f2e0</string>
</dict>
</plist>
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DLS</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>DLS</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+25
View File
@@ -0,0 +1,25 @@
import 'package:pocketbase/pocketbase.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _kAuthKey = 'pb_auth';
class PocketBaseClient {
PocketBaseClient._({required this.pb});
static PocketBaseClient? _instance;
static PocketBaseClient get instance => _instance!;
final PocketBase pb;
static Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getString(_kAuthKey);
final store = AsyncAuthStore(
save: (String data) => prefs.setString(_kAuthKey, data),
initial: stored,
);
_instance = PocketBaseClient._(
pb: PocketBase('https://pocket.kovaksoft.com', authStore: store),
);
}
}
+100
View File
@@ -0,0 +1,100 @@
import 'package:pocketbase/pocketbase.dart';
import '../api/pocketbase_client.dart';
import '../../models/tenant.dart';
import '../../models/user_profile.dart';
class AuthRepository {
AuthRepository._();
static final instance = AuthRepository._();
PocketBase get _pb => PocketBaseClient.instance.pb;
Future<AuthResult> login(String email, String password) async {
await _pb.collection('users').authWithPassword(email, password);
return _buildAuthResult();
}
Future<void> logout() async {
_pb.authStore.clear();
}
Future<bool> isLoggedIn() async {
if (!_pb.authStore.isValid) return false;
try {
await _pb.collection('users').authRefresh();
return true;
} catch (_) {
_pb.authStore.clear();
return false;
}
}
Future<AuthResult> register({
required String email,
required String password,
String? firstName,
String? lastName,
}) async {
await _pb.collection('users').create(body: {
'email': email,
'password': password,
'passwordConfirm': password,
'emailVisibility': true,
if (firstName != null && firstName.isNotEmpty) 'first_name': firstName,
if (lastName != null && lastName.isNotEmpty) 'last_name': lastName,
});
return login(email, password);
}
Future<AuthResult> refreshSession() async {
try {
await _pb.collection('users').authRefresh();
} catch (_) {}
return _buildAuthResult();
}
Future<void> updateUserLanguage(String userId, String languageCode) async {
await _pb.collection('users').update(userId, body: {
'preferred_language': languageCode,
});
}
Future<void> updateTenant(
String id, {
String? companyName,
String? defaultCurrency,
}) async {
final body = <String, dynamic>{};
if (companyName != null) body['company_name'] = companyName;
if (defaultCurrency != null) body['default_currency'] = defaultCurrency;
if (body.isEmpty) return;
await _pb.collection('tenants').update(id, body: body);
}
Future<AuthResult> _buildAuthResult() async {
final record = _pb.authStore.record!;
final user = UserProfile.fromJson(record.toJson());
List<TenantMembership> tenants = [];
try {
tenants = await _fetchUserTenants(record.id);
} catch (_) {}
return AuthResult(user: user, tenants: tenants);
}
Future<List<TenantMembership>> _fetchUserTenants(String userId) async {
final result = await _pb.collection('tenant_members').getList(
filter: 'user_id = "$userId"',
expand: 'tenant_id',
perPage: 50,
);
return result.items
.map((r) => TenantMembership.fromJson(r.toJson()))
.toList();
}
}
class AuthResult {
const AuthResult({required this.user, required this.tenants});
final UserProfile user;
final List<TenantMembership> tenants;
}
+777
View File
@@ -0,0 +1,777 @@
// ignore_for_file: lines_longer_than_80_chars
class AppStrings {
const AppStrings({
required this.settings,
required this.userInfo,
required this.labInfo,
required this.clinicInfo,
required this.labName,
required this.clinicName,
required this.currency,
required this.status,
required this.active,
required this.role,
required this.connections,
required this.clinicConnections,
required this.clinicConnectionsSub,
required this.labConnections,
required this.labConnectionsSub,
required this.otherMemberships,
required this.management,
required this.team,
required this.teamSub,
required this.discounts,
required this.discountsSub,
required this.reports,
required this.reportsSub,
required this.aiAssistant,
required this.aiAssistantSub,
required this.signOut,
required this.signOutTitle,
required this.signOutConfirm,
required this.cancel,
required this.save,
required this.edit,
required this.editLabInfo,
required this.editClinicInfo,
required this.labNameHint,
required this.clinicNameHint,
required this.preferences,
required this.appLanguage,
required this.languageSelection,
required this.currencySelection,
required this.languageTurkish,
required this.languageEnglish,
required this.languageRussian,
required this.languageArabic,
required this.languageGerman,
required this.type,
required this.roleOwner,
required this.roleAdmin,
required this.roleTechnician,
required this.roleDelivery,
required this.roleFinance,
required this.roleDoctor,
required this.roleMember,
required this.tenantKindClinic,
required this.tenantKindLab,
required this.signInWelcome,
required this.signInSubtitle,
required this.emailAddress,
required this.password,
required this.emailRequired,
required this.passwordRequired,
required this.signIn,
required this.noAccount,
required this.signUp,
required this.signInHeadline,
required this.signInTagline,
required this.footerCopyright,
required this.signUpTitle,
required this.signUpSubtitle,
required this.firstName,
required this.lastName,
required this.firstNameHint,
required this.lastNameHint,
required this.emailHint,
required this.passwordHint,
required this.confirmPassword,
required this.confirmPasswordHint,
required this.passwordMismatch,
required this.alreadyHaveAccount,
required this.finance,
required this.pendingReceivable,
required this.collected,
required this.pending,
required this.sortNewest,
required this.sortAmountDesc,
required this.sortAmountAsc,
required this.noPendingEntries,
required this.noPaidEntries,
required this.sort,
required this.retry,
required this.errorPrefix,
required this.laboratoryCategory,
required this.clinicCategory,
required this.jobsTitle,
required this.dashboardTitle,
required this.productsTitle,
required this.patientsTitle,
required this.close,
required this.confirm,
required this.currencyTRY,
required this.currencyUSD,
required this.currencyEUR,
required this.currencyGBP,
required this.currencyAED,
});
// ── General ───────────────────────────────────────────────────────────────
final String cancel;
final String save;
final String edit;
final String preferences;
final String close;
final String confirm;
final String retry;
final String errorPrefix;
final String sort;
// ── Settings ──────────────────────────────────────────────────────────────
final String settings;
final String userInfo;
final String labInfo;
final String clinicInfo;
final String labName;
final String clinicName;
final String currency;
final String status;
final String active;
final String role;
final String connections;
final String clinicConnections;
final String clinicConnectionsSub;
final String labConnections;
final String labConnectionsSub;
final String otherMemberships;
final String management;
final String team;
final String teamSub;
final String discounts;
final String discountsSub;
final String reports;
final String reportsSub;
final String aiAssistant;
final String aiAssistantSub;
final String signOut;
final String signOutTitle;
final String signOutConfirm;
final String editLabInfo;
final String editClinicInfo;
final String labNameHint;
final String clinicNameHint;
final String appLanguage;
final String languageSelection;
final String currencySelection;
final String languageTurkish;
final String languageEnglish;
final String languageRussian;
final String languageArabic;
final String languageGerman;
final String type;
// ── Roles & tenant ────────────────────────────────────────────────────────
final String roleOwner;
final String roleAdmin;
final String roleTechnician;
final String roleDelivery;
final String roleFinance;
final String roleDoctor;
final String roleMember;
final String tenantKindClinic;
final String tenantKindLab;
// ── Auth ──────────────────────────────────────────────────────────────────
final String signInWelcome;
final String signInSubtitle;
final String emailAddress;
final String password;
final String emailRequired;
final String passwordRequired;
final String signIn;
final String noAccount;
final String signUp;
final String signInHeadline;
final String signInTagline;
final String footerCopyright;
final String signUpTitle;
final String signUpSubtitle;
final String firstName;
final String lastName;
final String firstNameHint;
final String lastNameHint;
final String emailHint;
final String passwordHint;
final String confirmPassword;
final String confirmPasswordHint;
final String passwordMismatch;
final String alreadyHaveAccount;
// ── Finance ───────────────────────────────────────────────────────────────
final String finance;
final String pendingReceivable;
final String collected;
final String pending;
final String sortNewest;
final String sortAmountDesc;
final String sortAmountAsc;
final String noPendingEntries;
final String noPaidEntries;
// ── Navigation / categories ───────────────────────────────────────────────
final String laboratoryCategory;
final String clinicCategory;
final String jobsTitle;
final String dashboardTitle;
final String productsTitle;
final String patientsTitle;
// ── Currencies ────────────────────────────────────────────────────────────
final String currencyTRY;
final String currencyUSD;
final String currencyEUR;
final String currencyGBP;
final String currencyAED;
// ── Helpers ───────────────────────────────────────────────────────────────
String tenantSelected(String name) {
if (this == ar) return '$name تم الاختيار.';
if (this == ru) return '$name выбрана.';
if (this == de) return '$name ausgewählt.';
if (this == en) return '$name selected.';
return '$name seçildi.';
}
static AppStrings of(String languageCode) => switch (languageCode) {
'en' => en,
'ru' => ru,
'ar' => ar,
'de' => de,
_ => tr,
};
// ── Turkish ───────────────────────────────────────────────────────────────
static const tr = AppStrings(
cancel: 'İptal',
save: 'Kaydet',
edit: 'Düzenle',
preferences: 'Tercihler',
close: 'Kapat',
confirm: 'Onayla',
retry: 'Tekrar Dene',
errorPrefix: 'Hata',
sort: 'Sıralama',
settings: 'Ayarlar',
userInfo: 'Kullanıcı Bilgileri',
labInfo: 'Laboratuvar Bilgileri',
clinicInfo: 'Klinik Bilgileri',
labName: 'Laboratuvar Adı',
clinicName: 'Klinik Adı',
currency: 'Para Birimi',
status: 'Durum',
active: 'Aktif',
role: 'Rol',
connections: 'Bağlantılar',
clinicConnections: 'Klinik Bağlantıları',
clinicConnectionsSub: 'Bağlı klinikler ve istekler',
labConnections: 'Laboratuvar Bağlantıları',
labConnectionsSub: 'Bağlı lablar ve talepler',
otherMemberships: 'Diğer Üyelikler',
management: 'Yönetim',
team: 'Ekip',
teamSub: 'Üyeler ve davetler',
discounts: 'İndirimler',
discountsSub: 'Klinik ve ürün bazlı özel indirimler',
reports: 'Raporlar',
reportsSub: 'İş geçmişi, finans ve analiz',
aiAssistant: 'AI Asistan',
aiAssistantSub: 'İşler ve finans hakkında soru sor',
signOut: 'Çıkış Yap',
signOutTitle: 'Çıkış Yap',
signOutConfirm: 'Hesabınızdan çıkış yapmak istiyor musunuz?',
editLabInfo: 'Laboratuvar Bilgilerini Düzenle',
editClinicInfo: 'Klinik Bilgilerini Düzenle',
labNameHint: 'Laboratuvar adını girin',
clinicNameHint: 'Klinik adını girin',
appLanguage: 'Uygulama Dili',
languageSelection: 'Dil Seçimi',
currencySelection: 'Para Birimi Seçimi',
languageTurkish: 'Türkçe',
languageEnglish: 'English',
languageRussian: 'Русский',
languageArabic: 'العربية',
languageGerman: 'Deutsch',
type: 'Tür',
roleOwner: 'Sahibi',
roleAdmin: 'Yönetici',
roleTechnician: 'Teknisyen',
roleDelivery: 'Teslimat Elemanı',
roleFinance: 'Finans Elemanı',
roleDoctor: 'Hekim',
roleMember: 'Üye',
tenantKindClinic: 'Klinik',
tenantKindLab: 'Laboratuvar',
signInWelcome: 'Tekrar hoş geldiniz',
signInSubtitle: 'Hesabınıza giriş yapın',
emailAddress: 'E-posta adresi',
password: 'Şifre',
emailRequired: 'E-posta gereklidir',
passwordRequired: 'Şifre gereklidir',
signIn: 'Giriş Yap',
noAccount: 'Hesabın yok mu?',
signUp: 'Kayıt Ol',
signInHeadline: 'Dental Lab\nYönetimini\nBasitleştirin.',
signInTagline: 'İş takibi, klinik bağlantısı ve\ngerçek zamanlı durum izleme.',
footerCopyright: '© 2025 Dental Lab Sistemi · KovakSoft',
signUpTitle: 'Hesap Oluştur',
signUpSubtitle: 'DLS\'e kaydolun',
firstName: 'Ad',
lastName: 'Soyad',
firstNameHint: 'Adınızı girin',
lastNameHint: 'Soyadınızı girin',
emailHint: 'E-posta adresinizi girin',
passwordHint: 'Şifrenizi girin',
confirmPassword: 'Şifre Tekrar',
confirmPasswordHint: 'Şifrenizi tekrar girin',
passwordMismatch: 'Şifreler eşleşmiyor',
alreadyHaveAccount: 'Zaten hesabın var mı?',
finance: 'Finans',
pendingReceivable: 'Bekleyen Alacak',
collected: 'Tahsil Edilen',
pending: 'Bekleyen',
sortNewest: 'Yeniden Eskiye',
sortAmountDesc: 'Tutara Göre (Büyükten Küçüğe)',
sortAmountAsc: 'Tutara Göre (Küçükten Büyüğe)',
noPendingEntries: 'Bekleyen alacak yok',
noPaidEntries: 'Tahsil edilen kayıt yok',
laboratoryCategory: 'LABORATUVAR',
clinicCategory: 'KLİNİK',
jobsTitle: 'İşler',
dashboardTitle: 'Özet',
productsTitle: 'Ürünler',
patientsTitle: 'Hastalar',
currencyTRY: 'Türk Lirası (₺)',
currencyUSD: 'US Dollar (\$)',
currencyEUR: 'Euro (€)',
currencyGBP: 'British Pound (£)',
currencyAED: 'UAE Dirham (د.إ)',
);
// ── English ───────────────────────────────────────────────────────────────
static const en = AppStrings(
cancel: 'Cancel',
save: 'Save',
edit: 'Edit',
preferences: 'Preferences',
close: 'Close',
confirm: 'Confirm',
retry: 'Retry',
errorPrefix: 'Error',
sort: 'Sort',
settings: 'Settings',
userInfo: 'User Information',
labInfo: 'Laboratory Information',
clinicInfo: 'Clinic Information',
labName: 'Laboratory Name',
clinicName: 'Clinic Name',
currency: 'Currency',
status: 'Status',
active: 'Active',
role: 'Role',
connections: 'Connections',
clinicConnections: 'Clinic Connections',
clinicConnectionsSub: 'Connected clinics and requests',
labConnections: 'Laboratory Connections',
labConnectionsSub: 'Connected labs and requests',
otherMemberships: 'Other Memberships',
management: 'Management',
team: 'Team',
teamSub: 'Members and invitations',
discounts: 'Discounts',
discountsSub: 'Custom discounts by clinic and product',
reports: 'Reports',
reportsSub: 'Job history, finance and analytics',
aiAssistant: 'AI Assistant',
aiAssistantSub: 'Ask about jobs and finance',
signOut: 'Sign Out',
signOutTitle: 'Sign Out',
signOutConfirm: 'Are you sure you want to sign out?',
editLabInfo: 'Edit Laboratory Info',
editClinicInfo: 'Edit Clinic Info',
labNameHint: 'Enter laboratory name',
clinicNameHint: 'Enter clinic name',
appLanguage: 'App Language',
languageSelection: 'Language Selection',
currencySelection: 'Currency Selection',
languageTurkish: 'Türkçe',
languageEnglish: 'English',
languageRussian: 'Русский',
languageArabic: 'العربية',
languageGerman: 'Deutsch',
type: 'Type',
roleOwner: 'Owner',
roleAdmin: 'Admin',
roleTechnician: 'Technician',
roleDelivery: 'Delivery Staff',
roleFinance: 'Finance Staff',
roleDoctor: 'Doctor',
roleMember: 'Member',
tenantKindClinic: 'Clinic',
tenantKindLab: 'Laboratory',
signInWelcome: 'Welcome back',
signInSubtitle: 'Sign in to your account',
emailAddress: 'Email address',
password: 'Password',
emailRequired: 'Email is required',
passwordRequired: 'Password is required',
signIn: 'Sign In',
noAccount: "Don't have an account?",
signUp: 'Sign Up',
signInHeadline: 'Simplify Dental\nLab Management.',
signInTagline: 'Job tracking, clinic connections, and\nreal-time status monitoring.',
footerCopyright: '© 2025 Dental Lab System · KovakSoft',
signUpTitle: 'Create Account',
signUpSubtitle: 'Sign up for DLS',
firstName: 'First Name',
lastName: 'Last Name',
firstNameHint: 'Enter your first name',
lastNameHint: 'Enter your last name',
emailHint: 'Enter your email address',
passwordHint: 'Enter your password',
confirmPassword: 'Confirm Password',
confirmPasswordHint: 'Re-enter your password',
passwordMismatch: 'Passwords do not match',
alreadyHaveAccount: 'Already have an account?',
finance: 'Finance',
pendingReceivable: 'Outstanding Balance',
collected: 'Collected',
pending: 'Pending',
sortNewest: 'Newest to Oldest',
sortAmountDesc: 'By Amount (High to Low)',
sortAmountAsc: 'By Amount (Low to High)',
noPendingEntries: 'No outstanding balance',
noPaidEntries: 'No collected records',
laboratoryCategory: 'LABORATORY',
clinicCategory: 'CLINIC',
jobsTitle: 'Jobs',
dashboardTitle: 'Overview',
productsTitle: 'Products',
patientsTitle: 'Patients',
currencyTRY: 'Turkish Lira (₺)',
currencyUSD: 'US Dollar (\$)',
currencyEUR: 'Euro (€)',
currencyGBP: 'British Pound (£)',
currencyAED: 'UAE Dirham (د.إ)',
);
// ── Russian ───────────────────────────────────────────────────────────────
static const ru = AppStrings(
cancel: 'Отмена',
save: 'Сохранить',
edit: 'Изменить',
preferences: 'Предпочтения',
close: 'Закрыть',
confirm: 'Подтвердить',
retry: 'Повторить',
errorPrefix: 'Ошибка',
sort: 'Сортировка',
settings: 'Настройки',
userInfo: 'Информация о пользователе',
labInfo: 'Информация о лаборатории',
clinicInfo: 'Информация о клинике',
labName: 'Название лаборатории',
clinicName: 'Название клиники',
currency: 'Валюта',
status: 'Статус',
active: 'Активный',
role: 'Роль',
connections: 'Подключения',
clinicConnections: 'Подключения к клиникам',
clinicConnectionsSub: 'Подключённые клиники и запросы',
labConnections: 'Подключения к лабораториям',
labConnectionsSub: 'Подключённые лаборатории и запросы',
otherMemberships: 'Другие членства',
management: 'Управление',
team: 'Команда',
teamSub: 'Участники и приглашения',
discounts: 'Скидки',
discountsSub: 'Специальные скидки по клинике и продукту',
reports: 'Отчёты',
reportsSub: 'История заказов, финансы и аналитика',
aiAssistant: 'ИИ-ассистент',
aiAssistantSub: 'Задавайте вопросы о заказах и финансах',
signOut: 'Выйти',
signOutTitle: 'Выйти',
signOutConfirm: 'Вы уверены, что хотите выйти из аккаунта?',
editLabInfo: 'Редактировать информацию о лаборатории',
editClinicInfo: 'Редактировать информацию о клинике',
labNameHint: 'Введите название лаборатории',
clinicNameHint: 'Введите название клиники',
appLanguage: 'Язык приложения',
languageSelection: 'Выбор языка',
currencySelection: 'Выбор валюты',
languageTurkish: 'Türkçe',
languageEnglish: 'English',
languageRussian: 'Русский',
languageArabic: 'العربية',
languageGerman: 'Deutsch',
type: 'Тип',
roleOwner: 'Владелец',
roleAdmin: 'Администратор',
roleTechnician: 'Техник',
roleDelivery: 'Сотрудник доставки',
roleFinance: 'Финансовый сотрудник',
roleDoctor: 'Врач',
roleMember: 'Участник',
tenantKindClinic: 'Клиника',
tenantKindLab: 'Лаборатория',
signInWelcome: 'Добро пожаловать',
signInSubtitle: 'Войдите в свой аккаунт',
emailAddress: 'Адрес эл. почты',
password: 'Пароль',
emailRequired: 'Эл. почта обязательна',
passwordRequired: 'Пароль обязателен',
signIn: 'Войти',
noAccount: 'Нет аккаунта?',
signUp: 'Зарегистрироваться',
signInHeadline: 'Упростите управление\nзубной лабораторией.',
signInTagline: 'Отслеживание заказов, связь с клиниками\nи мониторинг в реальном времени.',
footerCopyright: '© 2025 Dental Lab System · KovakSoft',
signUpTitle: 'Создать аккаунт',
signUpSubtitle: 'Зарегистрироваться в DLS',
firstName: 'Имя',
lastName: 'Фамилия',
firstNameHint: 'Введите ваше имя',
lastNameHint: 'Введите вашу фамилию',
emailHint: 'Введите адрес эл. почты',
passwordHint: 'Введите ваш пароль',
confirmPassword: 'Подтверждение пароля',
confirmPasswordHint: 'Повторите ваш пароль',
passwordMismatch: 'Пароли не совпадают',
alreadyHaveAccount: 'Уже есть аккаунт?',
finance: 'Финансы',
pendingReceivable: 'Задолженность',
collected: 'Получено',
pending: 'Ожидающие',
sortNewest: 'Сначала новые',
sortAmountDesc: 'По сумме (убывание)',
sortAmountAsc: 'По сумме (возрастание)',
noPendingEntries: 'Нет задолженностей',
noPaidEntries: 'Нет оплаченных записей',
laboratoryCategory: 'ЛАБОРАТОРИЯ',
clinicCategory: 'КЛИНИКА',
jobsTitle: 'Заказы',
dashboardTitle: 'Обзор',
productsTitle: 'Продукты',
patientsTitle: 'Пациенты',
currencyTRY: 'Турецкая лира (₺)',
currencyUSD: 'Доллар США (\$)',
currencyEUR: 'Евро (€)',
currencyGBP: 'Британский фунт (£)',
currencyAED: 'Дирхам ОАЭ (د.إ)',
);
// ── Arabic ────────────────────────────────────────────────────────────────
static const ar = AppStrings(
cancel: 'إلغاء',
save: 'حفظ',
edit: 'تعديل',
preferences: 'التفضيلات',
close: 'إغلاق',
confirm: 'تأكيد',
retry: 'إعادة المحاولة',
errorPrefix: 'خطأ',
sort: 'ترتيب',
settings: 'الإعدادات',
userInfo: 'معلومات المستخدم',
labInfo: 'معلومات المختبر',
clinicInfo: 'معلومات العيادة',
labName: 'اسم المختبر',
clinicName: 'اسم العيادة',
currency: 'العملة',
status: 'الحالة',
active: 'نشط',
role: 'الدور',
connections: 'الاتصالات',
clinicConnections: 'اتصالات العيادة',
clinicConnectionsSub: 'العيادات المتصلة والطلبات',
labConnections: 'اتصالات المختبر',
labConnectionsSub: 'المختبرات المتصلة والطلبات',
otherMemberships: 'عضويات أخرى',
management: 'الإدارة',
team: 'الفريق',
teamSub: 'الأعضاء والدعوات',
discounts: 'الخصومات',
discountsSub: 'خصومات مخصصة حسب العيادة والمنتج',
reports: 'التقارير',
reportsSub: 'تاريخ الأعمال والمالية والتحليلات',
aiAssistant: 'مساعد الذكاء الاصطناعي',
aiAssistantSub: 'اسأل عن الأعمال والمالية',
signOut: 'تسجيل الخروج',
signOutTitle: 'تسجيل الخروج',
signOutConfirm: 'هل أنت متأكد من تسجيل الخروج؟',
editLabInfo: 'تعديل معلومات المختبر',
editClinicInfo: 'تعديل معلومات العيادة',
labNameHint: 'أدخل اسم المختبر',
clinicNameHint: 'أدخل اسم العيادة',
appLanguage: 'لغة التطبيق',
languageSelection: 'اختيار اللغة',
currencySelection: 'اختيار العملة',
languageTurkish: 'Türkçe',
languageEnglish: 'English',
languageRussian: 'Русский',
languageArabic: 'العربية',
languageGerman: 'Deutsch',
type: 'النوع',
roleOwner: 'المالك',
roleAdmin: 'المسؤول',
roleTechnician: 'فني',
roleDelivery: 'موظف توصيل',
roleFinance: 'موظف مالي',
roleDoctor: 'طبيب',
roleMember: 'عضو',
tenantKindClinic: 'عيادة',
tenantKindLab: 'مختبر',
signInWelcome: 'مرحباً بعودتك',
signInSubtitle: 'سجّل دخولك إلى حسابك',
emailAddress: 'البريد الإلكتروني',
password: 'كلمة المرور',
emailRequired: 'البريد الإلكتروني مطلوب',
passwordRequired: 'كلمة المرور مطلوبة',
signIn: 'تسجيل الدخول',
noAccount: 'ليس لديك حساب؟',
signUp: 'إنشاء حساب',
signInHeadline: 'بسّط إدارة\nمختبر الأسنان.',
signInTagline: 'تتبع الأعمال والتواصل مع العيادات\nومراقبة الحالة في الوقت الفعلي.',
footerCopyright: '© 2025 Dental Lab System · KovakSoft',
signUpTitle: 'إنشاء حساب',
signUpSubtitle: 'سجّل في DLS',
firstName: 'الاسم الأول',
lastName: 'اسم العائلة',
firstNameHint: 'أدخل اسمك الأول',
lastNameHint: 'أدخل اسم عائلتك',
emailHint: 'أدخل بريدك الإلكتروني',
passwordHint: 'أدخل كلمة مرورك',
confirmPassword: 'تأكيد كلمة المرور',
confirmPasswordHint: 'أعد إدخال كلمة مرورك',
passwordMismatch: 'كلمتا المرور غير متطابقتين',
alreadyHaveAccount: 'لديك حساب بالفعل؟',
finance: 'المالية',
pendingReceivable: 'المستحقات',
collected: 'المحصّل',
pending: 'معلّق',
sortNewest: 'الأحدث أولاً',
sortAmountDesc: 'حسب المبلغ (تنازلي)',
sortAmountAsc: 'حسب المبلغ (تصاعدي)',
noPendingEntries: 'لا توجد مستحقات',
noPaidEntries: 'لا توجد سجلات محصّلة',
laboratoryCategory: 'المختبر',
clinicCategory: 'العيادة',
jobsTitle: 'الأعمال',
dashboardTitle: 'نظرة عامة',
productsTitle: 'المنتجات',
patientsTitle: 'المرضى',
currencyTRY: 'ليرة تركية (₺)',
currencyUSD: 'دولار أمريكي (\$)',
currencyEUR: 'يورو (€)',
currencyGBP: 'جنيه إسترليني (£)',
currencyAED: 'درهم إماراتي (د.إ)',
);
// ── German ────────────────────────────────────────────────────────────────
static const de = AppStrings(
cancel: 'Abbrechen',
save: 'Speichern',
edit: 'Bearbeiten',
preferences: 'Einstellungen',
close: 'Schließen',
confirm: 'Bestätigen',
retry: 'Wiederholen',
errorPrefix: 'Fehler',
sort: 'Sortieren',
settings: 'Einstellungen',
userInfo: 'Benutzerinformationen',
labInfo: 'Laborinformationen',
clinicInfo: 'Klinikinformationen',
labName: 'Laborname',
clinicName: 'Klinikname',
currency: 'Währung',
status: 'Status',
active: 'Aktiv',
role: 'Rolle',
connections: 'Verbindungen',
clinicConnections: 'Klinikverbindungen',
clinicConnectionsSub: 'Verbundene Kliniken und Anfragen',
labConnections: 'Laborverbindungen',
labConnectionsSub: 'Verbundene Labore und Anfragen',
otherMemberships: 'Andere Mitgliedschaften',
management: 'Verwaltung',
team: 'Team',
teamSub: 'Mitglieder und Einladungen',
discounts: 'Rabatte',
discountsSub: 'Individuelle Rabatte nach Klinik und Produkt',
reports: 'Berichte',
reportsSub: 'Auftragsverlauf, Finanzen und Analysen',
aiAssistant: 'KI-Assistent',
aiAssistantSub: 'Fragen zu Aufträgen und Finanzen stellen',
signOut: 'Abmelden',
signOutTitle: 'Abmelden',
signOutConfirm: 'Sind Sie sicher, dass Sie sich abmelden möchten?',
editLabInfo: 'Laborinformationen bearbeiten',
editClinicInfo: 'Klinikinformationen bearbeiten',
labNameHint: 'Laborname eingeben',
clinicNameHint: 'Klinikname eingeben',
appLanguage: 'App-Sprache',
languageSelection: 'Sprachauswahl',
currencySelection: 'Währungsauswahl',
languageTurkish: 'Türkçe',
languageEnglish: 'English',
languageRussian: 'Русский',
languageArabic: 'العربية',
languageGerman: 'Deutsch',
type: 'Typ',
roleOwner: 'Inhaber',
roleAdmin: 'Administrator',
roleTechnician: 'Techniker',
roleDelivery: 'Liefermitarbeiter',
roleFinance: 'Finanzmitarbeiter',
roleDoctor: 'Arzt',
roleMember: 'Mitglied',
tenantKindClinic: 'Klinik',
tenantKindLab: 'Labor',
signInWelcome: 'Willkommen zurück',
signInSubtitle: 'Melden Sie sich in Ihrem Konto an',
emailAddress: 'E-Mail-Adresse',
password: 'Passwort',
emailRequired: 'E-Mail ist erforderlich',
passwordRequired: 'Passwort ist erforderlich',
signIn: 'Anmelden',
noAccount: 'Kein Konto?',
signUp: 'Registrieren',
signInHeadline: 'Dental-Labor-Verwaltung\nvereinfachen.',
signInTagline: 'Auftragsverfolgung, Klinikverbindungen\nund Echtzeitüberwachung.',
footerCopyright: '© 2025 Dental Lab System · KovakSoft',
signUpTitle: 'Konto erstellen',
signUpSubtitle: 'Bei DLS registrieren',
firstName: 'Vorname',
lastName: 'Nachname',
firstNameHint: 'Vornamen eingeben',
lastNameHint: 'Nachnamen eingeben',
emailHint: 'E-Mail-Adresse eingeben',
passwordHint: 'Passwort eingeben',
confirmPassword: 'Passwort bestätigen',
confirmPasswordHint: 'Passwort erneut eingeben',
passwordMismatch: 'Passwörter stimmen nicht überein',
alreadyHaveAccount: 'Haben Sie bereits ein Konto?',
finance: 'Finanzen',
pendingReceivable: 'Ausstehende Forderungen',
collected: 'Eingezogen',
pending: 'Ausstehend',
sortNewest: 'Neueste zuerst',
sortAmountDesc: 'Nach Betrag (absteigend)',
sortAmountAsc: 'Nach Betrag (aufsteigend)',
noPendingEntries: 'Keine ausstehenden Forderungen',
noPaidEntries: 'Keine eingezogenen Einträge',
laboratoryCategory: 'LABOR',
clinicCategory: 'KLINIK',
jobsTitle: 'Aufträge',
dashboardTitle: 'Übersicht',
productsTitle: 'Produkte',
patientsTitle: 'Patienten',
currencyTRY: 'Türkische Lira (₺)',
currencyUSD: 'US-Dollar (\$)',
currencyEUR: 'Euro (€)',
currencyGBP: 'Britisches Pfund (£)',
currencyAED: 'VAE-Dirham (د.إ)',
);
}
+195
View File
@@ -0,0 +1,195 @@
import 'package:flutter/widgets.dart' show Locale;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pocketbase/pocketbase.dart';
import '../auth/auth_repository.dart';
import '../services/notification_service.dart';
import '../../models/tenant.dart';
import '../../models/user_profile.dart';
import 'locale_provider.dart';
class AuthState {
const AuthState({
this.profile,
this.activeTenant,
this.memberships = const [],
this.isLoading = true,
this.error,
});
final UserProfile? profile;
final TenantMembership? activeTenant;
final List<TenantMembership> memberships;
final bool isLoading;
final String? error;
bool get isAuthenticated => profile != null;
AuthState copyWith({
UserProfile? profile,
TenantMembership? activeTenant,
List<TenantMembership>? memberships,
bool? isLoading,
String? error,
bool clearError = false,
}) =>
AuthState(
profile: profile ?? this.profile,
activeTenant: activeTenant ?? this.activeTenant,
memberships: memberships ?? this.memberships,
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
);
}
class AuthNotifier extends StateNotifier<AuthState> {
AuthNotifier({this.onLocaleLoaded}) : super(const AuthState()) {
_init();
}
final void Function(String languageCode)? onLocaleLoaded;
final _repo = AuthRepository.instance;
Future<void> _init() async {
final loggedIn = await _repo.isLoggedIn();
if (!loggedIn) {
state = const AuthState(isLoading: false);
return;
}
try {
final result = await _repo.refreshSession();
state = AuthState(
profile: result.user,
memberships: result.tenants,
activeTenant:
result.tenants.isEmpty ? null : result.tenants.first,
isLoading: false,
);
final isLab = result.tenants.isNotEmpty && result.tenants.first.tenant.isLab;
NotificationService.loginUser(result.user.id, isLab: isLab);
_applyLocale(result.user.preferredLanguage);
} catch (_) {
state = const AuthState(isLoading: false);
}
}
void _applyLocale(String? code) {
if (code != null && code.isNotEmpty) {
onLocaleLoaded?.call(code);
}
}
Future<void> signIn(String email, String password) async {
state = state.copyWith(isLoading: true, clearError: true);
try {
final result = await _repo.login(email, password);
state = AuthState(
profile: result.user,
memberships: result.tenants,
activeTenant:
result.tenants.isEmpty ? null : result.tenants.first,
isLoading: false,
);
final isLab = result.tenants.isNotEmpty && result.tenants.first.tenant.isLab;
NotificationService.loginUser(result.user.id, isLab: isLab);
_applyLocale(result.user.preferredLanguage);
} catch (e) {
state = state.copyWith(isLoading: false, error: _parseError(e));
}
}
Future<void> register({
required String email,
required String password,
String? firstName,
String? lastName,
}) async {
state = state.copyWith(isLoading: true, clearError: true);
try {
final result = await _repo.register(
email: email,
password: password,
firstName: firstName,
lastName: lastName,
);
state = AuthState(
profile: result.user,
memberships: result.tenants,
activeTenant:
result.tenants.isEmpty ? null : result.tenants.first,
isLoading: false,
);
} catch (e) {
state = state.copyWith(isLoading: false, error: _parseError(e));
rethrow;
}
}
Future<void> signOut() async {
await _repo.logout();
await NotificationService.logoutUser();
state = const AuthState(isLoading: false);
}
void setActiveTenant(TenantMembership membership) {
state = state.copyWith(activeTenant: membership);
}
Future<void> refresh() async {
try {
final result = await _repo.refreshSession();
final currentId = state.activeTenant?.tenant.id;
final newActive = currentId != null
? result.tenants.firstWhere(
(m) => m.tenant.id == currentId,
orElse: () => result.tenants.isNotEmpty
? result.tenants.first
: state.activeTenant!,
)
: (result.tenants.isNotEmpty ? result.tenants.first : null);
state = state.copyWith(
profile: result.user,
memberships: result.tenants,
activeTenant: newActive,
);
} catch (_) {}
}
Future<void> updateLanguage(String languageCode) async {
final userId = state.profile?.id;
if (userId == null) return;
await _repo.updateUserLanguage(userId, languageCode);
}
Future<void> updateTenantInfo({
required String tenantId,
required String companyName,
String? defaultCurrency,
}) async {
await _repo.updateTenant(
tenantId,
companyName: companyName,
defaultCurrency: defaultCurrency,
);
await refresh();
}
String _parseError(Object e) {
if (e is ClientException) {
final code = e.statusCode;
if (code == 400 || code == 401 || code == 403) {
return 'E-posta veya şifre hatalı.';
}
final msg = e.response['message'] as String? ?? '';
if (msg.isNotEmpty) return msg;
}
return 'Bağlantı hatası. Lütfen tekrar deneyin.';
}
}
final authProvider =
StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(
onLocaleLoaded: (code) =>
ref.read(localeProvider.notifier).setLocale(Locale(code)),
);
});
+39
View File
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../l10n/app_strings.dart';
const _kLocaleKey = 'app_locale';
class LocaleNotifier extends StateNotifier<Locale> {
LocaleNotifier(Locale initial) : super(initial);
Future<void> setLocale(Locale locale) async {
state = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kLocaleKey, locale.languageCode);
}
static Future<Locale> load() async {
final prefs = await SharedPreferences.getInstance();
final code = prefs.getString(_kLocaleKey) ?? 'tr';
return Locale(code);
}
}
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>(
(ref) => LocaleNotifier(const Locale('tr')),
);
final stringsProvider = Provider<AppStrings>((ref) {
final locale = ref.watch(localeProvider);
return AppStrings.of(locale.languageCode);
});
const supportedLocales = [
Locale('tr'),
Locale('en'),
Locale('ru'),
Locale('ar'),
Locale('de'),
];
+496
View File
@@ -0,0 +1,496 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../theme/app_theme.dart';
import '../widgets/tooth_logo.dart';
import '../providers/auth_provider.dart';
import '../../models/tenant.dart';
import '../../features/auth/sign_in_screen.dart';
import '../../features/auth/sign_up_screen.dart';
import '../../features/auth/onboarding_screen.dart';
import '../../features/clinic/dashboard/clinic_dashboard_screen.dart';
import '../../features/clinic/jobs/clinic_jobs_screen.dart';
import '../../features/clinic/jobs/clinic_job_detail_screen.dart';
import '../../features/clinic/jobs/new_job_screen.dart';
import '../../features/clinic/patients/clinic_patients_screen.dart';
import '../../features/clinic/patients/clinic_patient_detail_screen.dart';
import '../../features/clinic/connections/clinic_connections_screen.dart';
import '../../features/clinic/finance/clinic_finance_screen.dart';
import '../../features/clinic/settings/clinic_settings_screen.dart';
import '../../features/lab/dashboard/lab_dashboard_screen.dart';
import '../../features/lab/jobs/lab_jobs_inbound_screen.dart';
import '../../features/lab/jobs/lab_all_jobs_screen.dart';
import '../../features/lab/jobs/lab_job_detail_screen.dart';
import '../../features/lab/products/lab_products_screen.dart';
import '../../features/lab/connections/lab_connections_screen.dart';
import '../../features/lab/finance/lab_finance_screen.dart';
import '../../features/lab/settings/lab_settings_screen.dart';
import '../../features/shared/reports_screen.dart';
import '../../features/shared/ai_chat_screen.dart';
import '../../features/lab/discounts/discounts_screen.dart';
import '../../features/lab/connections/connection_detail_screen.dart';
import '../../models/connection.dart';
// Auth routes
const routeSignIn = '/sign-in';
const routeSignUp = '/sign-up';
const routeOnboarding = '/onboarding';
// Clinic routes
const routeClinicDashboard = '/clinic/dashboard';
const routeClinicJobs = '/clinic/jobs';
const routeClinicJobDetail = '/clinic/jobs/:jobId';
const routeClinicJobNew = '/clinic/jobs/new';
const routeClinicPatients = '/clinic/patients';
const routeClinicPatientDetail = '/clinic/patients/:patientId';
const routeClinicConnections = '/clinic/connections';
const routeClinicFinance = '/clinic/finance';
const routeClinicSettings = '/clinic/settings';
const routeClinicReports = '/clinic/reports';
const routeClinicAi = '/clinic/ai';
// Lab routes
const routeLabDashboard = '/lab/dashboard';
const routeLabJobsInbound = '/lab/jobs/inbound';
const routeLabJobsAll = '/lab/jobs';
const routeLabJobDetail = '/lab/jobs/:jobId';
const routeLabProducts = '/lab/products';
const routeLabConnections = '/lab/connections';
const routeLabFinance = '/lab/finance';
const routeLabSettings = '/lab/settings';
const routeLabReports = '/lab/reports';
const routeLabAi = '/lab/ai';
const routeLabDiscounts = '/lab/discounts';
List<RouteBase> buildRoutes() => [
GoRoute(path: routeSignIn, builder: (_, __) => const SignInScreen()),
GoRoute(path: routeSignUp, builder: (_, __) => const SignUpScreen()),
GoRoute(path: routeOnboarding, builder: (_, __) => const OnboardingScreen()),
// ── Clinic shell ──────────────────────────────────────────────────────
ShellRoute(
builder: (context, state, child) => _ClinicShell(child: child),
routes: [
GoRoute(path: routeClinicDashboard, builder: (_, __) => const ClinicDashboardScreen()),
GoRoute(
path: routeClinicJobs,
builder: (_, __) => const ClinicJobsScreen(),
routes: [
GoRoute(path: 'new', builder: (_, __) => const NewJobScreen()),
GoRoute(
path: ':jobId',
builder: (_, s) => ClinicJobDetailScreen(jobId: s.pathParameters['jobId']!),
),
],
),
GoRoute(
path: routeClinicPatients,
builder: (_, __) => const ClinicPatientsScreen(),
routes: [
GoRoute(
path: ':patientId',
builder: (_, s) => ClinicPatientDetailScreen(patientId: s.pathParameters['patientId']!),
),
],
),
GoRoute(path: routeClinicConnections, builder: (_, __) => const ClinicConnectionsScreen()),
GoRoute(path: routeClinicFinance, builder: (_, __) => const ClinicFinanceScreen()),
GoRoute(path: routeClinicSettings, builder: (_, __) => const ClinicSettingsScreen()),
GoRoute(path: routeClinicReports, builder: (_, __) => const ReportsScreen()),
GoRoute(path: routeClinicAi, builder: (_, __) => const AiChatScreen()),
],
),
// ── Lab shell ─────────────────────────────────────────────────────────
ShellRoute(
builder: (context, state, child) => _LabShell(child: child),
routes: [
GoRoute(path: routeLabDashboard, builder: (_, __) => const LabDashboardScreen()),
GoRoute(path: routeLabJobsInbound, builder: (_, __) => const LabJobsInboundScreen()),
GoRoute(
path: routeLabJobsAll,
builder: (_, __) => const LabAllJobsScreen(),
routes: [
GoRoute(
path: ':jobId',
builder: (_, s) => LabJobDetailScreen(jobId: s.pathParameters['jobId']!),
),
],
),
GoRoute(path: routeLabProducts, builder: (_, __) => const LabProductsScreen()),
GoRoute(
path: routeLabConnections,
builder: (_, __) => const LabConnectionsScreen(),
routes: [
GoRoute(
path: ':connectionId/detail',
builder: (_, s) {
final extra = s.extra as Map<String, dynamic>?;
final connection = extra?['connection'] as Connection?;
final labTenantId = extra?['labTenantId'] as String? ?? '';
if (connection == null) {
return const Scaffold(
body: Center(child: Text('Bağlantı bulunamadı')),
);
}
return ConnectionDetailScreen(
connection: connection, labTenantId: labTenantId);
},
),
],
),
GoRoute(path: routeLabDiscounts, builder: (_, __) => const DiscountsScreen()),
GoRoute(path: routeLabFinance, builder: (_, __) => const LabFinanceScreen()),
GoRoute(path: routeLabSettings, builder: (_, __) => const LabSettingsScreen()),
GoRoute(path: routeLabReports, builder: (_, __) => const ReportsScreen()),
GoRoute(path: routeLabAi, builder: (_, __) => const AiChatScreen()),
],
),
];
// ── Nav item descriptor ───────────────────────────────────────────────────────
class _NavItem {
const _NavItem({
required this.route,
required this.icon,
required this.selectedIcon,
required this.label,
required this.visible,
});
final String route;
final Icon icon;
final Icon selectedIcon;
final String label;
final bool Function(TenantMembership?) visible;
}
// ── Clinic shell ──────────────────────────────────────────────────────────────
class _ClinicShell extends ConsumerStatefulWidget {
const _ClinicShell({required this.child});
final Widget child;
@override
ConsumerState<_ClinicShell> createState() => _ClinicShellState();
}
class _ClinicShellState extends ConsumerState<_ClinicShell> {
int _index = 0;
static final _allItems = [
_NavItem(route: routeClinicDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: 'Ana Sayfa', visible: (_) => true),
_NavItem(route: routeClinicJobs, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: 'İşler', visible: (m) => m?.showJobs ?? true),
_NavItem(route: routeClinicPatients, icon: const Icon(Icons.people_outline_rounded), selectedIcon: const Icon(Icons.people_rounded), label: 'Hastalar', visible: (m) => m?.showPatients ?? true),
_NavItem(route: routeClinicFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: 'Finans', visible: (m) => m?.showFinance ?? true),
_NavItem(route: routeClinicSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: 'Ayarlar', visible: (_) => true),
];
@override
Widget build(BuildContext context) {
final membership = ref.watch(authProvider).activeTenant;
final items = _allItems.where((it) => it.visible(membership)).toList();
final clampedIndex = _index.clamp(0, items.length - 1);
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
void onTap(int i) {
setState(() => _index = i);
context.go(items[i].route);
}
if (isDesktop) {
return Scaffold(
backgroundColor: AppColors.background,
body: Row(
children: [
_DesktopSidebar(destinations: items, selectedIndex: clampedIndex, onTap: onTap),
Expanded(child: widget.child),
],
),
);
}
return Scaffold(
body: widget.child,
bottomNavigationBar: NavigationBar(
selectedIndex: clampedIndex,
onDestinationSelected: onTap,
destinations: [
for (final it in items)
Semantics(
label: it.label,
button: true,
child: NavigationDestination(icon: it.icon, selectedIcon: it.selectedIcon, label: it.label),
),
],
),
);
}
}
// ── Lab shell ─────────────────────────────────────────────────────────────────
class _LabShell extends ConsumerStatefulWidget {
const _LabShell({required this.child});
final Widget child;
@override
ConsumerState<_LabShell> createState() => _LabShellState();
}
class _LabShellState extends ConsumerState<_LabShell> {
int _index = 0;
static final _allItems = [
_NavItem(route: routeLabDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: 'Ana Sayfa', visible: (_) => true),
_NavItem(route: routeLabJobsAll, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: 'İşler', visible: (m) => m?.showJobs ?? true),
_NavItem(route: routeLabProducts, icon: const Icon(Icons.inventory_2_outlined), selectedIcon: const Icon(Icons.inventory_2_rounded), label: 'Ürünler', visible: (m) => m?.showProducts ?? true),
_NavItem(route: routeLabFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: 'Finans', visible: (m) => m?.showFinance ?? true),
_NavItem(route: routeLabSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: 'Ayarlar', visible: (_) => true),
];
@override
Widget build(BuildContext context) {
final membership = ref.watch(authProvider).activeTenant;
final items = _allItems.where((it) => it.visible(membership)).toList();
final clampedIndex = _index.clamp(0, items.length - 1);
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
void onTap(int i) {
setState(() => _index = i);
context.go(items[i].route);
}
if (isDesktop) {
return Scaffold(
backgroundColor: AppColors.background,
body: Row(
children: [
_DesktopSidebar(destinations: items, selectedIndex: clampedIndex, onTap: onTap),
Expanded(child: widget.child),
],
),
);
}
return Scaffold(
body: widget.child,
bottomNavigationBar: NavigationBar(
selectedIndex: clampedIndex,
onDestinationSelected: onTap,
destinations: [
for (final it in items)
Semantics(
label: it.label,
button: true,
child: NavigationDestination(icon: it.icon, selectedIcon: it.selectedIcon, label: it.label),
),
],
),
);
}
}
// ── Desktop sidebar ───────────────────────────────────────────────────────────
class _DesktopSidebar extends StatefulWidget {
const _DesktopSidebar({
required this.destinations,
required this.selectedIndex,
required this.onTap,
});
final List<_NavItem> destinations;
final int selectedIndex;
final ValueChanged<int> onTap;
// Must match the toolbarHeight used in desktop SliverAppBar headers
static const double headerHeight = 64;
static const double _openWidth = 220;
static const double _closedWidth = 64;
@override
State<_DesktopSidebar> createState() => _DesktopSidebarState();
}
class _DesktopSidebarState extends State<_DesktopSidebar> {
bool _open = true;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeInOut,
width: _open ? _DesktopSidebar._openWidth : _DesktopSidebar._closedWidth,
decoration: const BoxDecoration(
color: AppColors.surface,
border: Border(right: BorderSide(color: AppColors.border)),
boxShadow: [BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(2, 0))],
),
child: ClipRect(
child: Column(
children: [
// Header
Container(
height: _DesktopSidebar.headerHeight,
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [AppColors.primary, AppColors.accent]),
border: Border(bottom: BorderSide(color: AppColors.border)),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(9),
border: Border.all(color: Colors.white.withValues(alpha: 0.25)),
),
child: const Center(child: ToothLogo(size: 18, color: Colors.white)),
),
if (_open) ...[
const SizedBox(width: 10),
const Text(
'DLS',
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.w800, letterSpacing: 1),
),
],
],
),
),
// Nav items
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 8),
for (int i = 0; i < widget.destinations.length; i++)
_SidebarItem(
icon: widget.destinations[i].icon,
selectedIcon: widget.destinations[i].selectedIcon,
label: widget.destinations[i].label,
selected: widget.selectedIndex == i,
open: _open,
onTap: () => widget.onTap(i),
),
const SizedBox(height: 8),
],
),
),
),
// Toggle button
Container(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: AppColors.border)),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => _open = !_open),
child: SizedBox(
height: 48,
child: Row(
mainAxisAlignment: _open ? MainAxisAlignment.start : MainAxisAlignment.center,
children: [
if (_open) const SizedBox(width: 20),
AnimatedRotation(
duration: const Duration(milliseconds: 220),
turns: _open ? 0.5 : 0,
child: const Icon(Icons.chevron_right_rounded, color: AppColors.textMuted, size: 20),
),
if (_open) ...[
const SizedBox(width: 8),
const Text('Daralt', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.textMuted)),
],
],
),
),
),
),
),
],
),
),
);
}
}
// ── Sidebar nav item ──────────────────────────────────────────────────────────
class _SidebarItem extends StatelessWidget {
const _SidebarItem({
required this.icon,
required this.selectedIcon,
required this.label,
required this.selected,
required this.open,
required this.onTap,
});
final Widget icon;
final Widget selectedIcon;
final String label;
final bool selected;
final bool open;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final item = Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Material(
color: selected ? const Color(0xFFDBEAFE) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: SizedBox(
height: 40,
child: open
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
IconTheme(
data: IconThemeData(
color: selected ? AppColors.primary : AppColors.textSecondary,
size: 20,
),
child: selected ? selectedIcon : icon,
),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
color: selected ? AppColors.primary : AppColors.textSecondary,
),
),
],
),
)
: Center(
child: IconTheme(
data: IconThemeData(
color: selected ? AppColors.primary : AppColors.textSecondary,
size: 20,
),
child: selected ? selectedIcon : icon,
),
),
),
),
),
);
if (!open) {
return Tooltip(message: label, preferBelow: false, child: item);
}
return item;
}
}
+52
View File
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../providers/auth_provider.dart';
import 'app_router.dart';
// Bridges Riverpod auth state changes to GoRouter's Listenable interface
class _AuthRouterNotifier extends ChangeNotifier {
_AuthRouterNotifier(this._ref) {
_ref.listen<AuthState>(authProvider, (_, __) => notifyListeners());
}
final Ref _ref;
}
final routerProvider = Provider<GoRouter>((ref) {
final notifier = _AuthRouterNotifier(ref);
return GoRouter(
refreshListenable: notifier,
initialLocation: routeSignIn,
redirect: (context, state) {
final auth = ref.read(authProvider);
if (auth.isLoading) return null;
final loc = state.matchedLocation;
final onLoginOrRegister = loc == routeSignIn || loc == routeSignUp;
final onAuthPage = onLoginOrRegister || loc == routeOnboarding;
if (!auth.isAuthenticated) {
return onAuthPage ? null : routeSignIn;
}
// Authenticated but no tenant → onboarding
if (auth.activeTenant == null) {
return loc == routeOnboarding ? null : routeOnboarding;
}
final isLab = auth.activeTenant!.tenant.isLab;
if (onAuthPage) {
return isLab ? routeLabDashboard : routeClinicDashboard;
}
if (isLab && loc.startsWith('/clinic')) return routeLabDashboard;
if (!isLab && loc.startsWith('/lab')) return routeClinicDashboard;
return null;
},
routes: buildRoutes(),
);
});
+171
View File
@@ -0,0 +1,171 @@
import '../../features/shared/job_files_repository.dart';
import '../../features/shared/tenant_team_repository.dart';
import '../../models/job_file.dart';
import '../../models/tenant.dart';
import '../api/pocketbase_client.dart';
// ── Message segments ──────────────────────────────────────────────────────────
sealed class MessageSegment {}
class TextSegment extends MessageSegment {
TextSegment(this.text);
final String text;
}
class ActionSegment extends MessageSegment {
ActionSegment(this.action);
final AiAction action;
}
// ── Action model ──────────────────────────────────────────────────────────────
class AiAction {
const AiAction({
required this.type,
required this.params,
required this.label,
});
final String type;
final Map<String, String> params;
final String label;
bool get isDangerous => type == 'cancel_job';
bool get isFileAction => type == 'job_files';
}
// ── Action outcome ────────────────────────────────────────────────────────────
sealed class ActionOutcome {}
class ActionSuccess extends ActionOutcome {
ActionSuccess(this.message);
final String message;
}
class ActionError extends ActionOutcome {
ActionError(this.error);
final String error;
}
class ActionFiles extends ActionOutcome {
ActionFiles(this.files);
final List<JobFile> files;
}
// ── Parser ────────────────────────────────────────────────────────────────────
List<MessageSegment> parseSegments(String text) {
// Strip code fences wrapping <dls-action> tags that the AI sometimes emits.
// Handles: ```xml\n<dls-action .../>\n``` and ```\n<dls-action .../>\n```
text = text.replaceAllMapped(
RegExp(r'```(?:xml)?\s*\n(\s*<dls-action\s[^>]*/>)\s*\n\s*```'),
(m) => m.group(1)!,
);
// Also handle inline variant: ```xml <dls-action .../> ```
text = text.replaceAllMapped(
RegExp(r'```(?:xml)?\s*(<dls-action\s[^>]*/>)\s*```'),
(m) => m.group(1)!,
);
final pattern = RegExp(r'<dls-action\s([^/]*?)/>', dotAll: true);
final segments = <MessageSegment>[];
int last = 0;
for (final m in pattern.allMatches(text)) {
final before = text.substring(last, m.start).trim();
if (before.isNotEmpty) segments.add(TextSegment(before));
final attrs = _parseAttrs(m.group(1) ?? '');
segments.add(ActionSegment(AiAction(
type: attrs['type'] ?? '',
params: attrs,
label: attrs['label'] ?? attrs['type'] ?? 'İşlem',
)));
last = m.end;
}
final rest = text.substring(last).trim();
if (rest.isNotEmpty) segments.add(TextSegment(rest));
return segments;
}
Map<String, String> _parseAttrs(String s) {
final result = <String, String>{};
for (final m in RegExp(r'(\w+)="([^"]*)"').allMatches(s)) {
result[m.group(1)!] = m.group(2)!;
}
return result;
}
// ── Executor ──────────────────────────────────────────────────────────────────
class AiActionExecutor {
static final _pb = PocketBaseClient.instance.pb;
static Future<ActionOutcome> execute(
AiAction action,
TenantMembership membership,
) async {
try {
return switch (action.type) {
'cancel_job' => await _cancelJob(action.params),
'mark_delivered' => await _markDelivered(action.params),
'job_files' => await _jobFiles(action.params),
'add_member' => await _addMember(action.params, membership),
_ => ActionError('Bilinmeyen işlem türü: ${action.type}'),
};
} catch (e) {
final msg = e.toString();
if (msg.length > 120) return ActionError('Sunucu hatası');
return ActionError(msg);
}
}
static Future<ActionOutcome> _cancelJob(Map<String, String> p) async {
final id = p['job_id'];
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
await _pb.collection('jobs').update(id, body: {'status': 'cancelled'});
return ActionSuccess('İş başarıyla iptal edildi.');
}
static Future<ActionOutcome> _markDelivered(Map<String, String> p) async {
final id = p['job_id'];
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
await _pb.collection('jobs').update(id, body: {'status': 'delivered'});
return ActionSuccess('İş teslim edildi olarak işaretlendi.');
}
static Future<ActionOutcome> _jobFiles(Map<String, String> p) async {
final id = p['job_id'];
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
final files = await JobFilesRepository.instance.listForJob(id);
if (files.isEmpty) return ActionSuccess('Bu iş için henüz dosya yüklenmemiş.');
return ActionFiles(files);
}
static Future<ActionOutcome> _addMember(
Map<String, String> p,
TenantMembership membership,
) async {
final email = p['email'];
final firstName = p['first_name'];
final lastName = p['last_name'] ?? '';
final role = p['role'];
final password = p['password'];
if (email == null || firstName == null || role == null || password == null) {
return ActionError('Eksik bilgi: e-posta, ad, rol ve şifre gerekli.');
}
await TenantTeamRepository.instance.addMember(
tenantId: membership.tenant.id,
email: email,
password: password,
firstName: firstName,
lastName: lastName,
role: TenantMembership.parseRole(role),
);
return ActionSuccess('$firstName $lastName ekibe eklendi.');
}
}
+226
View File
@@ -0,0 +1,226 @@
import 'package:pocketbase/pocketbase.dart';
import '../api/pocketbase_client.dart';
import '../../models/tenant.dart';
class AiContextBuilder {
AiContextBuilder._();
static final instance = AiContextBuilder._();
PocketBase get _pb => PocketBaseClient.instance.pb;
Future<String> build(TenantMembership membership) async {
final tenant = membership.tenant;
final tenantId = tenant.id;
final isLab = tenant.kind == TenantKind.lab;
final now = DateTime.now();
final dateStr = '${now.day}.${now.month}.${now.year}';
final results = await Future.wait([
_fetchActiveJobs(tenantId, isLab),
_fetchRecentDelivered(tenantId, isLab),
_fetchFinance(tenantId, isLab),
_fetchTeam(tenantId),
]);
final actions = _actionsPrompt(isLab);
return 'Sen DLS (Dental Lab System) uygulamasinin akilli asistanisin.\n'
'${tenant.companyName} adli ${isLab ? 'dental laboratuvarinin' : 'dis kliniginin'} verilerine erisebilirsin.\n'
'Kullanici rolu: ${isLab ? 'LABORATUVAR' : 'KLINIK'}\n'
'\n'
'Tarih: $dateStr\n'
'\n'
'${results[0]}\n'
'\n'
'${results[1]}\n'
'\n'
'${results[2]}\n'
'\n'
'${results[3]}\n'
'\n'
'$actions\n'
'\n'
'Yanit kurallari:\n'
'- Turkce, kisa ve net yaz\n'
'- Sadece yukaridaki verilerden hareketle yorum yap\n'
'- Listelerde madde isareti (- ) kullan\n'
'- Onemli bilgileri **kalin** yaz\n'
'- Aksiyon etiketlerini HERZAMAN metnin sonuna koy\n'
'- ${isLab ? 'Is kodlari icin [ID:...] formatini kullan' : 'Hasta kodlari ve is durumlarini net belirt'}\n';
}
static String _actionsPrompt(bool isLab) {
final buf = StringBuffer();
buf.writeln('## EYLEM YETKILERIN');
buf.writeln('Kullanici bir islem yapmak istediginde asagidaki XML etiketlerini yanita ekle:');
buf.writeln('');
buf.writeln('Is dosyalarini gostermek:');
buf.writeln('<dls-action type="job_files" job_id="JOB_ID" label="AB001 dosyalarini goster"/>');
buf.writeln('');
buf.writeln('Is iptal etmek:');
buf.writeln('<dls-action type="cancel_job" job_id="JOB_ID" label="AB001 isini iptal et"/>');
if (!isLab) {
buf.writeln('');
buf.writeln('Teslim edildi isaretlemek (sadece klinik):');
buf.writeln('<dls-action type="mark_delivered" job_id="JOB_ID" label="AB001 teslim edildi"/>');
}
buf.writeln('');
buf.writeln('Ekip uyesi eklemek (TUM bilgiler alindiktan sonra):');
buf.writeln('<dls-action type="add_member" email="..." first_name="..." last_name="..." role="technician|admin|doctor|delivery|finance|member" password="..." label="Ad Soyad ekle"/>');
buf.writeln('');
buf.writeln('KURALLAR:');
buf.writeln('- Etiketi SADECE kullanici acikca islem istediginde ekle');
buf.writeln('- Sifre sorulursa kullanicidan al, ASLA uydurma');
buf.writeln('- iptal gibi geri alinmaz islemleri acikca belirt');
buf.writeln('- Etiket icindeki job_id degerini yukaridaki is listesinden al');
buf.writeln('- <dls-action> etiketlerini KESINLİKLE kod blogu (```xml veya ```) icine ALMA, duz metin olarak yaz');
return buf.toString();
}
Future<String> _fetchActiveJobs(String tenantId, bool isLab) async {
try {
final tenantField = isLab ? 'lab_tenant_id' : 'clinic_tenant_id';
final counterpartField = isLab ? 'clinic_tenant_id' : 'lab_tenant_id';
final result = await _pb.collection('jobs').getList(
filter: '$tenantField = "$tenantId" && status != "delivered" && status != "cancelled"',
perPage: 60,
sort: '-created',
expand: counterpartField,
);
if (result.items.isEmpty) return '## Aktif Isler\nSu an aktif is yok.';
final counterpartLabel = isLab ? 'Klinik' : 'Lab';
final lines = result.items.map((r) {
final j = r.toJson();
final jobId = j['id'] as String? ?? '';
final expand = j['expand'] as Map<String, dynamic>?;
final counterpart =
(expand?[counterpartField] as Map?)?['company_name'] as String? ?? '-';
final status = _statusTr(j['status'] as String? ?? '');
final prosthetic = j['prosthetic_type'] as String? ?? '-';
final patient = j['patient_code'] as String? ?? '-';
final step = j['current_step'] as String?;
final stepPart = (step != null && step.isNotEmpty) ? ' | Adim: $step' : '';
final due = j['due_date'] as String? ?? '';
final duePart = due.isNotEmpty ? ' | Termin: ${due.substring(0, 10)}' : '';
return '- [ID:$jobId] Hasta: $patient | $prosthetic | $status$stepPart | $counterpartLabel: $counterpart$duePart';
}).join('\n');
return '## Aktif Isler (${result.items.length})\n$lines';
} catch (e) {
return '## Aktif Isler\n(Veri alinamadi: $e)';
}
}
Future<String> _fetchRecentDelivered(String tenantId, bool isLab) async {
try {
final tenantField = isLab ? 'lab_tenant_id' : 'clinic_tenant_id';
final counterpartField = isLab ? 'clinic_tenant_id' : 'lab_tenant_id';
final result = await _pb.collection('jobs').getList(
filter: '$tenantField = "$tenantId" && status = "delivered"',
perPage: 10,
sort: '-updated',
expand: counterpartField,
);
if (result.items.isEmpty) return '## Son Teslim Edilenler\nHenuz teslim edilen is yok.';
final counterpartLabel = isLab ? 'Klinik' : 'Lab';
final lines = result.items.map((r) {
final j = r.toJson();
final jobId = j['id'] as String? ?? '';
final expand = j['expand'] as Map<String, dynamic>?;
final counterpart =
(expand?[counterpartField] as Map?)?['company_name'] as String? ?? '-';
final prosthetic = j['prosthetic_type'] as String? ?? '-';
final patient = j['patient_code'] as String? ?? '-';
final updated = (j['updated'] as String? ?? '');
final datePart = updated.length >= 10 ? updated.substring(0, 10) : '';
return '- [ID:$jobId] Hasta: $patient | $prosthetic | $counterpartLabel: $counterpart${datePart.isNotEmpty ? ' | Tarih: $datePart' : ''}';
}).join('\n');
return '## Son Teslim Edilenler (son 10)\n$lines';
} catch (_) {
return '## Son Teslim Edilenler\n(Veri alinamadi)';
}
}
Future<String> _fetchFinance(String tenantId, bool isLab) async {
try {
final type = isLab ? 'receivable' : 'payable';
final result = await _pb.collection('finance_entries').getList(
filter: 'tenant_id = "$tenantId" && type = "$type"',
perPage: 200,
);
double pending = 0, paid = 0;
for (final r in result.items) {
final j = r.toJson();
final amount = (j['amount'] as num?)?.toDouble() ?? 0;
if (j['status'] == 'pending') {
pending += amount;
} else {
paid += amount;
}
}
final label = isLab ? 'alacak' : 'borc';
return '## Finans\n'
'- Bekleyen $label: ${pending.toStringAsFixed(0)} TL\n'
'- Tahsil edilen: ${paid.toStringAsFixed(0)} TL';
} catch (_) {
return '## Finans\n(Veri alinamadi)';
}
}
Future<String> _fetchTeam(String tenantId) async {
try {
final result = await _pb.collection('tenant_members').getList(
filter: 'tenant_id = "$tenantId"',
expand: 'user_id',
perPage: 50,
);
if (result.items.isEmpty) return '## Ekip\nUye yok.';
final lines = result.items.map((r) {
final j = r.toJson();
final expand = j['expand'] as Map<String, dynamic>?;
final user = expand?['user_id'] as Map<String, dynamic>?;
final first = (user?['first_name'] as String?) ?? '';
final last = (user?['last_name'] as String?) ?? '';
final email = (user?['email'] as String?) ?? '';
final name =
'$first $last'.trim().isNotEmpty ? '$first $last'.trim() : email;
final role = _roleTr(j['role'] as String? ?? '');
return '- $name ($role)';
}).join('\n');
return '## Ekip (${result.items.length} uye)\n$lines';
} catch (_) {
return '## Ekip\n(Veri alinamadi)';
}
}
static String _statusTr(String s) => switch (s) {
'pending' => 'Bekliyor',
'in_progress' => 'Devam ediyor',
'sent' => 'Gonderildi',
'revision' => 'Revizyon',
'delivered' => 'Teslim edildi',
'cancelled' => 'Iptal',
_ => s,
};
static String _roleTr(String s) => switch (s) {
'owner' => 'Sahibi',
'admin' => 'Yonetici',
'technician' => 'Teknisyen',
'delivery' => 'Teslimat',
'finance' => 'Finans',
'doctor' => 'Hekim',
_ => 'Uye',
};
}
+71
View File
@@ -0,0 +1,71 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
class AiService {
static const _baseUrl = 'https://api.featherless.ai/v1';
static const _apiKey =
'rc_e10f49aaa4f7af03dcd9da115cfc12cc1988665e895955c11f77788ee5ad93c6';
static const _model = 'Qwen/Qwen2.5-7B-Instruct';
AiService._();
static final instance = AiService._();
Stream<String> streamChat({
required String systemPrompt,
required List<Map<String, String>> messages,
}) async* {
final client = http.Client();
try {
final request = http.Request(
'POST',
Uri.parse('$_baseUrl/chat/completions'),
);
request.headers.addAll({
'Authorization': 'Bearer $_apiKey',
'Content-Type': 'application/json',
});
request.body = jsonEncode({
'model': _model,
'messages': [
{'role': 'system', 'content': systemPrompt},
...messages,
],
'stream': true,
'max_tokens': 2048,
'temperature': 0.7,
});
final response = await client.send(request);
if (response.statusCode != 200) {
final body = await response.stream.bytesToString();
String msg = 'API hatası ${response.statusCode}';
try {
final j = jsonDecode(body) as Map<String, dynamic>;
msg = (j['error'] as Map?)?['message'] as String? ?? msg;
} catch (_) {}
throw Exception(msg);
}
final lines = response.stream
.transform(utf8.decoder)
.transform(const LineSplitter());
await for (final line in lines) {
if (!line.startsWith('data: ')) continue;
final payload = line.substring(6).trim();
if (payload == '[DONE]') break;
try {
final j = jsonDecode(payload) as Map<String, dynamic>;
final choices = j['choices'] as List?;
if (choices == null || choices.isEmpty) continue;
final delta = choices.first['delta'] as Map<String, dynamic>?;
final content = delta?['content'] as String?;
if (content != null && content.isNotEmpty) yield content;
} catch (_) {}
}
} finally {
client.close();
}
}
}
+117
View File
@@ -0,0 +1,117 @@
import 'package:pocketbase/pocketbase.dart';
import '../api/pocketbase_client.dart';
import '../../models/job.dart';
class JobHistoryEntry {
const JobHistoryEntry({
required this.id,
required this.action,
required this.createdAt,
this.step,
this.note,
});
final String id;
final JobHistoryAction action;
final JobStep? step;
final String? note;
final DateTime createdAt;
}
enum JobHistoryAction {
accepted,
handedToClinic,
approved,
revisionRequested,
delivered,
cancelled,
}
extension JobHistoryActionExt on JobHistoryAction {
String get value => switch (this) {
JobHistoryAction.accepted => 'accepted',
JobHistoryAction.handedToClinic => 'handed_to_clinic',
JobHistoryAction.approved => 'approved',
JobHistoryAction.revisionRequested => 'revision_requested',
JobHistoryAction.delivered => 'delivered',
JobHistoryAction.cancelled => 'cancelled',
};
}
class JobHistoryService {
JobHistoryService._();
static final instance = JobHistoryService._();
PocketBase get _pb => PocketBaseClient.instance.pb;
String get _currentUserId =>
(_pb.authStore.record?.id) ?? (_pb.authStore.model as dynamic)?.id as String? ?? '';
Future<List<JobHistoryEntry>> listForJob(String jobId) async {
try {
final result = await _pb.collection('job_status_history').getList(
filter: 'job_id = "$jobId"',
perPage: 200,
);
return (result.items.map((r) {
final j = r.toJson();
String? str(dynamic v) {
final s = v as String?;
return (s == null || s.isEmpty) ? null : s;
}
return JobHistoryEntry(
id: j['id'] as String,
action: _parseAction(j['action_type'] as String? ?? ''),
step: str(j['step']) != null ? _parseStep(j['step'] as String) : null,
note: str(j['note']),
createdAt: DateTime.parse(j['created'] as String),
);
}).toList()..sort((a, b) => a.createdAt.compareTo(b.createdAt)));
} catch (_) {
return [];
}
}
static JobHistoryAction _parseAction(String s) => switch (s) {
'accepted' => JobHistoryAction.accepted,
'handed_to_clinic' => JobHistoryAction.handedToClinic,
'approved' => JobHistoryAction.approved,
'revision_requested' => JobHistoryAction.revisionRequested,
'delivered' => JobHistoryAction.delivered,
_ => JobHistoryAction.cancelled,
};
static JobStep _parseStep(String s) => switch (s) {
'alt_yapi_prova' => JobStep.altYapiProva,
'ust_yapi_prova' => JobStep.ustYapiProva,
'mum_prova' => JobStep.mumProva,
'disler_prova' => JobStep.dislerProva,
'dayanak_prova' => JobStep.dayanakProva,
'kron_prova' => JobStep.kronProva,
'cila_bitim' => JobStep.cilaBitim,
_ => JobStep.olcu,
};
Future<void> append({
required String jobId,
required String clinicTenantId,
required String labTenantId,
required JobHistoryAction action,
JobStep? step,
String? note,
String? userId,
}) async {
try {
await _pb.collection('job_status_history').create(body: {
'job_id': jobId,
'clinic_tenant_id': clinicTenantId,
'lab_tenant_id': labTenantId,
'completed_by': userId ?? _currentUserId,
'action_type': action.value,
if (step != null) 'step': step.value,
if (note != null && note.isNotEmpty) 'note': note,
});
} catch (_) {
// history failures must never block the main mutation
}
}
}
@@ -0,0 +1,64 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart';
import 'package:onesignal_flutter/onesignal_flutter.dart';
// ─── Replace with your OneSignal App ID from onesignal.com ──────────────────
const _kOneSignalAppId = '524cb6d8-2640-4f85-bb24-c9c762233de7';
// ────────────────────────────────────────────────────────────────────────────
class NotificationService {
NotificationService._();
static GoRouter? _router;
static bool _initialized = false;
static void setRouter(GoRouter router) => _router = router;
static bool get _supported =>
!kIsWeb && (Platform.isIOS || Platform.isAndroid || Platform.isMacOS);
static Future<void> init() async {
if (!_supported || _initialized) return;
_initialized = true;
OneSignal.initialize(_kOneSignalAppId);
await OneSignal.Notifications.requestPermission(true);
// Show notification even when app is in foreground
OneSignal.Notifications.addForegroundWillDisplayListener((event) {
event.notification.display();
});
// Tap → navigate to job detail
OneSignal.Notifications.addClickListener((event) {
final data = event.notification.additionalData;
if (data == null) return;
final jobId = data['job_id'] as String?;
final tenantType = data['tenant_type'] as String?;
if (jobId == null || _router == null) return;
if (tenantType == 'lab') {
_router!.push('/lab/jobs/$jobId');
} else {
_router!.push('/clinic/jobs/$jobId');
}
});
}
/// Call after successful login. Links the OneSignal player to this user.
static Future<void> loginUser(String userId, {bool isLab = false}) async {
if (!_supported) return;
try {
await OneSignal.login(userId);
OneSignal.User.addTagWithKey('tenant_type', isLab ? 'lab' : 'clinic');
} catch (_) {}
}
/// Call on logout.
static Future<void> logoutUser() async {
if (!_supported) return;
try {
await OneSignal.logout();
} catch (_) {}
}
}
+37
View File
@@ -0,0 +1,37 @@
import 'package:pocketbase/pocketbase.dart';
import '../api/pocketbase_client.dart';
typedef UnsubFn = Future<void> Function();
class RealtimeService {
RealtimeService._();
static final instance = RealtimeService._();
final _pb = PocketBaseClient.instance.pb;
UnsubFn watch(
String collection, {
String topic = '*',
String filter = '',
required void Function(RecordSubscriptionEvent) onEvent,
}) {
UnsubFn? cancel;
_pb.collection(collection).subscribe(topic, onEvent, filter: filter).then((fn) {
cancel = fn;
});
return () async {
try {
final fn = cancel;
if (fn != null) {
await fn();
} else {
await _pb.collection(collection).unsubscribe(topic);
}
} catch (_) {
await _pb.collection(collection).unsubscribe(topic);
}
};
}
}
+299
View File
@@ -0,0 +1,299 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
abstract final class AppColors {
// Primary — professional navy
static const primary = Color(0xFF1E3A5F);
static const onPrimary = Color(0xFFFFFFFF);
// Accent — sky blue CTA
static const accent = Color(0xFF0369A1);
static const onAccent = Color(0xFFFFFFFF);
// Status
static const pending = Color(0xFFF59E0B);
static const pendingBg = Color(0xFFFFFBEB);
static const inProgress = Color(0xFF0369A1);
static const inProgressBg = Color(0xFFEFF6FF);
static const success = Color(0xFF059669);
static const successBg = Color(0xFFECFDF5);
static const cancelled = Color(0xFFDC2626);
static const cancelledBg = Color(0xFFFEF2F2);
// Surfaces
static const background = Color(0xFFF1F5F9);
static const surface = Color(0xFFFFFFFF);
static const surfaceVariant = Color(0xFFF8FAFC);
static const muted = Color(0xFFE2E8F0);
static const border = Color(0xFFE2E8F0);
// Text
static const textPrimary = Color(0xFF0F172A);
static const textSecondary = Color(0xFF64748B);
static const textMuted = Color(0xFF94A3B8);
// Dark variants
static const darkBackground = Color(0xFF0F172A);
static const darkSurface = Color(0xFF1E293B);
static const darkSurfaceVariant = Color(0xFF273344);
static const darkBorder = Color(0xFF334155);
static const darkTextPrimary = Color(0xFFF1F5F9);
static const darkTextSecondary = Color(0xFF94A3B8);
}
abstract final class AppLayout {
/// Window width above which the sidebar navigation is shown instead of bottom nav.
static const double sidebarBreakpoint = 720.0;
/// Window width above which wide-desktop content layouts activate
/// (e.g., 3-column stat card row, 2-column forms).
static const double wideBreakpoint = 1100.0;
/// Maximum content width used for dashboard horizontal padding.
static const double contentMaxWidth = 1040.0;
}
abstract final class AppTheme {
static TextTheme _buildTextTheme(Color bodyColor, Color displayColor) {
final base = GoogleFonts.plusJakartaSansTextTheme();
return base.copyWith(
displayLarge: base.displayLarge?.copyWith(color: displayColor, fontWeight: FontWeight.w800),
displayMedium: base.displayMedium?.copyWith(color: displayColor, fontWeight: FontWeight.w700),
headlineLarge: base.headlineLarge?.copyWith(color: displayColor, fontWeight: FontWeight.w700),
headlineMedium: base.headlineMedium?.copyWith(color: displayColor, fontWeight: FontWeight.w700),
headlineSmall: base.headlineSmall?.copyWith(color: displayColor, fontWeight: FontWeight.w600),
titleLarge: base.titleLarge?.copyWith(color: displayColor, fontWeight: FontWeight.w600),
titleMedium: base.titleMedium?.copyWith(color: displayColor, fontWeight: FontWeight.w600),
titleSmall: base.titleSmall?.copyWith(color: displayColor, fontWeight: FontWeight.w500),
bodyLarge: base.bodyLarge?.copyWith(color: bodyColor),
bodyMedium: base.bodyMedium?.copyWith(color: bodyColor),
bodySmall: base.bodySmall?.copyWith(color: AppColors.textSecondary),
labelLarge: base.labelLarge?.copyWith(fontWeight: FontWeight.w600),
labelMedium: base.labelMedium?.copyWith(fontWeight: FontWeight.w500),
);
}
static final light = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme(
brightness: Brightness.light,
primary: AppColors.primary,
onPrimary: AppColors.onPrimary,
primaryContainer: const Color(0xFFDBEAFE),
onPrimaryContainer: AppColors.primary,
secondary: AppColors.accent,
onSecondary: AppColors.onAccent,
secondaryContainer: const Color(0xFFE0F2FE),
onSecondaryContainer: AppColors.accent,
tertiary: AppColors.success,
onTertiary: Colors.white,
tertiaryContainer: AppColors.successBg,
onTertiaryContainer: AppColors.success,
error: AppColors.cancelled,
onError: Colors.white,
errorContainer: AppColors.cancelledBg,
onErrorContainer: AppColors.cancelled,
surface: AppColors.surface,
onSurface: AppColors.textPrimary,
surfaceContainerHighest: AppColors.surfaceVariant,
onSurfaceVariant: AppColors.textSecondary,
outline: AppColors.border,
outlineVariant: AppColors.muted,
scrim: Colors.black54,
inverseSurface: AppColors.darkSurface,
onInverseSurface: AppColors.darkTextPrimary,
inversePrimary: const Color(0xFF93C5FD),
),
scaffoldBackgroundColor: AppColors.background,
textTheme: _buildTextTheme(AppColors.textPrimary, AppColors.textPrimary),
appBarTheme: AppBarTheme(
backgroundColor: AppColors.surface,
foregroundColor: AppColors.textPrimary,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
shadowColor: Colors.transparent,
centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle.dark,
titleTextStyle: GoogleFonts.plusJakartaSans(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
iconTheme: const IconThemeData(color: AppColors.textPrimary, size: 22),
),
cardTheme: CardThemeData(
elevation: 0,
color: AppColors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: AppColors.border, width: 1),
),
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: AppColors.surface,
elevation: 0,
shadowColor: Colors.transparent,
indicatorColor: const Color(0xFFDBEAFE),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: AppColors.primary, size: 22);
}
return IconThemeData(color: AppColors.textSecondary, size: 22);
}),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final style = GoogleFonts.plusJakartaSans(fontSize: 11);
if (states.contains(WidgetState.selected)) {
return style.copyWith(fontWeight: FontWeight.w600, color: AppColors.primary);
}
return style.copyWith(fontWeight: FontWeight.w500, color: AppColors.textSecondary);
}),
surfaceTintColor: Colors.transparent,
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: GoogleFonts.plusJakartaSans(fontSize: 15, fontWeight: FontWeight.w600),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
minimumSize: const Size(0, 48),
side: const BorderSide(color: AppColors.border, width: 1.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: GoogleFonts.plusJakartaSans(fontSize: 15, fontWeight: FontWeight.w600),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.accent, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
labelStyle: GoogleFonts.plusJakartaSans(color: AppColors.textSecondary),
hintStyle: GoogleFonts.plusJakartaSans(color: AppColors.textMuted),
),
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
side: BorderSide.none,
),
dividerTheme: const DividerThemeData(
color: AppColors.border,
thickness: 1,
space: 1,
),
listTileTheme: const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
),
);
static final dark = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme(
brightness: Brightness.dark,
primary: const Color(0xFF93C5FD),
onPrimary: const Color(0xFF1E3A5F),
primaryContainer: const Color(0xFF1E3A5F),
onPrimaryContainer: const Color(0xFFDBEAFE),
secondary: const Color(0xFF7DD3FC),
onSecondary: const Color(0xFF0C4A6E),
secondaryContainer: const Color(0xFF0C4A6E),
onSecondaryContainer: const Color(0xFFE0F2FE),
tertiary: const Color(0xFF6EE7B7),
onTertiary: const Color(0xFF064E3B),
tertiaryContainer: const Color(0xFF064E3B),
onTertiaryContainer: const Color(0xFFD1FAE5),
error: const Color(0xFFFCA5A5),
onError: const Color(0xFF7F1D1D),
errorContainer: const Color(0xFF7F1D1D),
onErrorContainer: const Color(0xFFFEE2E2),
surface: AppColors.darkSurface,
onSurface: AppColors.darkTextPrimary,
surfaceContainerHighest: AppColors.darkSurfaceVariant,
onSurfaceVariant: AppColors.darkTextSecondary,
outline: AppColors.darkBorder,
outlineVariant: const Color(0xFF1E293B),
scrim: Colors.black87,
inverseSurface: const Color(0xFFF1F5F9),
onInverseSurface: AppColors.textPrimary,
inversePrimary: AppColors.primary,
),
scaffoldBackgroundColor: AppColors.darkBackground,
textTheme: _buildTextTheme(AppColors.darkTextPrimary, AppColors.darkTextPrimary),
appBarTheme: AppBarTheme(
backgroundColor: AppColors.darkSurface,
foregroundColor: AppColors.darkTextPrimary,
elevation: 0,
scrolledUnderElevation: 1,
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: GoogleFonts.plusJakartaSans(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.darkTextPrimary,
),
),
cardTheme: CardThemeData(
elevation: 0,
color: AppColors.darkSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: AppColors.darkBorder, width: 1),
),
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: AppColors.darkSurface,
elevation: 0,
indicatorColor: const Color(0xFF1E3A5F),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: Color(0xFF93C5FD), size: 22);
}
return IconThemeData(color: AppColors.darkTextSecondary, size: 22);
}),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final style = GoogleFonts.plusJakartaSans(fontSize: 11);
if (states.contains(WidgetState.selected)) {
return style.copyWith(fontWeight: FontWeight.w600, color: const Color(0xFF93C5FD));
}
return style.copyWith(fontWeight: FontWeight.w500, color: AppColors.darkTextSecondary);
}),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF93C5FD),
foregroundColor: const Color(0xFF1E3A5F),
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: GoogleFonts.plusJakartaSans(fontSize: 15, fontWeight: FontWeight.w600),
),
),
dividerTheme: const DividerThemeData(
color: AppColors.darkBorder,
thickness: 1,
space: 1,
),
);
}
+35
View File
@@ -0,0 +1,35 @@
class CurrencyFormatter {
static const _symbols = {
'TRY': '',
'USD': '\$',
'EUR': '',
'GBP': '£',
'AED': 'د.إ',
};
static const _rtlSymbols = {'AED'};
static String symbol(String code) => _symbols[code] ?? code;
static String format(double amount, String currencyCode) {
final sym = symbol(currencyCode);
final isRtl = _rtlSymbols.contains(currencyCode);
final value = _formatNumber(amount);
return isRtl ? '$value $sym' : '$sym$value';
}
static String _formatNumber(double amount) {
final formatted = amount.toStringAsFixed(2);
final parts = formatted.split('.');
final intPart = parts[0];
final decPart = parts[1];
final buf = StringBuffer();
final digits = intPart.replaceAll('-', '');
final isNeg = intPart.startsWith('-');
for (int i = 0; i < digits.length; i++) {
if (i > 0 && (digits.length - i) % 3 == 0) buf.write(',');
buf.write(digits[i]);
}
return '${isNeg ? '-' : ''}$buf.$decPart';
}
}
+40
View File
@@ -0,0 +1,40 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../api/pocketbase_client.dart';
import '../../models/job_file.dart';
import '../theme/app_theme.dart';
class FileDownloadHelper {
static Future<void> download(BuildContext context, JobFile file, {Rect? shareOrigin}) async {
if (file.downloadUrl.isEmpty) return;
final messenger = ScaffoldMessenger.of(context);
try {
final pb = PocketBaseClient.instance.pb;
final fileToken = await pb.files.getToken();
final uri = Uri.parse('${file.downloadUrl}?token=$fileToken');
final response = await http.get(uri);
if (response.statusCode != 200) throw Exception('HTTP ${response.statusCode}');
final dir = await getTemporaryDirectory();
final path = '${dir.path}/${file.name}';
await File(path).writeAsBytes(response.bodyBytes);
await Share.shareXFiles(
[XFile(path, mimeType: file.mimeType ?? 'application/octet-stream')],
subject: file.name,
sharePositionOrigin: shareOrigin ?? const Rect.fromLTWH(0, 0, 1, 1),
);
} catch (e) {
if (context.mounted) {
messenger.showSnackBar(
SnackBar(
content: Text('İndirilemedi: $e'),
backgroundColor: AppColors.cancelled,
),
);
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class AppSearchField extends StatelessWidget {
const AppSearchField({
super.key,
required this.controller,
required this.onChanged,
this.hint,
});
final TextEditingController controller;
final ValueChanged<String> onChanged;
final String? hint;
@override
Widget build(BuildContext context) {
return Container(
color: AppColors.surface,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: ListenableBuilder(
listenable: controller,
builder: (context, _) => Container(
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.border),
),
child: TextField(
controller: controller,
onChanged: onChanged,
style: const TextStyle(
fontSize: 14,
color: AppColors.textPrimary,
),
decoration: InputDecoration(
hintText: hint ?? 'Ara...',
hintStyle: const TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
prefixIcon: const Icon(
Icons.search_rounded,
color: AppColors.textMuted,
size: 20,
),
suffixIcon: controller.text.isNotEmpty
? GestureDetector(
onTap: () {
controller.clear();
onChanged('');
},
child: const Padding(
padding: EdgeInsets.all(12),
child: Icon(
Icons.close_rounded,
color: AppColors.textMuted,
size: 16,
),
),
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
),
);
}
}
+328
View File
@@ -0,0 +1,328 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_theme.dart';
import 'tooth_logo.dart';
class GradientAppBar extends StatelessWidget implements PreferredSizeWidget {
const GradientAppBar({
super.key,
required this.title,
required this.category,
this.actions = const [],
this.searchController,
this.onSearchChanged,
this.searchHint,
});
final String title;
final String category;
final List<Widget> actions;
final TextEditingController? searchController;
final ValueChanged<String>? onSearchChanged;
final String? searchHint;
bool get _hasSearch =>
searchController != null && onSearchChanged != null;
@override
Size get preferredSize =>
Size.fromHeight(kToolbarHeight + (_hasSearch ? 52.0 : 0.0));
@override
Widget build(BuildContext context) {
final isDesktop =
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
final searchBottom = _hasSearch
? _SearchBarBottom(
controller: searchController!,
onChanged: onSearchChanged!,
hint: searchHint ?? 'Ara...',
)
: null;
if (isDesktop) {
return AppBar(
backgroundColor: AppColors.surface,
foregroundColor: AppColors.textPrimary,
elevation: 0,
scrolledUnderElevation: 0,
automaticallyImplyLeading: false,
titleSpacing: 24,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'DLS',
style: TextStyle(
fontSize: 11,
color: AppColors.textSecondary.withValues(alpha: 0.8),
letterSpacing: 0.3,
),
),
Text(
title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
],
),
actions: [
...actions,
if (actions.isNotEmpty) const SizedBox(width: 8),
],
iconTheme:
const IconThemeData(color: AppColors.textSecondary, size: 22),
actionsIconTheme:
const IconThemeData(color: AppColors.textSecondary, size: 22),
bottom: searchBottom,
);
}
return AppBar(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle.light,
automaticallyImplyLeading: false,
leadingWidth: 60,
leading: Padding(
padding: const EdgeInsets.only(left: 16, top: 8, bottom: 8),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child:
const Center(child: ToothLogo(size: 20, color: Colors.white)),
),
),
titleSpacing: 8,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
category,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 1.5,
),
),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
actions: actions.isNotEmpty
? [...actions, const SizedBox(width: 4)]
: null,
iconTheme: const IconThemeData(color: Colors.white, size: 22),
actionsIconTheme:
const IconThemeData(color: Colors.white, size: 22),
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF0F172A), AppColors.primary],
),
),
),
bottom: searchBottom,
);
}
}
// ── iOS-26-style search bar shown below the AppBar title ─────────────────────
class _SearchBarBottom extends StatelessWidget implements PreferredSizeWidget {
const _SearchBarBottom({
required this.controller,
required this.onChanged,
required this.hint,
});
final TextEditingController controller;
final ValueChanged<String> onChanged;
final String hint;
@override
Size get preferredSize => const Size.fromHeight(52);
@override
Widget build(BuildContext context) {
final isDesktop =
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
final bg = isDesktop
? AppColors.surfaceVariant
: Colors.white.withValues(alpha: 0.15);
final textColor = isDesktop ? AppColors.textPrimary : Colors.white;
final iconColor = isDesktop
? AppColors.textMuted
: Colors.white.withValues(alpha: 0.65);
final hintColor = isDesktop
? AppColors.textMuted
: Colors.white.withValues(alpha: 0.5);
return SizedBox(
height: 52,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 10),
child: ListenableBuilder(
listenable: controller,
builder: (context, _) => Container(
height: 38,
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12),
border: isDesktop
? Border.all(color: AppColors.border)
: null,
),
child: TextField(
controller: controller,
onChanged: onChanged,
style: TextStyle(color: textColor, fontSize: 15),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: hintColor, fontSize: 15),
prefixIcon: Padding(
padding: const EdgeInsets.only(left: 10, right: 6),
child: Icon(Icons.search_rounded,
size: 18, color: iconColor),
),
prefixIconConstraints:
const BoxConstraints(minWidth: 36, minHeight: 36),
suffixIcon: controller.text.isNotEmpty
? GestureDetector(
onTap: () {
controller.clear();
onChanged('');
},
child: Padding(
padding: const EdgeInsets.only(right: 10),
child: Icon(Icons.close_rounded,
size: 16, color: iconColor),
),
)
: null,
suffixIconConstraints:
const BoxConstraints(minWidth: 32, minHeight: 36),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
isDense: true,
),
),
),
),
),
);
}
}
// ── Sort / filter bottom sheet ────────────────────────────────────────────────
Future<int?> showSortSheet(
BuildContext context, {
required String title,
required List<String> options,
required int current,
}) {
return showModalBottomSheet<int>(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => _SortSheet(
title: title,
options: options,
current: current,
),
);
}
class _SortSheet extends StatelessWidget {
const _SortSheet({
required this.title,
required this.options,
required this.current,
});
final String title;
final List<String> options;
final int current;
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
),
),
const SizedBox(height: 8),
for (int i = 0; i < options.length; i++)
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20),
title: Text(
options[i],
style: TextStyle(
color: i == current
? AppColors.primary
: AppColors.textPrimary,
fontWeight: i == current
? FontWeight.w600
: FontWeight.normal,
),
),
trailing: i == current
? const Icon(Icons.check_rounded,
color: AppColors.primary, size: 20)
: null,
onTap: () => Navigator.pop(context, i),
),
SizedBox(height: MediaQuery.paddingOf(context).bottom + 8),
],
),
);
}
}
+121
View File
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class PillTabs extends StatelessWidget {
const PillTabs({
super.key,
required this.tabs,
required this.selected,
required this.onSelect,
this.counts,
});
final List<String> tabs;
final int selected;
final ValueChanged<int> onSelect;
final List<int?>? counts;
@override
Widget build(BuildContext context) {
return Container(
color: AppColors.surface,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(16, 10, 16, 10),
child: Row(
children: [
for (int i = 0; i < tabs.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
_PillTab(
label: tabs[i],
count: counts != null && i < counts!.length ? counts![i] : null,
selected: selected == i,
onTap: () => onSelect(i),
),
],
],
),
),
const Divider(height: 1, thickness: 1, color: AppColors.border),
],
),
);
}
}
class _PillTab extends StatelessWidget {
const _PillTab({
required this.label,
required this.selected,
required this.onTap,
this.count,
});
final String label;
final bool selected;
final VoidCallback onTap;
final int? count;
@override
Widget build(BuildContext context) {
return Semantics(
label: label,
button: true,
excludeSemantics: true,
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: selected ? AppColors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected ? AppColors.primary : AppColors.border,
width: 1.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
color: selected ? Colors.white : AppColors.textSecondary,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
if (count != null) ...[
const SizedBox(width: 6),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: selected
? Colors.white.withValues(alpha: 0.25)
: AppColors.inProgressBg,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$count',
style: TextStyle(
color: selected ? Colors.white : AppColors.inProgress,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
),
],
],
),
),
),
);
}
}
+104
View File
@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
/// Renders the DLS brand logo — navy tooth + cyan chevrons.
///
/// [color] null → brand colors (#00397C tooth + #57B8CE chevrons).
/// Pass a color (e.g. Colors.white) for monochrome override on dark backgrounds.
class ToothLogo extends StatelessWidget {
const ToothLogo({
super.key,
required this.size,
this.color,
});
final double size;
final Color? color;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size * 1.9,
height: size,
child: CustomPaint(
painter: _DlsLogoPainter(color: color),
),
);
}
}
class _DlsLogoPainter extends CustomPainter {
const _DlsLogoPainter({this.color});
final Color? color;
static const _navy = Color(0xFF00397C);
static const _cyan = Color(0xFF57B8CE);
@override
void paint(Canvas canvas, Size size) {
final toothColor = color ?? _navy;
final chevronColor = color ?? _cyan;
// Content bounding box in SVG 200×200 space: x=[42.5..157.5], y=[72..133]
// Width=115, Height=61 → aspect ~1.885 ≈ widget aspect 1.9
const svgLeft = 42.5, svgTop = 72.0, svgWidth = 115.0, svgHeight = 61.0;
final s = size.height / svgHeight;
final dx = (size.width - svgWidth * s) / 2.0 - svgLeft * s;
final dy = (size.height - svgHeight * s) / 2.0 - svgTop * s;
canvas.translate(dx, dy);
canvas.scale(s);
_drawTooth(canvas, toothColor);
_drawChevrons(canvas, chevronColor);
}
static void _drawTooth(Canvas canvas, Color color) {
// SVG path with scale(0.58) + translate(100,100) applied inline.
const cx = 100.0, cy = 100.0, sc = 0.58;
double px(double v) => cx + v * sc;
double py(double v) => cy + v * sc;
final path = Path()
..moveTo(px(0), py(-46))
..cubicTo(px(-22), py(-50), px(-44), py(-38), px(-44), py(-12))
..cubicTo(px(-44), py(8), px(-34), py(32), px(-26), py(46))
..cubicTo(px(-20), py(57), px(-11), py(53), px(-8), py(33))
..cubicTo(px(-6), py(19), px(-2), py(17), px(0), py(17))
..cubicTo(px(2), py(17), px(6), py(19), px(8), py(33))
..cubicTo(px(11), py(53), px(20), py(57), px(26), py(46))
..cubicTo(px(34), py(32), px(44), py(8), px(44), py(-12))
..cubicTo(px(44), py(-38), px(22), py(-50), px(0), py(-46))
..close();
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.fill);
}
static void _drawChevrons(Canvas canvas, Color color) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 11.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
// Polyline points + translate(100,100). Left: (-52,-22)→(-34,0)→(-52,22)
canvas.drawPath(
Path()
..moveTo(48, 78)
..lineTo(66, 100)
..lineTo(48, 122),
paint,
);
// Right: (52,-22)→(34,0)→(52,22)
canvas.drawPath(
Path()
..moveTo(152, 78)
..lineTo(134, 100)
..lineTo(152, 122),
paint,
);
}
@override
bool shouldRepaint(_DlsLogoPainter old) => old.color != color;
}
+104
View File
@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// Animated floating blob background used on auth screens.
/// [bright] = true → white blobs (for dark/gradient backgrounds).
/// [bright] = false → primary/accent blobs (for light backgrounds).
class AnimatedAuthBg extends StatefulWidget {
const AnimatedAuthBg({super.key, this.bright = false});
final bool bright;
@override
State<AnimatedAuthBg> createState() => _AnimatedAuthBgState();
}
class _AnimatedAuthBgState extends State<AnimatedAuthBg>
with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
late Animation<double> _anim;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: const Duration(seconds: 8),
)..repeat(reverse: true);
_anim = CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut);
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
Color _blob(double alpha) => widget.bright
? Colors.white.withValues(alpha: alpha * 1.5)
: AppColors.primary.withValues(alpha: alpha);
Color _blobAccent(double alpha) => widget.bright
? Colors.white.withValues(alpha: alpha * 1.2)
: AppColors.accent.withValues(alpha: alpha);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _anim,
builder: (_, __) {
final t = _anim.value;
return Stack(
children: [
Positioned(
top: -80 + t * 30,
left: -60 + t * 20,
child: AuthBlob(size: 300, color: _blob(0.08)),
),
Positioned(
top: 200 - t * 40,
right: -100 + t * 25,
child: AuthBlob(size: 250, color: _blobAccent(0.06)),
),
Positioned(
bottom: 100 + t * 30,
left: 50 - t * 15,
child: AuthBlob(size: 200, color: _blob(0.05)),
),
Positioned(
bottom: -50 + t * 20,
right: -50 + t * 10,
child: AuthBlob(size: 280, color: _blobAccent(0.07)),
),
Positioned(
top: 350 + t * 25,
left: 80 + t * 20,
child: AuthBlob(size: 160, color: _blob(0.04)),
),
Positioned(
top: -40 - t * 10,
left: 120 + t * 30,
child: AuthBlob(size: 180, color: _blobAccent(0.05)),
),
],
);
},
);
}
}
/// A simple solid circle used as a background blob.
class AuthBlob extends StatelessWidget {
const AuthBlob({super.key, required this.size, required this.color});
final double size;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
);
}
}
@@ -0,0 +1,32 @@
import 'package:pocketbase/pocketbase.dart';
import '../../core/api/pocketbase_client.dart';
import '../../core/auth/auth_repository.dart';
class OnboardingRepository {
OnboardingRepository._();
static final instance = OnboardingRepository._();
PocketBase get _pb => PocketBaseClient.instance.pb;
Future<AuthResult> createTenantAndJoin({
required String kind,
required String companyName,
}) async {
final userId = _pb.authStore.record!.id;
final tenant = await _pb.collection('tenants').create(body: {
'kind': kind,
'company_name': companyName,
'status': 'active',
'default_currency': 'TRY',
});
await _pb.collection('tenant_members').create(body: {
'tenant_id': tenant.id,
'user_id': userId,
'role': 'owner',
});
return AuthRepository.instance.refreshSession();
}
}
+461
View File
@@ -0,0 +1,461 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/auth_provider.dart';
import 'onboarding_repository.dart';
class OnboardingScreen extends ConsumerStatefulWidget {
const OnboardingScreen({super.key});
@override
ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _nameCtrl = TextEditingController();
String _selectedKind = 'clinic';
bool _loading = false;
String? _error;
late AnimationController _animCtrl;
late Animation<double> _fadeAnim;
late Animation<Offset> _slideAnim;
@override
void initState() {
super.initState();
_animCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut);
_slideAnim = Tween<Offset>(
begin: const Offset(0, 0.08),
end: Offset.zero,
).animate(CurvedAnimation(parent: _animCtrl, curve: Curves.easeOutCubic));
_animCtrl.forward();
}
@override
void dispose() {
_animCtrl.dispose();
_nameCtrl.dispose();
super.dispose();
}
Future<void> _create() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
try {
final result = await OnboardingRepository.instance.createTenantAndJoin(
kind: _selectedKind,
companyName: _nameCtrl.text.trim(),
);
if (!mounted) return;
ref.read(authProvider.notifier).setActiveTenant(result.tenants.first);
} catch (e) {
setState(() {
_error = 'Hesap oluşturulamadı. Lütfen tekrar deneyin.';
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final size = MediaQuery.sizeOf(context);
return Scaffold(
backgroundColor: const Color(0xFF4F46E5),
body: Stack(
children: [
// ── Gradient background ──────────────────────────────────────────
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomCenter,
colors: [Color(0xFF3730A3), Color(0xFF6366F1)],
),
),
),
// ── Decorative circles ───────────────────────────────────────────
Positioned(
top: -40,
right: -60,
child: Container(
width: 220,
height: 220,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.06),
),
),
),
Positioned(
top: 80,
left: -70,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.04),
),
),
),
// ── Content ──────────────────────────────────────────────────────
Column(
children: [
// Header
SafeArea(
bottom: false,
child: SizedBox(
height: size.height * 0.26,
child: Stack(
children: [
// Sign out
Positioned(
right: 8,
top: 4,
child: TextButton.icon(
onPressed: () =>
ref.read(authProvider.notifier).signOut(),
icon: const Icon(Icons.logout_rounded,
color: Colors.white70, size: 18),
label: const Text(
'Çıkış',
style: TextStyle(color: Colors.white70),
),
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 68,
height: 68,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1.5,
),
),
child: const Icon(
Icons.domain_add_rounded,
size: 32,
color: Colors.white,
),
),
const SizedBox(height: 14),
const Text(
'Kurumunuzu Oluşturun',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
const SizedBox(height: 4),
Text(
'Klinik veya laboratuvar olarak kayıt olun',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.70),
fontSize: 13,
),
),
],
),
),
],
),
),
),
// Form card
Expanded(
child: FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(
position: _slideAnim,
child: Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(32),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 24,
offset: const Offset(0, -4),
),
],
),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(28, 32, 28, 24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Kurum Türünü Seçin',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 14),
// Kind cards
Row(
children: [
Expanded(
child: _KindCard(
icon: Icons.local_hospital_outlined,
label: 'Klinik',
description: 'Diş kliniği',
value: 'clinic',
selected: _selectedKind == 'clinic',
onTap: () => setState(
() => _selectedKind = 'clinic'),
),
),
const SizedBox(width: 12),
Expanded(
child: _KindCard(
icon: Icons.science_outlined,
label: 'Laboratuvar',
description: 'Diş laboratuvarı',
value: 'lab',
selected: _selectedKind == 'lab',
onTap: () =>
setState(() => _selectedKind = 'lab'),
),
),
],
),
const SizedBox(height: 24),
// Company name
Text(
'Kurum Adı',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
TextFormField(
controller: _nameCtrl,
textInputAction: TextInputAction.done,
textCapitalization: TextCapitalization.words,
onFieldSubmitted: (_) => _create(),
decoration: InputDecoration(
labelText: _selectedKind == 'clinic'
? 'Klinik Adı'
: 'Laboratuvar Adı',
prefixIcon: const Icon(
Icons.business_outlined,
size: 20),
filled: true,
fillColor: cs.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFF4F46E5), width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(
color: cs.error, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide:
BorderSide(color: cs.error, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 16),
),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Kurum adı gereklidir';
}
if (v.trim().length < 3) {
return 'En az 3 karakter olmalıdır';
}
return null;
},
),
// Error banner
if (_error != null) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: cs.errorContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.error_outline_rounded,
color: cs.onErrorContainer, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: TextStyle(
color: cs.onErrorContainer,
fontSize: 13),
),
),
],
),
),
],
const SizedBox(height: 28),
FilledButton(
onPressed: _loading ? null : _create,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
backgroundColor: const Color(0xFF4F46E5),
),
child: _loading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Devam Et',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
),
),
),
),
],
),
],
),
);
}
}
class _KindCard extends StatelessWidget {
const _KindCard({
required this.icon,
required this.label,
required this.description,
required this.value,
required this.selected,
required this.onTap,
});
final IconData icon;
final String label;
final String description;
final String value;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selected ? const Color(0xFF4F46E5) : cs.outlineVariant,
width: selected ? 2 : 1,
),
color: selected
? const Color(0xFF4F46E5).withValues(alpha: 0.08)
: cs.surfaceContainerLow,
),
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: selected
? const Color(0xFF4F46E5).withValues(alpha: 0.12)
: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 26,
color: selected
? const Color(0xFF4F46E5)
: cs.onSurfaceVariant,
),
),
const SizedBox(height: 10),
Text(
label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: selected ? const Color(0xFF4F46E5) : cs.onSurface,
),
),
const SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 11,
color: cs.onSurfaceVariant,
),
),
],
),
),
);
}
}
+888
View File
@@ -0,0 +1,888 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/l10n/app_strings.dart';
import '../../core/providers/auth_provider.dart';
import '../../core/providers/locale_provider.dart';
import '../../core/router/app_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/tooth_logo.dart';
class SignInScreen extends ConsumerStatefulWidget {
const SignInScreen({super.key});
@override
ConsumerState<SignInScreen> createState() => _SignInScreenState();
}
class _SignInScreenState extends ConsumerState<SignInScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
bool _obscure = true;
@override
void dispose() {
_emailCtrl.dispose();
_passCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
await ref
.read(authProvider.notifier)
.signIn(_emailCtrl.text.trim(), _passCtrl.text);
}
@override
Widget build(BuildContext context) {
final auth = ref.watch(authProvider);
final s = ref.watch(stringsProvider);
final locale = ref.watch(localeProvider);
final isDesktop = MediaQuery.sizeOf(context).width > 800;
return Scaffold(
backgroundColor: AppColors.background,
body: isDesktop
? _buildDesktop(context, auth, s, locale)
: _buildMobile(context, auth, s, locale),
);
}
// ── Mobile ─────────────────────────────────────────────────────────────────
Widget _buildMobile(
BuildContext context, dynamic auth, AppStrings s, Locale locale) {
return Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 56),
// Logo mark
Center(
child: Container(
width: 68,
height: 68,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF0B1D35), Color(0xFF1A5C8A)],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color(0xFF0B1D35).withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child:
const Center(child: ToothLogo(size: 34, color: Colors.white)),
),
).animate().fadeIn(duration: 400.ms).scale(begin: const Offset(0.8, 0.8)),
const SizedBox(height: 24),
Center(
child: Text(
s.signInWelcome,
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
letterSpacing: -0.5,
),
),
).animate(delay: 60.ms).fadeIn(duration: 400.ms).slideY(begin: 0.1),
const SizedBox(height: 6),
Center(
child: Text(
s.signInSubtitle,
style: const TextStyle(
fontSize: 14, color: AppColors.textSecondary),
),
).animate(delay: 100.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 36),
_buildFormFields(auth, s),
const SizedBox(height: 24),
_buildSignUpLink(context, s),
const SizedBox(height: 32),
],
),
),
),
Positioned(
top: MediaQuery.paddingOf(context).top + 12,
right: 12,
child: _LanguageButton(locale: locale, s: s, ref: ref),
),
],
);
}
// ── Desktop ────────────────────────────────────────────────────────────────
Widget _buildDesktop(
BuildContext context, dynamic auth, AppStrings s, Locale locale) {
return Row(
children: [
// LEFT PANEL
Expanded(
flex: 55,
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.55, 1.0],
colors: [
Color(0xFF080F1E),
Color(0xFF0D2D58),
Color(0xFF0E4A82),
],
),
),
),
const Positioned(top: -140, left: -140, child: _Ring(size: 520, opacity: 0.06)),
const Positioned(bottom: -100, right: -100, child: _Ring(size: 400, opacity: 0.05)),
const Positioned(top: 160, right: 60, child: _Ring(size: 100, opacity: 0.09)),
const Positioned(bottom: 220, left: 60, child: _Ring(size: 70, opacity: 0.07)),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 64, vertical: 52),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
),
),
child: const Center(
child: ToothLogo(size: 20, color: Colors.white)),
),
const SizedBox(width: 12),
const Text(
'DLS',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w800,
letterSpacing: 1.5,
),
),
],
).animate().fadeIn(duration: 500.ms),
const Spacer(),
Text(
s.signInHeadline,
style: const TextStyle(
color: Colors.white,
fontSize: 46,
fontWeight: FontWeight.w800,
height: 1.1,
letterSpacing: -1.0,
),
)
.animate(delay: 100.ms)
.fadeIn(duration: 500.ms)
.slideY(begin: 0.1, end: 0),
const SizedBox(height: 18),
Text(
s.signInTagline,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 16,
height: 1.6,
),
).animate(delay: 160.ms).fadeIn(duration: 500.ms),
const SizedBox(height: 44),
const _DashboardPreviewCard()
.animate(delay: 220.ms)
.fadeIn(duration: 600.ms)
.slideY(begin: 0.12, end: 0),
const Spacer(),
Text(
s.footerCopyright,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.3),
fontSize: 12,
),
).animate(delay: 300.ms).fadeIn(duration: 500.ms),
],
),
),
],
),
),
// RIGHT PANEL
Stack(
children: [
Container(
width: 460,
color: Colors.white,
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 52, vertical: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0B1D35),
Color(0xFF1A5C8A)
],
),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: ToothLogo(
size: 24, color: Colors.white)),
).animate().fadeIn(duration: 400.ms),
const SizedBox(height: 32),
Text(
s.signInWelcome,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
letterSpacing: -0.5,
),
)
.animate(delay: 60.ms)
.fadeIn(duration: 400.ms)
.slideY(begin: 0.08, end: 0),
const SizedBox(height: 6),
Text(
s.signInSubtitle,
style: const TextStyle(
fontSize: 15,
color: AppColors.textSecondary,
),
).animate(delay: 100.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 40),
_buildFormFields(auth, s)
.animate(delay: 140.ms)
.fadeIn(duration: 400.ms)
.slideY(begin: 0.08, end: 0),
const SizedBox(height: 28),
_buildSignUpLink(context, s)
.animate(delay: 200.ms)
.fadeIn(duration: 400.ms),
],
),
),
),
),
),
),
),
),
Positioned(
top: MediaQuery.paddingOf(context).top + 16,
right: 16,
child: _LanguageButton(locale: locale, s: s, ref: ref),
),
],
),
],
);
}
// ── Form fields (shared) ────────────────────────────────────────────────────
Widget _buildFormFields(dynamic auth, AppStrings s) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Field(
controller: _emailCtrl,
label: s.emailAddress,
icon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (v) =>
(v == null || v.trim().isEmpty) ? s.emailRequired : null,
),
const SizedBox(height: 14),
_Field(
controller: _passCtrl,
label: s.password,
icon: Icons.lock_outline_rounded,
obscureText: _obscure,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
suffixIcon: IconButton(
icon: Icon(
_obscure
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
size: 20,
color: AppColors.textSecondary,
),
onPressed: () => setState(() => _obscure = !_obscure),
),
validator: (v) =>
(v == null || v.isEmpty) ? s.passwordRequired : null,
),
if (auth.error != null) ...[
const SizedBox(height: 14),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.cancelled.withValues(alpha: 0.25)),
),
child: Row(
children: [
const Icon(Icons.error_outline_rounded,
color: AppColors.cancelled, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
auth.error!,
style: const TextStyle(
color: AppColors.cancelled, fontSize: 13),
),
),
],
),
),
],
const SizedBox(height: 24),
DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0B1D35), Color(0xFF1A5C8A)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0xFF0B1D35).withValues(alpha: 0.35),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: FilledButton(
onPressed: auth.isLoading ? null : _submit,
style: FilledButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white.withValues(alpha: 0.5),
disabledBackgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: auth.isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5, color: Colors.white),
)
: Text(
s.signIn,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600),
),
),
),
],
),
);
}
// ── Sign-up link ───────────────────────────────────────────────────────────
Widget _buildSignUpLink(BuildContext context, AppStrings s) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
s.noAccount,
style:
const TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
TextButton(
onPressed: () => context.go(routeSignUp),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF0D4C85),
padding: const EdgeInsets.symmetric(horizontal: 8),
),
child: Text(
s.signUp,
style:
const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
),
),
],
);
}
}
// ── Language button ───────────────────────────────────────────────────────────
class _LanguageButton extends StatelessWidget {
const _LanguageButton(
{required this.locale, required this.s, required this.ref});
final Locale locale;
final AppStrings s;
final WidgetRef ref;
static const _flags = {
'tr': '🇹🇷',
'en': '🇬🇧',
'ru': '🇷🇺',
'ar': '🇸🇦',
'de': '🇩🇪',
};
@override
Widget build(BuildContext context) {
final flag = _flags[locale.languageCode] ?? '🌐';
return GestureDetector(
onTap: () => _showPicker(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(flag, style: const TextStyle(fontSize: 15)),
const SizedBox(width: 4),
const Icon(Icons.expand_more_rounded,
size: 14, color: AppColors.textSecondary),
],
),
),
);
}
void _showPicker(BuildContext context) {
final options = [
('tr', '🇹🇷', s.languageTurkish),
('en', '🇬🇧', s.languageEnglish),
('ru', '🇷🇺', s.languageRussian),
('ar', '🇸🇦', s.languageArabic),
('de', '🇩🇪', s.languageGerman),
];
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) => Container(
decoration: const BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Text(
s.languageSelection,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 12),
for (final (code, flag, label) in options)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
leading: Text(flag, style: const TextStyle(fontSize: 24)),
title: Text(
label,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
trailing: locale.languageCode == code
? const Icon(Icons.check_circle_rounded,
color: AppColors.accent)
: null,
onTap: () {
ref.read(localeProvider.notifier).setLocale(Locale(code));
Navigator.pop(context);
},
),
SizedBox(height: MediaQuery.paddingOf(context).bottom + 4),
],
),
),
);
}
}
// ── Decorative ring ───────────────────────────────────────────────────────────
class _Ring extends StatelessWidget {
const _Ring({required this.size, required this.opacity});
final double size;
final double opacity;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: opacity),
width: 1.5,
),
),
);
}
}
// ── Dashboard preview card (glassmorphism) ────────────────────────────────────
class _DashboardPreviewCard extends StatelessWidget {
const _DashboardPreviewCard();
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
width: 340,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.12),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.bar_chart_rounded,
color: Colors.white,
size: 15,
),
),
const SizedBox(width: 10),
Text(
'Bugünkü Durum',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
const Spacer(),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Canlı',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 18),
const Row(
children: [
_StatChip(value: '24', label: 'Aktif', color: Color(0xFF60A5FA)),
SizedBox(width: 8),
_StatChip(
value: '8', label: 'Bekliyor', color: Color(0xFFFBBF24)),
SizedBox(width: 8),
_StatChip(
value: '142', label: 'Bu ay', color: Color(0xFF34D399)),
],
),
const SizedBox(height: 18),
const _PreviewBar(
label: 'Zirkon', value: 0.76, color: Color(0xFF60A5FA)),
const SizedBox(height: 10),
const _PreviewBar(
label: 'Metal alt.', value: 0.48, color: Color(0xFFFBBF24)),
const SizedBox(height: 10),
const _PreviewBar(
label: 'Porselen', value: 0.62, color: Color(0xFF34D399)),
],
),
),
),
);
}
}
class _StatChip extends StatelessWidget {
const _StatChip({
required this.value,
required this.label,
required this.color,
});
final String value;
final String label;
final Color color;
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withValues(alpha: 0.2)),
),
child: Column(
children: [
Text(
value,
style: TextStyle(
color: color,
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 11,
),
),
],
),
),
);
}
}
class _PreviewBar extends StatelessWidget {
const _PreviewBar({
required this.label,
required this.value,
required this.color,
});
final String label;
final double value;
final Color color;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 12,
),
),
Text(
'${(value * 100).toInt()}%',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 5),
LayoutBuilder(
builder: (_, constraints) => Stack(
children: [
Container(
height: 5,
width: constraints.maxWidth,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(10),
),
),
Container(
height: 5,
width: constraints.maxWidth * value,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(10),
),
),
],
),
),
],
);
}
}
// ── Form field ────────────────────────────────────────────────────────────────
class _Field extends StatelessWidget {
const _Field({
required this.controller,
required this.label,
required this.icon,
this.keyboardType,
this.textInputAction,
this.obscureText = false,
this.suffixIcon,
this.onFieldSubmitted,
this.validator,
});
final TextEditingController controller;
final String label;
final IconData icon;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final bool obscureText;
final Widget? suffixIcon;
final ValueChanged<String>? onFieldSubmitted;
final FormFieldValidator<String>? validator;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
textInputAction: textInputAction,
obscureText: obscureText,
onFieldSubmitted: onFieldSubmitted,
validator: validator,
style: const TextStyle(fontSize: 15, color: AppColors.textPrimary),
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon, size: 20, color: AppColors.textSecondary),
suffixIcon: suffixIcon,
filled: true,
fillColor: const Color(0xFFF8FAFC),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF0D4C85), width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
const BorderSide(color: AppColors.cancelled, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 2),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: const TextStyle(
color: AppColors.textSecondary, fontSize: 14),
),
);
}
}
+619
View File
@@ -0,0 +1,619 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/providers/auth_provider.dart';
import '../../core/router/app_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/tooth_logo.dart';
import 'auth_widgets.dart';
class SignUpScreen extends ConsumerStatefulWidget {
const SignUpScreen({super.key});
@override
ConsumerState<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends ConsumerState<SignUpScreen> {
final _formKey = GlobalKey<FormState>();
final _firstNameCtrl = TextEditingController();
final _lastNameCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
final _confirmPassCtrl = TextEditingController();
bool _obscure = true;
bool _obscureConfirm = true;
bool _loading = false;
String? _error;
@override
void dispose() {
_firstNameCtrl.dispose();
_lastNameCtrl.dispose();
_emailCtrl.dispose();
_passCtrl.dispose();
_confirmPassCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
try {
await ref.read(authProvider.notifier).register(
email: _emailCtrl.text.trim(),
password: _passCtrl.text,
firstName: _firstNameCtrl.text.trim(),
lastName: _lastNameCtrl.text.trim(),
);
} catch (e) {
setState(() {
_error = _parseError(e.toString());
_loading = false;
});
}
}
String _parseError(String msg) {
if (msg.contains('already') || msg.contains('unique') || msg.contains('UNIQUE')) {
return 'Bu e-posta adresi zaten kayıtlı.';
}
if (msg.contains('403') || msg.contains('Forbidden')) {
return 'Kayıt şu anda kapalı. Lütfen yönetici ile iletişime geçin.';
}
return 'Kayıt olunamadı. Lütfen tekrar deneyin.';
}
@override
Widget build(BuildContext context) {
final isDesktop = MediaQuery.sizeOf(context).width > 800;
return Scaffold(
backgroundColor: AppColors.background,
body: isDesktop ? _buildDesktop(context) : _buildMobile(context),
);
}
// ── Mobile layout ──────────────────────────────────────────────────────────
Widget _buildMobile(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: _buildForm(context, isMobile: true),
),
);
}
// ── Desktop layout ─────────────────────────────────────────────────────────
Widget _buildDesktop(BuildContext context) {
return Row(
children: [
// LEFT PANEL — solid gradient + white animated blobs on top
Expanded(
flex: 5,
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primary, Color(0xFF1A5C8A)],
),
),
),
const AnimatedAuthBg(bright: true),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 56),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1.5,
),
),
child: const Center(child: ToothLogo(size: 38, color: Colors.white)),
),
const SizedBox(height: 24),
const Text(
'DLS',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 2,
),
),
const SizedBox(height: 4),
Text(
'Dental Lab Sistemi',
style: TextStyle(
fontSize: 17,
color: Colors.white.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 48),
const _FeatureBullet(
icon: Icons.dashboard_rounded,
text: 'İş takibi tek ekranda',
),
const SizedBox(height: 16),
const _FeatureBullet(
icon: Icons.link_rounded,
text: 'Klinik-lab bağlantısı',
),
const SizedBox(height: 16),
const _FeatureBullet(
icon: Icons.bolt_rounded,
text: 'Gerçek zamanlı durum',
),
],
),
),
),
],
),
),
// RIGHT PANEL — light gray so white card stands out
Container(
width: 480,
color: AppColors.background,
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [_buildForm(context, isMobile: false)],
),
),
),
),
),
),
),
),
],
);
}
// ── Shared form content ────────────────────────────────────────────────────
Widget _buildForm(BuildContext context, {required bool isMobile}) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isMobile) const SizedBox(height: 48),
// ── Back button + branding (mobile only) ───────────────────────
if (isMobile) ...[
Row(
children: [
IconButton(
onPressed: () => context.go(routeSignIn),
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
style: IconButton.styleFrom(
foregroundColor: AppColors.textPrimary,
backgroundColor: AppColors.surface,
padding: const EdgeInsets.all(10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppColors.border),
),
),
),
],
).animate().fadeIn(duration: 300.ms),
const SizedBox(height: 28),
Center(
child: Column(
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primary, AppColors.accent],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.accent.withValues(alpha: 0.3),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
),
child: const Icon(
Icons.person_add_alt_1_rounded,
size: 32,
color: Colors.white,
),
),
const SizedBox(height: 16),
const Text(
'Hesap Oluştur',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
const Text(
'DLS ağına katılın',
style: TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
).animate().fadeIn(duration: 400.ms).slideY(begin: -0.08, end: 0),
const SizedBox(height: 32),
],
// Desktop back button (outside card)
if (!isMobile) ...[
Row(
children: [
IconButton(
onPressed: () => context.go(routeSignIn),
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 18),
style: IconButton.styleFrom(
foregroundColor: AppColors.textPrimary,
backgroundColor: AppColors.surface,
padding: const EdgeInsets.all(8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: AppColors.border),
),
),
),
],
).animate().fadeIn(duration: 300.ms),
const SizedBox(height: 20),
],
// ── Form card ──────────────────────────────────────────────────
Container(
padding: EdgeInsets.all(isMobile ? 24 : 32),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isMobile ? 0.05 : 0.09),
blurRadius: isMobile ? 16 : 28,
spreadRadius: 0,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Heading inside card on desktop
if (!isMobile) ...[
const Text(
'Hesap Oluştur',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
).animate().fadeIn(duration: 400.ms).slideY(begin: -0.08, end: 0),
const SizedBox(height: 4),
const Text(
'DLS ağına katılın',
style: TextStyle(fontSize: 14, color: AppColors.textSecondary),
).animate(delay: 40.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 24),
],
// Ad / Soyad satırı
Row(
children: [
Expanded(
child: _Field(
controller: _firstNameCtrl,
label: 'Ad',
icon: Icons.badge_outlined,
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Gerekli' : null,
),
),
const SizedBox(width: 12),
Expanded(
child: _Field(
controller: _lastNameCtrl,
label: 'Soyad',
icon: Icons.badge_outlined,
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Gerekli' : null,
),
),
],
),
const SizedBox(height: 12),
_Field(
controller: _emailCtrl,
label: 'E-posta',
icon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'E-posta gereklidir';
if (!v.contains('@')) return 'Geçerli bir e-posta girin';
return null;
},
),
const SizedBox(height: 12),
_Field(
controller: _passCtrl,
label: 'Şifre',
icon: Icons.lock_outline_rounded,
obscureText: _obscure,
textInputAction: TextInputAction.next,
suffixIcon: IconButton(
icon: Icon(
_obscure ? Icons.visibility_outlined : Icons.visibility_off_outlined,
size: 20,
color: AppColors.textSecondary,
),
onPressed: () => setState(() => _obscure = !_obscure),
),
validator: (v) {
if (v == null || v.isEmpty) return 'Şifre gereklidir';
if (v.length < 8) return 'En az 8 karakter olmalıdır';
return null;
},
),
const SizedBox(height: 12),
_Field(
controller: _confirmPassCtrl,
label: 'Şifre Tekrar',
icon: Icons.lock_outline_rounded,
obscureText: _obscureConfirm,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirm
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
size: 20,
color: AppColors.textSecondary,
),
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
),
validator: (v) =>
(v != _passCtrl.text) ? 'Şifreler eşleşmiyor' : null,
),
if (_error != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppColors.cancelledBg,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.cancelled.withValues(alpha: 0.3)),
),
child: Row(
children: [
const Icon(Icons.error_outline_rounded,
color: AppColors.cancelled, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: const TextStyle(
color: AppColors.cancelled, fontSize: 13),
),
),
],
),
),
],
const SizedBox(height: 20),
FilledButton(
onPressed: _loading ? null : _submit,
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: _loading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5, color: Colors.white),
)
: const Text(
'Kayıt Ol',
style:
TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
],
),
).animate(delay: 100.ms).fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Zaten hesabın var mı?',
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
TextButton(
onPressed: () => context.go(routeSignIn),
style: TextButton.styleFrom(
foregroundColor: AppColors.accent,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
child: const Text(
'Giriş Yap',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
),
],
).animate(delay: 200.ms).fadeIn(duration: 400.ms),
SizedBox(height: isMobile ? 32 : 16),
],
),
);
}
}
// ── Feature bullet (desktop left panel) ──────────────────────────────────────
class _FeatureBullet extends StatelessWidget {
const _FeatureBullet({required this.icon, required this.text});
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 18, color: Colors.white),
),
const SizedBox(width: 14),
Text(
text,
style: TextStyle(
fontSize: 15,
color: Colors.white.withValues(alpha: 0.9),
fontWeight: FontWeight.w500,
),
),
],
);
}
}
// ── Form field ────────────────────────────────────────────────────────────────
class _Field extends StatelessWidget {
const _Field({
required this.controller,
required this.label,
required this.icon,
this.keyboardType,
this.textCapitalization = TextCapitalization.none,
this.textInputAction,
this.obscureText = false,
this.suffixIcon,
this.onFieldSubmitted,
this.validator,
});
final TextEditingController controller;
final String label;
final IconData icon;
final TextInputType? keyboardType;
final TextCapitalization textCapitalization;
final TextInputAction? textInputAction;
final bool obscureText;
final Widget? suffixIcon;
final ValueChanged<String>? onFieldSubmitted;
final FormFieldValidator<String>? validator;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
textCapitalization: textCapitalization,
textInputAction: textInputAction,
obscureText: obscureText,
onFieldSubmitted: onFieldSubmitted,
validator: validator,
style: const TextStyle(fontSize: 15, color: AppColors.textPrimary),
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon, size: 20, color: AppColors.textSecondary),
suffixIcon: suffixIcon,
filled: true,
fillColor: AppColors.background,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.accent, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More