diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml
new file mode 100644
index 0000000..5cc5eb8
--- /dev/null
+++ b/.idea/libraries/Dart_SDK.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml
new file mode 100644
index 0000000..2b96ac4
--- /dev/null
+++ b/.idea/libraries/KotlinJavaRuntime.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..cf15b80
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/.idea/runConfigurations/main_dart.xml b/.idea/runConfigurations/main_dart.xml
new file mode 100644
index 0000000..aab7b5c
--- /dev/null
+++ b/.idea/runConfigurations/main_dart.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..5b3388c
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..006d73b
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,30 @@
+# 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: "fcf2c11572af6f390246c056bc905eca609533a0"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: fcf2c11572af6f390246c056bc905eca609533a0
+ base_revision: fcf2c11572af6f390246c056bc905eca609533a0
+ - platform: web
+ create_revision: fcf2c11572af6f390246c056bc905eca609533a0
+ base_revision: fcf2c11572af6f390246c056bc905eca609533a0
+
+ # 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'
diff --git a/.vscode/Region.code-snippets b/.vscode/Region.code-snippets
new file mode 100644
index 0000000..a4b8c5a
--- /dev/null
+++ b/.vscode/Region.code-snippets
@@ -0,0 +1,24 @@
+{
+ "Region: Проверено": {
+ "prefix": "region-done",
+ "body": [
+ "// region 🟢 Проверено: $1",
+ "",
+ "$2",
+ "",
+ "// endregion"
+ ],
+ "description": "Сворачиваемый блок: проверено"
+ },
+ "Region: Не проверено": {
+ "prefix": "region-pending",
+ "body": [
+ "// region 🔴 Не проверено: $1",
+ "",
+ "$2",
+ "",
+ "// endregion"
+ ],
+ "description": "Сворачиваемый блок: не проверено"
+ }
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..b09be31
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "files.associations": {
+ "*.ejs": "html",
+ "vector": "cpp"
+ },
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..f9b3034
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flutter_lints/flutter.yaml
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/android/.gitignore
@@ -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
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 0000000..e5eaf3b
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+ id("com.google.gms.google-services")
+}
+
+dependencies {
+ implementation(platform("com.google.firebase:firebase-bom:34.8.0"))
+
+ implementation("com.google.firebase:firebase-auth")
+ implementation("com.google.firebase:firebase-firestore")
+ implementation("com.google.firebase:firebase-analytics") // опционально
+}
+
+android {
+ namespace = "com.autoritet.stocky"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = "27.0.12077973"
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.autoritet.stocky"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = 23
+ 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 = "../.."
+}
diff --git a/android/app/google-services.json b/android/app/google-services.json
new file mode 100644
index 0000000..4251d37
--- /dev/null
+++ b/android/app/google-services.json
@@ -0,0 +1,86 @@
+{
+ "project_info": {
+ "project_number": "785043562091",
+ "project_id": "apllicationswork",
+ "storage_bucket": "apllicationswork.firebasestorage.app"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:785043562091:android:359470686cf07bd27c5b2e",
+ "android_client_info": {
+ "package_name": "com.autorite.service"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBGgbGEs6fe9jJgyIWjsRaCmrifC4pjxQo"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:785043562091:android:acd0ecee72a5c6967c5b2e",
+ "android_client_info": {
+ "package_name": "com.autoritet.mechanic"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBGgbGEs6fe9jJgyIWjsRaCmrifC4pjxQo"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:785043562091:android:abc115731c662f297c5b2e",
+ "android_client_info": {
+ "package_name": "com.autoritet.stocky"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBGgbGEs6fe9jJgyIWjsRaCmrifC4pjxQo"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:785043562091:android:e49461a9466508b77c5b2e",
+ "android_client_info": {
+ "package_name": "com.example.receiver"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBGgbGEs6fe9jJgyIWjsRaCmrifC4pjxQo"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..56eeb34
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/autoritet/stocky/MainActivity.kt b/android/app/src/main/kotlin/com/autoritet/stocky/MainActivity.kt
new file mode 100644
index 0000000..f78cf02
--- /dev/null
+++ b/android/app/src/main/kotlin/com/autoritet/stocky/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.autoritet.stocky
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..89176ef
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,21 @@
+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("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/android/flutter_stocky_android.iml b/android/flutter_stocky_android.iml
new file mode 100644
index 0000000..1899969
--- /dev/null
+++ b/android/flutter_stocky_android.iml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..f018a61
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ac3b479
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -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.12-all.zip
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 0000000..bd7522f
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,28 @@
+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.7.3" apply false
+ // START: FlutterFire Configuration
+ id("com.google.gms.google-services") version("4.3.15") apply false
+ // END: FlutterFire Configuration
+ id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/firebase.json b/firebase.json
new file mode 100644
index 0000000..3b670fe
--- /dev/null
+++ b/firebase.json
@@ -0,0 +1 @@
+{"flutter":{"platforms":{"android":{"default":{"projectId":"apllicationswork","appId":"1:785043562091:android:abc115731c662f297c5b2e","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"apllicationswork","configurations":{"android":"1:785043562091:android:abc115731c662f297c5b2e","web":"1:785043562091:web:bae3c514d31ac7ce7c5b2e","windows":"1:785043562091:web:6f0beb2b421d6ba27c5b2e"}}}}}}
\ No newline at end of file
diff --git a/flutter_stocky.iml b/flutter_stocky.iml
new file mode 100644
index 0000000..f66303d
--- /dev/null
+++ b/flutter_stocky.iml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/Pages/controllers/AuthController.dart b/lib/Pages/controllers/AuthController.dart
new file mode 100644
index 0000000..bc927e3
--- /dev/null
+++ b/lib/Pages/controllers/AuthController.dart
@@ -0,0 +1,75 @@
+// lib/controllers/auth_controller.dart
+import 'package:Stocky/services/api/api_service.dart';
+import 'package:Stocky/services/local/StorageService.dart';
+
+import 'package:get/get.dart';
+
+class AuthController extends GetxController {
+ // Состояния
+ final _state = 'inputPhone'.obs;
+
+ final ApiService apicontr = Get.find();
+ Rx phoneNumber = ''.obs;
+ Rx verificationCode = ''.obs;
+
+ // Получаем текущее состояние
+ String get state => _state.value;
+
+ // Меняем состояние
+ void setState(String newState) {
+ _state.value = newState;
+ }
+
+ // Ввод номера
+ void phoneNumberInput(String value) {
+ phoneNumber.value = value;
+ }
+
+ // Проверка номера
+ bool get isPhoneNumberValid {
+ final clean = phoneNumber.value.replaceAll(RegExp(r'[^0-9]'), '');
+ return clean.length == 11 && clean.startsWith('7'); // или 10, если без +7
+ }
+
+ // Получить "чистый" номер (для отправки на сервер)
+ String get phoneNumberClean {
+ return phoneNumber.value.replaceAll(RegExp(r'[^0-9]'), '');
+ }
+
+ // Ввод кода
+ void verificationCodeInput(String value) {
+ verificationCode.value = value;
+ }
+
+ Future sendCode() async {
+ if (phoneNumber.value.isEmpty) {
+ Get.snackbar('Ошибка', 'Введите номер телефона');
+ return;
+ }
+ if (!isPhoneNumberValid) {
+ Get.snackbar('Ошибка', 'Неверный номер телефона');
+ return;
+ }
+ await Future.delayed(const Duration(seconds: 1));
+ setState('inputCode');
+ await apicontr.registerByPhone(phoneNumberClean);
+ }
+
+ // Подтвердить код
+ Future submit() async {
+ if (verificationCode.value.isEmpty) {
+ Get.snackbar('Ошибка', 'Введите код подтверждения');
+ return;
+ }
+ await Future.delayed(const Duration(seconds: 1));
+ final reg = await apicontr.loginByPhone(
+ phoneNumber.value,
+ verificationCode.value,
+ );
+ if (reg) {
+ Get.find().putNumber(phoneNumber.value);
+ Get.snackbar('Успешно', 'Вы вошли!');
+ Get.offAllNamed('/home');
+ }
+ }
+}
diff --git a/lib/Pages/controllers/MainController.dart b/lib/Pages/controllers/MainController.dart
new file mode 100644
index 0000000..8d60df8
--- /dev/null
+++ b/lib/Pages/controllers/MainController.dart
@@ -0,0 +1,233 @@
+import 'package:Stocky/models/task_adapter.dart';
+import 'package:Stocky/models/Documents.dart';
+import 'package:Stocky/services/api/api_service.dart';
+import 'package:Stocky/services/api/push_notification_service.dart';
+import 'package:Stocky/services/local/StorageService.dart';
+
+import 'package:get/get.dart';
+
+class MainController extends GetxController {
+ final username = ''.obs;
+
+ final tasks = [].obs;
+ final documents = [].obs;
+ final RxBool isLoading = false.obs;
+
+ final RxBool isTask = true.obs;
+ final RxBool isProj = true.obs;
+
+ bool _isSyncingTasks = false;
+
+ Future getSettings() async {
+ username.value = await Get.find().getUsername();
+ }
+
+ Future fetchTasks() async {
+ if (_isSyncingTasks) return;
+ _isSyncingTasks = true;
+
+ try {
+ isLoading.value = true;
+ final localTasks = Get.find().getTasks();
+
+ if (localTasks.isEmpty) {
+ await _fetchTasksFromServer();
+ } else {
+ await _syncTasksWithServer(localTasks);
+ }
+
+ final activeTasks = await Get.find()
+ .getCompletedTasks();
+
+ tasks.assignAll(activeTasks);
+ } catch (e) {
+ print('Ошибка при синхронизации задач: $e');
+ } finally {
+ _isSyncingTasks = false;
+ isLoading.value = false;
+ }
+ }
+
+ Future _syncTasksWithServer(List localTasks) async {
+ try {
+ // Шаг 1: Обрабатываем все несинхронизированные задачи
+ final unsyncedTasks = localTasks.where((t) => !t.isSynced).toList();
+ for (final task in unsyncedTasks) {
+ if (task.isDeletedLocally == true) {
+ // Удаление: пытаемся удалить на сервере
+ final deleted = await Get.find().deleteTask(
+ task.id.toString(),
+ );
+ if (deleted) {
+ Get.find().removeTask(task.id);
+ } else {
+ // Не удалось — оставляем помеченной, попробуем позже
+ continue;
+ }
+ } else {
+ // Создание или обновление
+ final syncedTask = await Get.find().upsertTask(task);
+ if (syncedTask != null) {
+ syncedTask.isSynced = true;
+ syncedTask.isDeletedLocally = false;
+ Get.find().updateTask(syncedTask);
+ }
+ // Если не удалось — остаётся с isSynced = false
+ }
+ }
+
+ // Шаг 2: Получаем актуальный список с сервера
+ final serverTasks = await Get.find().fetchTasks();
+ await Get.find().saveTasks(serverTasks);
+ } catch (e) {
+ print('Ошибка синхронизации: $e');
+ // В случае ошибки — восстанавливаем локальный список
+ tasks.assignAll(localTasks);
+ }
+ }
+
+ Future _fetchTasksFromServer() async {
+ try {
+ final serverTasks = await Get.find().fetchTasks();
+ await Get.find().saveTasks(serverTasks);
+ tasks.assignAll(serverTasks);
+ } catch (e) {
+ print('Ошибка загрузки задач с сервера: $e');
+ // Остаёмся с пустым списком
+ }
+ }
+
+ Future assignTaskToCurrentUser(Task task) async {
+ task.executor = username.value;
+ task.isSynced = false;
+ updateTask(task);
+ }
+
+ Future completeTask(Task task) async {
+ task.isCompleted = true;
+ task.endTime = DateTime.now();
+ task.isSynced = false;
+ updateTask(task);
+ final activeTasks = await Get.find()
+ .getCompletedTasks();
+
+ tasks.assignAll(activeTasks);
+ }
+
+ Future addTask(Task task) async {
+ task.isSynced = false;
+ task.isDeletedLocally = false;
+
+ Get.find().addTask(task);
+ tasks.add(task);
+
+ _syncTaskToServer(task);
+ }
+
+ Future updateTask(Task updatedTask) async {
+ final index = tasks.indexWhere((t) => t.id == updatedTask.id);
+ if (index == -1) return;
+
+ updatedTask.isSynced = false;
+ updatedTask.isDeletedLocally = false;
+
+ Get.find().updateTask(updatedTask);
+ tasks[index] = updatedTask;
+
+ _syncTaskToServer(updatedTask);
+ }
+
+ Future deleteTask(Task task) async {
+ final index = tasks.indexWhere((t) => t.id == task.id);
+ if (index == -1) return;
+
+ try {
+ final success = await Get.find().deleteTask(
+ task.id.toString(),
+ );
+ if (success) {
+ Get.find().removeTask(task.id);
+ tasks.removeAt(index);
+ } else {
+ // Нет связи — помечаем для отложенного удаления
+ _markTaskForDeletion(task);
+ }
+ } catch (e) {
+ print('Ошибка при удалении задачи: $e');
+ _markTaskForDeletion(task);
+ }
+ }
+
+ void _markTaskForDeletion(Task task) {
+ task.isDeletedLocally = true;
+ task.isSynced = false;
+ Get.find().updateTask(task);
+ }
+
+ Future _syncTaskToServer(Task task) async {
+ final storage = Get.find();
+
+ if (task.isDeletedLocally == true) {
+ final success = await Get.find().deleteTask(
+ task.id.toString(),
+ );
+ if (success) {
+ storage.removeTask(task.id);
+ tasks.removeWhere((t) => t.id == task.id);
+ } else {
+ // Оставить помеченной — будет обработано при полной синхронизации
+ }
+ return;
+ }
+
+ try {
+ final syncedTask = await Get.find().upsertTask(task);
+ if (syncedTask == null) return;
+
+ if (syncedTask.id != task.id) {
+ syncedTask.isSynced = true;
+ syncedTask.isDeletedLocally = false;
+ await storage.replaceTask(task.id, syncedTask);
+ } else {
+ task.isSynced = true;
+ task.isDeletedLocally = false;
+ await storage.updateTask(syncedTask);
+ }
+
+ final index = tasks.indexWhere((t) => t.id == task.id);
+ if (index != -1) {
+ if (syncedTask.id != task.id) {
+ tasks.removeAt(index);
+ tasks.add(syncedTask);
+ } else {
+ tasks[index] = syncedTask;
+ }
+ }
+ } catch (e) {
+ print('Ошибка синхронизации: $e');
+ }
+ }
+
+ Task? openTask(String taskId) {
+ return tasks.firstWhereOrNull((task) => task.id == taskId);
+ }
+
+ @override
+ void onInit() async {
+ super.onInit();
+
+ Get.find().foregroundMessageStream.listen((
+ message,
+ ) {
+ // _handleMessageAction(message); // тот же метод, что и выше
+ });
+
+ getSettings();
+
+ fetchTasks();
+ }
+
+ void openTaskFromNotification(String taskId) {
+ Get.toNamed('/task', arguments: {'id': taskId});
+ }
+}
diff --git a/lib/Pages/controllers/SplashScreenController.dart b/lib/Pages/controllers/SplashScreenController.dart
new file mode 100644
index 0000000..68c238a
--- /dev/null
+++ b/lib/Pages/controllers/SplashScreenController.dart
@@ -0,0 +1,32 @@
+import 'package:Stocky/services/local/StorageService.dart';
+
+import 'package:get/get.dart';
+
+class SplashScreenController extends GetxController {
+ late Future _authCheck;
+
+ @override
+ void onInit() {
+ super.onInit();
+ _authCheck = _checkAuthentication();
+ }
+
+ Future _checkAuthentication() async {
+ final LocalStorageService settings = Get.find();
+ final accessToken = settings.getAccessToken();
+ if (accessToken != null && accessToken.isNotEmpty) {
+ return true;
+ }
+ return false;
+ }
+
+ Future navigateBasedOnAuth() async {
+ final isAuthenticated = await _authCheck;
+
+ if (isAuthenticated) {
+ Get.offAllNamed('/home');
+ } else {
+ Get.offAllNamed('/login');
+ }
+ }
+}
diff --git a/lib/Pages/views/login_screen.dart b/lib/Pages/views/login_screen.dart
new file mode 100644
index 0000000..4a9cf67
--- /dev/null
+++ b/lib/Pages/views/login_screen.dart
@@ -0,0 +1,148 @@
+// lib/pages/login_screen.dart
+import 'package:flutter/material.dart';
+import 'package:Stocky/Pages/controllers/AuthController.dart';
+import 'package:Stocky/Pages/widgets/phone_input_formatter.dart';
+import 'package:Stocky/classes/styles.dart';
+
+import 'package:get/get.dart';
+import 'package:extended_masked_text/extended_masked_text.dart';
+
+class LoginScreen extends StatelessWidget {
+ final AuthController controller = Get.put(AuthController());
+
+ LoginScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text("Вход", style: AppStyles.titleLarge)),
+ body: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "Добро пожаловать!",
+ style: AppStyles.titleLarge, // ← Стиль заголовка
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 30),
+ Obx(() => _buildAuthForm(controller)),
+ const SizedBox(height: 40),
+ Text(
+ "Мы отправим SMS для подтверждения",
+ style: AppStyles.description, // ← Стиль описания
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildAuthForm(AuthController controller) {
+ if (controller.state == 'inputPhone') {
+ return _buildPhoneInputForm(controller);
+ } else {
+ return _buildCodeInputForm(controller);
+ }
+ }
+
+ Widget _buildPhoneInputForm(AuthController controller) {
+ final phonecontrol = MaskedTextController(mask: '+7 (000) 000-00-00');
+ return Column(
+ children: [
+ // Ввод номера
+ TextField(
+ controller: phonecontrol,
+ onChanged: controller.phoneNumberInput,
+ keyboardType: TextInputType.phone,
+ decoration: InputDecoration(
+ labelText: "Номер телефона",
+ prefixIcon: AppStyles.createPrimaryIcon(Icons.phone),
+ hintText: "+7 (999) 123-45-67",
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
+ filled: true,
+ fillColor: Colors.grey[100],
+ labelStyle: AppStyles.body, // ← Стиль для метки
+ hintStyle: AppStyles.description, // ← Стиль для подсказки
+ ),
+ inputFormatters: [PhoneInputFormatter()],
+ ),
+ const SizedBox(height: 30),
+
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: controller.sendCode,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.blue,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ child: Text(
+ "Отправить код",
+ style: AppStyles.title, // ← Стиль текста кнопки
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildCodeInputForm(AuthController controller) {
+ return Column(
+ children: [
+ // Ввод кода
+ TextField(
+ controller: MaskedTextController(mask: '00000'),
+ onChanged: controller.verificationCodeInput,
+ keyboardType: TextInputType.number,
+ obscureText: true,
+ decoration: InputDecoration(
+ labelText: "Код подтверждения",
+ prefixIcon: AppStyles.createPrimaryIcon(Icons.lock),
+ hintText: "Введите 6-значный код",
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
+ filled: true,
+ fillColor: Colors.grey[100],
+ labelStyle: AppStyles.body,
+ hintStyle: AppStyles.description,
+ ),
+ ),
+
+ const SizedBox(height: 30),
+
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: controller.submit,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ child: Text("Подтвердить", style: AppStyles.title),
+ ),
+ ),
+
+ // Кнопка "Назад"
+ const SizedBox(height: 20),
+ TextButton(
+ onPressed: () {
+ controller.setState('inputPhone');
+ controller.verificationCode('');
+ },
+ child: Text(
+ "Вернуться к вводу номера",
+ style: AppStyles.link, // ← Стиль ссылки
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/Pages/views/main_screen.dart b/lib/Pages/views/main_screen.dart
new file mode 100644
index 0000000..0fcfdd6
--- /dev/null
+++ b/lib/Pages/views/main_screen.dart
@@ -0,0 +1,596 @@
+import 'package:flutter/material.dart';
+
+import 'package:Stocky/Pages/controllers/MainController.dart';
+import 'package:Stocky/Pages/views/tasks_screen.dart';
+import 'package:Stocky/classes/styles.dart';
+import 'package:Stocky/models/task_adapter.dart';
+
+import 'package:eva_icons_flutter/eva_icons_flutter.dart';
+import 'package:flutter_advanced_avatar/flutter_advanced_avatar.dart';
+
+import 'package:get/get.dart';
+
+class HomeScreen extends GetWidget {
+ HomeScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: AppColors.backgroundColor,
+ appBar: _buildAppBar(context),
+ drawer: _buildDrawer(context),
+ body: SafeArea(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: _buildFilterChips(context),
+ ),
+ Obx(
+ () => controller.isProj.value
+ ? SizedBox(
+ height: 220,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: _buildScrollableSection(
+ context,
+ child: _buildProjectsSection(context),
+ ),
+ ),
+ )
+ : SizedBox(),
+ ),
+ // Задачи / Процессы
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Obx(
+ () => controller.isTask.value
+ ? _buildProcessSection(context)
+ : SizedBox(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildScrollableSection(
+ BuildContext context, {
+ required Widget child,
+ }) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return SingleChildScrollView(child: child);
+ },
+ );
+ }
+
+ AppBar _buildAppBar(BuildContext context) {
+ return AppBar(
+ title: const Text('Рабочая область'),
+ backgroundColor: AppColors.backgroundColor,
+ actions: [
+ IconButton(
+ icon: Icon(
+ EvaIcons.bellOutline,
+ size: 26,
+ color: Colors.blueGrey[700],
+ ),
+ onPressed: () {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Оповещения'),
+ backgroundColor: Colors.blueAccent,
+ ),
+ );
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildFilterChips(BuildContext context) {
+ return Row(
+ children: [
+ Obx(
+ () => FilterChip(
+ label: const Text('Задачи'),
+ selected: controller.isTask.value,
+ onSelected: (bool selected) {
+ controller.isTask.value = selected;
+ },
+ selectedColor: Colors.blueAccent.withOpacity(0.1),
+ checkmarkColor: Colors.blueAccent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Obx(
+ () => FilterChip(
+ label: const Text('Проекты'),
+ selected: controller.isProj.value,
+ onSelected: (bool selected) {
+ controller.isProj.value = selected;
+ },
+ selectedColor: Colors.blueAccent.withOpacity(0.1),
+ checkmarkColor: Colors.blueAccent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildProjectsSection(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Projects',
+ style: AppStyles.sectionHeader.copyWith(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 12),
+ SizedBox(
+ height: 160,
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ physics: const BouncingScrollPhysics(),
+ padding: const EdgeInsets.only(right: 8),
+ itemCount: 4,
+ itemBuilder: (context, index) =>
+ _buildProjectCard(context, index + 1),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildProjectCard(BuildContext context, int index) {
+ final projectColors = [
+ [Colors.indigo[900]!, Colors.indigo[400]!],
+ [Colors.blue[900]!, Colors.blue[400]!],
+ [Colors.red[900]!, Colors.red[400]!],
+ [Colors.green[900]!, Colors.green[400]!],
+ ];
+ final titles = [
+ 'Front End Development',
+ 'Graphic Design',
+ 'Graphic Design',
+ 'Graphic Design',
+ ];
+ final dates = [
+ 'September 2020',
+ 'October 2020',
+ 'October 2020',
+ 'October 2020',
+ ];
+
+ return Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: Container(
+ width: 160,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: projectColors[index - 1],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(24),
+ ),
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.white.withOpacity(0.3),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ EvaIcons.personOutline,
+ color: Colors.white,
+ size: 20,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Project $index',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ titles[index - 1],
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ dates[index - 1],
+ style: TextStyle(
+ color: Colors.white.withOpacity(0.8),
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildProcessSection(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Активные задачи',
+ style: AppStyles.sectionHeader.copyWith(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Obx(() {
+ if (controller.tasks.isEmpty) {
+ return Center(child: Text('Нет активных задач'));
+ }
+
+ return RefreshIndicator(
+ onRefresh: () => controller.fetchTasks(),
+ child: Column(
+ children: controller.tasks
+ .map((task) => _buildProcessTaskCard(context, task))
+ .toList(),
+ ),
+ );
+ }),
+ ],
+ );
+ }
+
+ Widget _buildProcessTaskCard(BuildContext context, Task task) {
+ final now = DateTime.now();
+ final startDate = task.startTime;
+
+ String formatTime(DateTime dt) => dt.toString().substring(11, 16); // HH:mm
+
+ String formatDateTime(DateTime dt) {
+ if (dt.year == now.year && dt.month == now.month && dt.day == now.day) {
+ return 'Сегодня в ${formatTime(dt)}';
+ } else if (dt.year == now.year) {
+ return '${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')} в ${formatTime(dt)}';
+ } else {
+ return '${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year} в ${formatTime(dt)}';
+ }
+ }
+
+ final int subtaskCount = task.subtasks?.length ?? 0;
+ final String subtaskLabel = subtaskCount == 1
+ ? '1 пункт'
+ : subtaskCount < 5
+ ? '$subtaskCount пункта'
+ : '$subtaskCount пунктов';
+
+ return InkWell(
+ onTap: () {
+ Get.dialog(_buildTaskExecutionDialog(task), barrierDismissible: true);
+ },
+ child: Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.05),
+ blurRadius: 4,
+ spreadRadius: 1,
+ ),
+ ],
+ ),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.pink[50],
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ EvaIcons.shoppingCartOutline,
+ color: Colors.pink[400],
+ size: 24,
+ ),
+ ),
+ const SizedBox(width: 16),
+
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ task.title,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: Colors.black87,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 4),
+ if (task.description.isNotEmpty == true)
+ Text(
+ task.description,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontSize: 12, color: Colors.grey[700]),
+ ),
+ ],
+ ),
+ ),
+
+ if (subtaskCount > 0)
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ formatDateTime(startDate),
+ style: TextStyle(fontSize: 12, color: Colors.grey[600]),
+ ),
+ if (subtaskCount > 0)
+ Text(
+ subtaskLabel,
+ style: TextStyle(fontSize: 12, color: Colors.grey[600]),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildDrawer(BuildContext context) {
+ return Drawer(
+ child: ListView(
+ padding: EdgeInsets.zero,
+ children: [
+ Obx(
+ () => DrawerHeader(
+ decoration: BoxDecoration(color: Colors.blue[900]),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ AdvancedAvatar(
+ name: controller.username.value,
+ autoTextSize: true,
+ size: 80,
+ statusAlignment: Alignment.topRight,
+ ),
+
+ const SizedBox(height: 12),
+ Text(
+ controller.username.value,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ListTile(
+ leading: const Icon(Icons.home),
+ title: const Text('Главная'),
+ onTap: () {
+ Navigator.pop(context);
+ },
+ ),
+ ListTile(
+ leading: const Icon(Icons.calendar_today),
+ title: const Text('Задачи'),
+ onTap: () {
+ Navigator.pop(context);
+ Get.to(TasksScreen()); // Переход на страницу задач
+ },
+ ),
+ ListTile(
+ leading: const Icon(Icons.settings),
+ title: const Text('Настройки'),
+ onTap: () {
+ Navigator.pop(context);
+ },
+ ),
+ Divider(),
+ ListTile(
+ leading: const Icon(Icons.logout),
+ title: const Text('Выход'),
+ onTap: () {
+ // Логика выхода
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTaskExecutionDialog(Task task) {
+ return Dialog(
+ insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 60),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxHeight: 500),
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: StatefulBuilder(
+ builder: (context, setState) {
+ final bool isUnassigned = task.executor.isEmpty;
+ final bool allSubtasksDone =
+ task.items.isNotEmpty &&
+ task.items.every((item) => item.isDone);
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Text(
+ task.title,
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+
+ if (task.description.isNotEmpty)
+ Text(
+ task.description,
+ style: const TextStyle(
+ fontSize: 15,
+ color: Colors.grey,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ if (task.description.isNotEmpty) const SizedBox(height: 16),
+
+ if (task.items.isNotEmpty)
+ Column(
+ children: task.items
+ .map(
+ (item) => ListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(item.text),
+ leading: item.isDone
+ ? const Icon(
+ Icons.check_circle,
+ color: Colors.green,
+ )
+ : const Icon(
+ Icons.radio_button_unchecked,
+ color: Colors.grey,
+ ),
+ onTap: () {
+ setState(() {
+ item.isDone = !item.isDone;
+ });
+ controller.updateTask(task);
+ },
+ ),
+ )
+ .toList(),
+ ),
+
+ // Автор — внизу, перед кнопками
+ if (task.author.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Text(
+ 'Автор: ${task.author}',
+ style: const TextStyle(
+ fontSize: 13,
+ color: Colors.grey,
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Кнопки
+ Wrap(
+ spacing: 12,
+ runSpacing: 8,
+ alignment: WrapAlignment.center,
+ children: [
+ // Кнопка "Взять заявку"
+ if (isUnassigned)
+ ElevatedButton(
+ onPressed: () {
+ controller.assignTaskToCurrentUser(task);
+ // Обновляем состояние, чтобы скрыть кнопку "Взять"
+ // и, возможно, показать новые данные
+ setState(() {});
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.blue[600],
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ child: const Text(
+ 'Взять заявку',
+ style: TextStyle(color: Colors.white),
+ ),
+ ),
+
+ // Кнопка "Завершить" — появляется, если все подзадачи сделаны
+ if (allSubtasksDone)
+ ElevatedButton(
+ onPressed: () {
+ controller.completeTask(task);
+ Get.back(); // закрываем диалог ТОЛЬКО при завершении
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green[600],
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ child: const Text(
+ 'Завершить',
+ style: TextStyle(color: Colors.white),
+ ),
+ ),
+
+ // Кнопка "Закрыть" — всегда доступна
+ OutlinedButton(
+ onPressed: Get.back,
+ style: OutlinedButton.styleFrom(
+ side: const BorderSide(color: Colors.grey),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ child: const Text('Закрыть'),
+ ),
+ ],
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/Pages/views/splash_screen.dart b/lib/Pages/views/splash_screen.dart
new file mode 100644
index 0000000..ca1bd63
--- /dev/null
+++ b/lib/Pages/views/splash_screen.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/material.dart';
+import 'package:Stocky/Pages/controllers/SplashScreenController.dart';
+import 'package:get/get.dart';
+
+class SplashScreen extends StatelessWidget {
+ const SplashScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // Автоматически создаём и привязываем контроллер к этому экрану
+ final controller = Get.put(SplashScreenController());
+
+ // Запускаем логику навигации один раз, когда виджет встроен в дерево
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ controller.navigateBasedOnAuth();
+ });
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: const [
+ CircularProgressIndicator(),
+ SizedBox(height: 20),
+ Text('Загрузка...', style: TextStyle(fontSize: 18)),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/Pages/views/task_screen.dart b/lib/Pages/views/task_screen.dart
new file mode 100644
index 0000000..a40d340
--- /dev/null
+++ b/lib/Pages/views/task_screen.dart
@@ -0,0 +1,211 @@
+import 'package:flutter/material.dart';
+
+import 'package:Stocky/Pages/controllers/MainController.dart';
+import 'package:Stocky/classes/styles.dart';
+import 'package:Stocky/models/task_adapter.dart';
+
+import 'package:get/get.dart';
+
+class TaskFormScreen extends StatefulWidget {
+ final Task? task;
+
+ const TaskFormScreen({super.key, this.task});
+
+ @override
+ State createState() => _TaskFormScreenState();
+}
+
+class _TaskFormScreenState extends State {
+ final _formKey = GlobalKey();
+ final _titleController = TextEditingController();
+ final _descriptionController = TextEditingController();
+ final _itemsController = TextEditingController();
+
+ DateTime? _selectedDate;
+
+ List _items = [];
+ String _newItem = '';
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.task != null) {
+ _titleController.text = widget.task!.title;
+ _descriptionController.text = widget.task!.description;
+ _selectedDate = widget.task!.startTime;
+ _items = List.from(widget.task!.items);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(
+ widget.task != null ? 'Редактировать задачу' : 'Новая задача',
+ ),
+ actions: [
+ TextButton(
+ onPressed: _saveTask,
+ child: const Text('Сохранить', style: TextStyle(fontSize: 16)),
+ ),
+ ],
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Form(
+ key: _formKey,
+ child: ListView(
+ children: [
+ // Дата создания
+ ListTile(
+ title: Text(
+ _selectedDate != null
+ ? 'Дата: ${_selectedDate!.day}.${_selectedDate!.month}.${_selectedDate!.year}'
+ : 'Выберите дату',
+ ),
+ trailing: const Icon(Icons.calendar_today),
+ onTap: _selectDate,
+ ),
+
+ TextFormField(
+ controller: _titleController,
+ decoration: const InputDecoration(
+ labelText: 'Название задачи',
+ border: OutlineInputBorder(),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Введите название задачи';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 16),
+
+ // Описание (опционально)
+ TextFormField(
+ controller: _descriptionController,
+ decoration: const InputDecoration(
+ labelText: 'Описание',
+ border: OutlineInputBorder(),
+ alignLabelWithHint: true,
+ ),
+ maxLines: 3,
+ ),
+ const SizedBox(height: 16),
+
+ // Пункты задачи (чеклист)
+ Text('Пункты задачи', style: AppStyles.titleSmall),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _itemsController,
+ decoration: const InputDecoration(
+ labelText: 'Добавить пункт',
+ border: OutlineInputBorder(),
+ ),
+ onChanged: (value) {
+ _newItem = value;
+ },
+ ),
+ ),
+ IconButton(icon: const Icon(Icons.add), onPressed: _addItem),
+ ],
+ ),
+ const SizedBox(height: 8),
+
+ // Список пунктов
+ if (_items.isNotEmpty)
+ ..._items.map(
+ (item) => Card(
+ child: ListTile(
+ title: Text(item.text),
+ trailing: IconButton(
+ icon: const Icon(Icons.delete, color: Colors.red),
+ onPressed: () => _removeItem(item),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _selectDate() async {
+ final DateTime? picked = await showDatePicker(
+ context: context,
+ initialDate: _selectedDate ?? DateTime.now(),
+ firstDate: DateTime(2020),
+ lastDate: DateTime(2101),
+ );
+ if (picked != null) {
+ setState(() {
+ _selectedDate = picked;
+ });
+ }
+ }
+
+ void _addItem() {
+ if (_newItem.trim().isNotEmpty) {
+ setState(() {
+ _items.add(
+ Subtask(
+ id: DateTime.now().microsecondsSinceEpoch.toString(),
+ text: _newItem.trim(),
+ isDone: false,
+ ),
+ );
+
+ _itemsController.clear();
+ _newItem = '';
+ });
+ }
+ }
+
+ void _removeItem(Subtask item) {
+ setState(() {
+ _items.remove(item);
+ });
+ }
+
+ void _saveTask() {
+ if (_formKey.currentState!.validate()) {
+ final mainController = Get.find();
+
+ String generateId() {
+ return DateTime.now().microsecondsSinceEpoch.toString();
+ }
+
+ final task = Task(
+ id: widget.task?.id ?? generateId(),
+ title: _titleController.text,
+ description: _descriptionController.text.trim(),
+ startTime: _selectedDate ?? DateTime.now(),
+ items: List.from(_items),
+ author: mainController.username.value,
+ executor: mainController.username.value,
+ isSynced: false,
+ isDeletedLocally: false,
+ );
+
+ try {
+ if (widget.task == null) {
+ mainController.addTask(task);
+ } else {
+ mainController.updateTask(task);
+ }
+ Get.back();
+ } catch (e) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('Ошибка сохранения: $e')));
+ }
+ }
+ }
+}
diff --git a/lib/Pages/views/tasks_screen.dart b/lib/Pages/views/tasks_screen.dart
new file mode 100644
index 0000000..d439cd3
--- /dev/null
+++ b/lib/Pages/views/tasks_screen.dart
@@ -0,0 +1,313 @@
+// lib/Pages/views/tasks_screen.dart
+import 'package:flutter/material.dart';
+
+import 'package:Stocky/Pages/controllers/MainController.dart';
+import 'package:Stocky/Pages/views/task_screen.dart';
+import 'package:Stocky/classes/styles.dart';
+import 'package:Stocky/models/task_adapter.dart';
+
+import 'package:eva_icons_flutter/eva_icons_flutter.dart';
+import 'package:swipeable_tile/swipeable_tile.dart';
+import 'package:get/get.dart';
+
+class TasksScreen extends GetWidget {
+ const TasksScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: AppColors.backgroundColor,
+ appBar: _buildAppBar(context),
+ body: SafeArea(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildDateSelector(context),
+ const SizedBox(height: 20),
+ _buildTasksList(context),
+ ],
+ ),
+ ),
+ ),
+ floatingActionButton: FloatingActionButton(
+ onPressed: () {
+ // После создания — синхронизация не нужна: addTask сам всё делает
+ Get.to(() => const TaskFormScreen());
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }
+
+ AppBar _buildAppBar(BuildContext context) {
+ return AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ title: const Text(
+ 'Sep 2020',
+ style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold),
+ ),
+ actions: [
+ IconButton(
+ icon: Icon(EvaIcons.search, size: 26, color: Colors.blueGrey[700]),
+ onPressed: () {},
+ ),
+ ],
+ );
+ }
+
+ Widget _buildDateSelector(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.blue[900],
+ borderRadius: BorderRadius.circular(24),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: List.generate(7, (index) {
+ final dayName = [
+ 'Sun',
+ 'Mon',
+ 'Tue',
+ 'Wed',
+ 'Thu',
+ 'Fri',
+ 'Sat',
+ ][index];
+ final dayNumber = 20 + index;
+
+ return GestureDetector(
+ onTap: () {
+ // Логика выбора даты (опционально)
+ },
+ child: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: dayNumber == 21 ? Colors.white : Colors.transparent,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ dayName,
+ style: TextStyle(
+ fontSize: 10,
+ color: dayNumber == 21 ? Colors.blue[900] : Colors.white,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ dayNumber.toString(),
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
+ color: dayNumber == 21 ? Colors.blue[900] : Colors.white,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }),
+ ),
+ );
+ }
+
+ Widget _buildTasksList(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Tasks',
+ style: AppStyles.sectionHeader.copyWith(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Obx(() {
+ // 🔑 ФИЛЬТРУЕМ: скрываем задачи, помеченные на удаление
+ final visibleTasks = controller.tasks
+ .where((task) => task.isDeletedLocally != true)
+ .toList();
+
+ if (visibleTasks.isEmpty) {
+ return Center(
+ child: Text(
+ 'No tasks found',
+ style: TextStyle(color: Colors.grey[600]),
+ ),
+ );
+ }
+
+ return Column(
+ children: visibleTasks
+ .map((task) => _buildTaskCard(context, task))
+ .toList(),
+ );
+ }),
+ ],
+ );
+ }
+
+ Widget _buildTaskCard(BuildContext context, Task task) {
+ final startDate = task.startTime;
+ String formatTime(DateTime dt) => dt.toString().substring(11, 16); // HH:mm
+
+ return SwipeableTile.card(
+ color: Colors.white,
+ shadow: BoxShadow(
+ color: Colors.black.withOpacity(0.35),
+ blurRadius: 4,
+ offset: const Offset(2, 2),
+ ),
+ key: ValueKey(task.id), // ← лучше использовать id, а не UniqueKey()
+ horizontalPadding: 8,
+ verticalPadding: 8,
+ child: InkWell(
+ onTap: () {
+ Get.dialog(_buildTaskExecutionDialog(task), barrierDismissible: true);
+ },
+ onLongPress: () => Get.to(
+ TaskFormScreen(task: task),
+ ), // ← синхронизация происходит автоматически через updateTask
+ borderRadius: BorderRadius.circular(16),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.pink[50],
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ _getTaskIcon('meeting'),
+ size: 24,
+ color: Colors.pink[400],
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ task.title,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: Colors.black87,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 4),
+ Text(
+ '${formatTime(startDate)} to ${formatTime(startDate.add(Duration(hours: 2)))}',
+ style: const TextStyle(
+ fontSize: 13,
+ color: Colors.grey,
+ height: 1.4,
+ ),
+ ),
+ ],
+ ),
+ ),
+ IconButton(
+ onPressed: () {
+ // Доп. действия (например, меню)
+ },
+ icon: const Icon(Icons.more_vert, color: Colors.grey),
+ ),
+ ],
+ ),
+ ),
+ ),
+ onSwiped: (direction) => controller.deleteTask(task),
+ backgroundBuilder: (context, direction, progress) {
+ return Container(
+ color: Colors.redAccent,
+ child: const Align(
+ alignment: Alignment.centerRight,
+ child: Padding(
+ padding: EdgeInsets.all(8.0),
+ child: Text(
+ 'Удалить',
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ IconData _getTaskIcon(String category) {
+ switch (category.toLowerCase()) {
+ case 'client call':
+ return Icons.phone_rounded;
+ default:
+ return EvaIcons.shoppingCartOutline;
+ }
+ }
+
+ Widget _buildTaskExecutionDialog(Task task) {
+ // Здесь можно реализовать логику выполнения подзадач
+ // Пока оставим как есть (без сохранения изменений)
+ return Material(
+ color: Colors.transparent,
+ child: Padding(
+ padding: const EdgeInsets.all(30.0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ task.title,
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Text(task.description, style: const TextStyle(fontSize: 16)),
+ const SizedBox(height: 16),
+ ...task.items.map(
+ (item) => ListTile(
+ title: Text(item.text),
+ leading: item.isDone
+ ? const Icon(Icons.check_circle, color: Colors.green)
+ : const Icon(Icons.radio_button_unchecked),
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(onPressed: Get.back, child: const Text('Закрыть')),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/Pages/widgets/phone_input_formatter.dart b/lib/Pages/widgets/phone_input_formatter.dart
new file mode 100644
index 0000000..51649ed
--- /dev/null
+++ b/lib/Pages/widgets/phone_input_formatter.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/services.dart';
+
+class PhoneInputFormatter extends TextInputFormatter {
+ @override
+ TextEditingValue formatEditUpdate(
+ TextEditingValue oldValue,
+ TextEditingValue newValue,
+ ) {
+ // Если ввод пустой — возвращаем его как есть
+ if (newValue.text.isEmpty) {
+ return newValue;
+ }
+
+ // Убираем все нецифровые символы из ввода
+ String input = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
+
+ // Ограничиваем длину — максимум 11 цифр (без +7)
+ if (input.length > 11) {
+ input = input.substring(0, 11);
+ }
+
+ // Применяем маску
+ String formatted = _applyPhoneMask(input);
+
+ // Возвращаем отформатированное значение, сохраняя курсор
+ return newValue.copyWith(
+ text: formatted,
+ selection: TextSelection.collapsed(
+ offset: _getCursorOffset(input, formatted),
+ ),
+ );
+ }
+
+ // Применяет маску +7 (XXX) XXX-XX-XX
+ String _applyPhoneMask(String digits) {
+ if (digits.isEmpty) return '';
+ if (digits.length == 1) return '+7';
+ if (digits.length == 2) return '+7 (${'$digits'.substring(1, 2)})';
+ if (digits.length == 3) return '+7 (${digits.substring(1)})';
+ if (digits.length == 4) return '+7 (${digits.substring(1, 4)})';
+ if (digits.length == 5)
+ return '+7 (${digits.substring(1, 4)}) ${digits.substring(4, 5)}';
+ if (digits.length == 6)
+ return '+7 (${digits.substring(1, 4)}) ${digits.substring(4, 6)}';
+ if (digits.length == 7)
+ return '+7 (${digits.substring(1, 4)}) ${digits.substring(4, 7)}';
+ if (digits.length == 8)
+ return '+7 (${digits.substring(1, 4)}) ${digits.substring(4, 7)}-${digits.substring(7, 8)}';
+ if (digits.length == 9)
+ return '+7 (${digits.substring(1, 4)}) ${digits.substring(4, 7)}-${digits.substring(7, 9)}';
+ if (digits.length == 10)
+ return '+7 (${digits.substring(1, 4)}) ${digits.substring(4, 7)}-${digits.substring(7, 9)}-${digits.substring(9, 10)}';
+ if (digits.length == 11)
+ return '+7 (${digits.substring(1, 4)}) ${digits.substring(4, 7)}-${digits.substring(7, 9)}-${digits.substring(9, 11)}';
+
+ return digits;
+ }
+
+ // Вычисляет новую позицию курсора после форматирования
+ int _getCursorOffset(String oldDigits, String formatted) {
+ // Если ввод был короче — курсор должен быть в конце
+ if (oldDigits.length >= formatted.length) {
+ return formatted.length;
+ }
+
+ // Сопоставляем позиции: сколько цифр было до текущей позиции
+ int cursorPosition = 0;
+ for (int i = 0; i < oldDigits.length; i++) {
+ String before = _applyPhoneMask(oldDigits.substring(0, i + 1));
+ String after = _applyPhoneMask(oldDigits.substring(0, i + 2));
+ if (after.length > before.length) {
+ cursorPosition++;
+ }
+ }
+ return cursorPosition;
+ }
+}
diff --git a/lib/classes/styles.dart b/lib/classes/styles.dart
new file mode 100644
index 0000000..08d27a4
--- /dev/null
+++ b/lib/classes/styles.dart
@@ -0,0 +1,114 @@
+import 'package:flutter/material.dart';
+
+class AppStyles {
+ // Заголовки
+ static const TextStyle title = TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w900,
+ fontFamily: 'Geologica',
+ color: Colors.black87,
+ );
+
+ static const TextStyle titleLarge = TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ fontFamily: 'Geologica',
+ color: Colors.black87,
+ );
+
+ static const TextStyle titleSmall = TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Geologica',
+ color: Colors.black87,
+ );
+
+ // Тексты группировки/разделов
+ static const TextStyle sectionHeader = TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w700,
+ fontFamily: 'Geologica',
+ color: Colors.black87,
+ );
+
+ // Основной текст
+ static const TextStyle body = TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.normal,
+ fontFamily: 'Geologica',
+ color: Colors.black87,
+ );
+
+ // Описание/вспомогательный текст
+ static const TextStyle description = TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.normal,
+ fontFamily: 'Geologica',
+ color: Colors.grey,
+ );
+
+ // Мелкий текст
+ static const TextStyle caption = TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.normal,
+ fontFamily: 'Geologica',
+ color: Colors.grey,
+ );
+
+ // Ссылки и кликабельные элементы
+ static const TextStyle link = TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Geologica',
+ color: Colors.blue,
+ decoration: TextDecoration.underline,
+ );
+
+ // Статусы
+ static const TextStyle statusActive = TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Geologica',
+ color: Colors.orange,
+ );
+
+ static const TextStyle statusCompleted = TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Geologica',
+ color: Colors.green,
+ );
+
+ static const TextStyle statusPending = TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Geologica',
+ color: Colors.blue,
+ );
+
+ // Стили для иконок
+ static const double iconSizeLarge = 32.0;
+ static const double iconSizeMedium = 24.0;
+ static const double iconSizeSmall = 16.0;
+
+ static const Color iconColorPrimary = Colors.blue;
+ static const Color iconColorSecondary = Colors.grey;
+ static const Color iconColorAccent = Colors.orange;
+
+ // Вспомогательные методы для иконок
+ static Icon createPrimaryIcon(IconData icon, {double? size}) {
+ return Icon(icon, size: size ?? iconSizeMedium, color: iconColorPrimary);
+ }
+
+ static Icon createSecondaryIcon(IconData icon, {double? size}) {
+ return Icon(icon, size: size ?? iconSizeMedium, color: iconColorSecondary);
+ }
+
+ static Icon createAccentIcon(IconData icon, {double? size}) {
+ return Icon(icon, size: size ?? iconSizeMedium, color: iconColorAccent);
+ }
+}
+
+class AppColors {
+ static const Color backgroundColor = Color(0xFFF8F9FC);
+}
diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart
new file mode 100644
index 0000000..0a6485a
--- /dev/null
+++ b/lib/firebase_options.dart
@@ -0,0 +1,74 @@
+// File generated by FlutterFire CLI.
+// ignore_for_file: type=lint
+import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
+import 'package:flutter/foundation.dart'
+ show defaultTargetPlatform, kIsWeb, TargetPlatform;
+
+/// Default [FirebaseOptions] for use with your Firebase apps.
+///
+/// Example:
+/// ```dart
+/// import 'firebase_options.dart';
+/// // ...
+/// await Firebase.initializeApp(
+/// options: DefaultFirebaseOptions.currentPlatform,
+/// );
+/// ```
+class DefaultFirebaseOptions {
+ static FirebaseOptions get currentPlatform {
+ if (kIsWeb) {
+ return web;
+ }
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ return android;
+ case TargetPlatform.iOS:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions have not been configured for ios - '
+ 'you can reconfigure this by running the FlutterFire CLI again.',
+ );
+ case TargetPlatform.macOS:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions have not been configured for macos - '
+ 'you can reconfigure this by running the FlutterFire CLI again.',
+ );
+ case TargetPlatform.windows:
+ return windows;
+ case TargetPlatform.linux:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions have not been configured for linux - '
+ 'you can reconfigure this by running the FlutterFire CLI again.',
+ );
+ default:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions are not supported for this platform.',
+ );
+ }
+ }
+
+ static const FirebaseOptions web = FirebaseOptions(
+ apiKey: 'AIzaSyDf-atQsOTsyNlPXgc8yamPEo0P77vU72g',
+ appId: '1:785043562091:web:bae3c514d31ac7ce7c5b2e',
+ messagingSenderId: '785043562091',
+ projectId: 'apllicationswork',
+ authDomain: 'apllicationswork.firebaseapp.com',
+ storageBucket: 'apllicationswork.firebasestorage.app',
+ );
+
+ static const FirebaseOptions android = FirebaseOptions(
+ apiKey: 'AIzaSyBGgbGEs6fe9jJgyIWjsRaCmrifC4pjxQo',
+ appId: '1:785043562091:android:abc115731c662f297c5b2e',
+ messagingSenderId: '785043562091',
+ projectId: 'apllicationswork',
+ storageBucket: 'apllicationswork.firebasestorage.app',
+ );
+
+ static const FirebaseOptions windows = FirebaseOptions(
+ apiKey: 'AIzaSyDf-atQsOTsyNlPXgc8yamPEo0P77vU72g',
+ appId: '1:785043562091:web:6f0beb2b421d6ba27c5b2e',
+ messagingSenderId: '785043562091',
+ projectId: 'apllicationswork',
+ authDomain: 'apllicationswork.firebaseapp.com',
+ storageBucket: 'apllicationswork.firebasestorage.app',
+ );
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..a9d775b
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,49 @@
+import 'package:Stocky/services/api/push_notification_service.dart';
+import 'package:firebase_core/firebase_core.dart';
+import 'package:flutter/material.dart';
+
+import 'package:Stocky/Pages/controllers/MainController.dart';
+import 'package:Stocky/Pages/views/login_screen.dart';
+import 'package:Stocky/Pages/views/main_screen.dart';
+import 'package:Stocky/Pages/views/splash_screen.dart';
+
+import 'package:Stocky/services/api/api_service.dart';
+import 'package:Stocky/services/local/StorageService.dart';
+
+import 'firebase_options.dart';
+
+import 'package:get/get.dart';
+
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ await LocalStorageService.init();
+ await ApiService.init();
+
+ await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+
+ Get.put(PushNotificationService());
+
+ //Подключаем контроллер
+ Get.lazyPut(() => MainController());
+
+ runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return GetMaterialApp(
+ title: 'Авторизация',
+ debugShowCheckedModeBanner: false,
+ initialRoute: '/',
+ getPages: [
+ GetPage(name: '/', page: () => SplashScreen()),
+ GetPage(name: '/login', page: () => LoginScreen()),
+ GetPage(name: '/home', page: () => HomeScreen()),
+ ],
+ );
+ }
+}
diff --git a/lib/models/Documents.dart b/lib/models/Documents.dart
new file mode 100644
index 0000000..c856bb8
--- /dev/null
+++ b/lib/models/Documents.dart
@@ -0,0 +1,5 @@
+class Document {
+ int? id;
+
+ Document({this.id});
+}
diff --git a/lib/models/task_adapter.dart b/lib/models/task_adapter.dart
new file mode 100644
index 0000000..eeafc0c
--- /dev/null
+++ b/lib/models/task_adapter.dart
@@ -0,0 +1,117 @@
+// task_adapter.dart
+import 'package:hive/hive.dart';
+
+part 'task_adapter.g.dart';
+
+@HiveType(typeId: 0)
+class Task extends HiveObject {
+ @HiveField(0)
+ final String id;
+
+ @HiveField(1)
+ final String title;
+
+ @HiveField(3)
+ String description;
+
+ @HiveField(4)
+ final String author;
+
+ @HiveField(5)
+ String executor;
+
+ @HiveField(6)
+ final DateTime startTime;
+
+ @HiveField(7)
+ DateTime? endTime;
+
+ @HiveField(8)
+ bool isCompleted;
+
+ @HiveField(9)
+ bool isSynced;
+
+ @HiveField(10) // Новое поле: список подзадач
+ final List items;
+
+ @HiveField(11)
+ bool isDeletedLocally;
+
+ @HiveField(12)
+ bool isNew;
+
+ Task({
+ required this.id,
+ required this.title,
+ required this.startTime,
+ required this.author,
+ required this.executor,
+ this.endTime,
+ this.isCompleted = false,
+ this.description = "",
+ this.isSynced = false,
+ this.isDeletedLocally = false,
+ this.items = const [],
+ this.isNew = true,
+ });
+
+ Map toMap() {
+ return {
+ 'id': id,
+ 'title': title,
+ 'startTime': startTime.toIso8601String(),
+ 'endTime': endTime?.toIso8601String() ?? '',
+ 'author': author,
+ 'executor': executor,
+ 'isCompleted': isCompleted,
+ 'description': description,
+ 'items': items.map((item) => item.toMap()).toList(),
+ };
+ }
+
+ factory Task.fromMap(Map map) {
+ return Task(
+ id: map['_id'],
+ title: map['title'],
+ description: map['description'],
+ author: map['author'],
+ executor: map['executor'],
+ startTime: DateTime.parse(map['startTime']),
+ endTime: map['endTime'] != null ? DateTime.parse(map['endTime']) : null,
+ isCompleted: map['isCompleted'],
+ isSynced: true,
+ isDeletedLocally: false,
+ isNew: false,
+ items:
+ (map['items'] as List?)
+ ?.map((item) => Subtask.fromMap(item))
+ .toList() ??
+ [],
+ );
+ }
+
+ get subtasks => items;
+}
+
+@HiveType(typeId: 1)
+class Subtask extends HiveObject {
+ @HiveField(0)
+ final String id;
+
+ @HiveField(1)
+ String text;
+
+ @HiveField(2)
+ bool isDone;
+
+ Subtask({required this.id, required this.text, this.isDone = false});
+
+ Map toMap() {
+ return {'id': id, 'text': text, 'isDone': isDone};
+ }
+
+ factory Subtask.fromMap(Map map) {
+ return Subtask(id: map['id'], text: map['text'], isDone: map['isDone']);
+ }
+}
diff --git a/lib/models/task_adapter.g.dart b/lib/models/task_adapter.g.dart
new file mode 100644
index 0000000..7a24ef5
--- /dev/null
+++ b/lib/models/task_adapter.g.dart
@@ -0,0 +1,114 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'task_adapter.dart';
+
+// **************************************************************************
+// TypeAdapterGenerator
+// **************************************************************************
+
+class TaskAdapter extends TypeAdapter {
+ @override
+ final int typeId = 0;
+
+ @override
+ Task read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = {
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return Task(
+ id: fields[0] as String,
+ title: fields[1] as String,
+ startTime: fields[6] as DateTime,
+ author: fields[4] as String,
+ executor: fields[5] as String,
+ endTime: fields[7] as DateTime?,
+ isCompleted: fields[8] as bool,
+ description: fields[3] as String,
+ isSynced: fields[9] as bool,
+ isDeletedLocally: fields[11] as bool,
+ items: (fields[10] as List).cast(),
+ isNew: fields[12] as bool,
+ );
+ }
+
+ @override
+ void write(BinaryWriter writer, Task obj) {
+ writer
+ ..writeByte(12)
+ ..writeByte(0)
+ ..write(obj.id)
+ ..writeByte(1)
+ ..write(obj.title)
+ ..writeByte(3)
+ ..write(obj.description)
+ ..writeByte(4)
+ ..write(obj.author)
+ ..writeByte(5)
+ ..write(obj.executor)
+ ..writeByte(6)
+ ..write(obj.startTime)
+ ..writeByte(7)
+ ..write(obj.endTime)
+ ..writeByte(8)
+ ..write(obj.isCompleted)
+ ..writeByte(9)
+ ..write(obj.isSynced)
+ ..writeByte(10)
+ ..write(obj.items)
+ ..writeByte(11)
+ ..write(obj.isDeletedLocally)
+ ..writeByte(12)
+ ..write(obj.isNew);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is TaskAdapter &&
+ runtimeType == other.runtimeType &&
+ typeId == other.typeId;
+}
+
+class SubtaskAdapter extends TypeAdapter {
+ @override
+ final int typeId = 1;
+
+ @override
+ Subtask read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = {
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return Subtask(
+ id: fields[0] as String,
+ text: fields[1] as String,
+ isDone: fields[2] as bool,
+ );
+ }
+
+ @override
+ void write(BinaryWriter writer, Subtask obj) {
+ writer
+ ..writeByte(3)
+ ..writeByte(0)
+ ..write(obj.id)
+ ..writeByte(1)
+ ..write(obj.text)
+ ..writeByte(2)
+ ..write(obj.isDone);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is SubtaskAdapter &&
+ runtimeType == other.runtimeType &&
+ typeId == other.typeId;
+}
diff --git a/lib/services/api/api_service.dart b/lib/services/api/api_service.dart
new file mode 100644
index 0000000..b1ce9a5
--- /dev/null
+++ b/lib/services/api/api_service.dart
@@ -0,0 +1,170 @@
+// lib/services/api/api_service.dart
+
+import 'package:flutter/material.dart';
+
+import 'package:Stocky/models/task_adapter.dart';
+import 'package:Stocky/services/local/StorageService.dart';
+import 'package:Stocky/utils/constants.dart';
+
+import 'package:dio/dio.dart';
+import 'package:get/get.dart';
+
+class ApiService extends GetxService {
+ static final ApiService _instance = ApiService._internal();
+
+ ApiService._internal() {
+ _setupDio();
+ }
+
+ final Dio _dio = Dio();
+
+ String? get accessToken => Get.find().getAccessToken();
+
+ factory ApiService() => _instance;
+
+ void _setupDio() {
+ _dio.options.baseUrl = Constants.BASE_URL;
+ _dio.options.connectTimeout = const Duration(seconds: 10);
+ _dio.options.receiveTimeout = const Duration(seconds: 10);
+ _dio.options.contentType = Headers.jsonContentType;
+ _dio.interceptors.add(
+ InterceptorsWrapper(
+ onRequest: (options, handler) async {
+ final token = accessToken;
+ if (token != null) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+ return handler.next(options);
+ },
+ onResponse: (response, handler) => handler.next(response),
+ ),
+ );
+ }
+
+ Future registerByPhone(String phone) async {
+ try {
+ await _dio.post(
+ '/register',
+ data: {'application': 'warehouse', 'phone': phone},
+ );
+ } catch (e) {
+ debugPrint('Регистрация по телефону: $e');
+ }
+ }
+
+ Future loginByPhone(String phone, String digCode) async {
+ try {
+ final response = await _dio.post(
+ '/login',
+ data: {
+ 'phone': phone,
+ 'digCode': digCode,
+ 'application': 'warehouse',
+ }, // ← убран пробел
+ );
+
+ if (response.statusCode == 200) {
+ final data = response.data;
+ final token = data['token'];
+ final username = data['name'];
+ Get.find().saveAccessToken(token);
+ Get.find().putUsername(username);
+ return true;
+ }
+ } catch (e) {
+ debugPrint('Логин по телефону: $e');
+ }
+ return false;
+ }
+
+ Future> fetchTasks() async {
+ try {
+ final username = await Get.find().getUsername();
+ final response = await _dio.get('/warehouse/tasks/$username');
+ if (response.statusCode == 200) {
+ final List list = response.data;
+ return list.map((item) => Task.fromMap(item)).toList();
+ } else {
+ debugPrint(
+ 'Некорректный статус при загрузке задач: ${response.statusCode}',
+ );
+ }
+ } catch (e) {
+ debugPrint('Ошибка загрузки задач: $e');
+ }
+ return [];
+ }
+
+ Future upsertTask(Task task) async {
+ if (task.isNew == true || task.id.isEmpty) {
+ final saved = await saveTask(task);
+ return saved;
+ } else {
+ final success = await updateTask(task);
+ return success ? task : null;
+ }
+ }
+
+ Future saveTask(Task task) async {
+ try {
+ final response = await _dio.post('/warehouse/task', data: task.toMap());
+ if (response.statusCode == 200) {
+ return response.data;
+ }
+ } catch (e) {
+ debugPrint('Ошибка сохранения задачи: $e');
+ }
+ return null;
+ }
+
+ Future updateTask(Task task) async {
+ try {
+ debugPrint('Обновление задачи: ${task.toMap()}');
+ final response = await _dio.put('/warehouse/task', data: task.toMap());
+ return response.statusCode == 201;
+ } catch (e) {
+ debugPrint('Ошибка обновления задачи: $e');
+ }
+ return false;
+ }
+
+ Future deleteTask(String taskId) async {
+ try {
+ final response = await _dio.delete('/warehouse/task/$taskId');
+ return response.statusCode == 200;
+ } catch (e) {
+ debugPrint('Ошибка удаления задачи: $e');
+ }
+ return false;
+ }
+
+ void clearAuth() {
+ Get.find().clearAuth();
+ }
+
+ static Future init() async {
+ final service = ApiService();
+ Get.put(service, permanent: true);
+ return service;
+ }
+
+ Future setFcmToken(String token) async {
+ try {
+ final response = await _dio.put(
+ '/warehouse/user',
+ data: {
+ "number": await Get.find().getNumber(),
+ "notification": {
+ "token": token.toString(),
+ "enable": true,
+ "type": "FCM",
+ },
+ },
+ );
+ return response.statusCode == 200;
+ } catch (e) {
+ debugPrint('Ошибка обновление токена: $e');
+ }
+ return false;
+ }
+}
diff --git a/lib/services/api/push_notification_service.dart b/lib/services/api/push_notification_service.dart
new file mode 100644
index 0000000..44d613f
--- /dev/null
+++ b/lib/services/api/push_notification_service.dart
@@ -0,0 +1,151 @@
+// lib/services/api/push_notification_service.dart
+import 'dart:async';
+
+import 'package:Stocky/Pages/controllers/MainController.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:firebase_core/firebase_core.dart';
+import 'package:firebase_messaging/firebase_messaging.dart';
+
+import 'api_service.dart'; // Убедитесь, что путь правильный
+
+class PushNotificationService extends GetxService {
+ final FirebaseMessaging _messaging = FirebaseMessaging.instance;
+
+ final StreamController _tokenController =
+ StreamController.broadcast();
+
+ final StreamController _foregroundMessageController =
+ StreamController.broadcast();
+
+ // Поток для уведомлений, пришедших при открытом приложении
+ Stream get foregroundMessageStream =>
+ _foregroundMessageController.stream;
+
+ // Поток FCM-токена
+ Stream get tokenStream => _tokenController.stream;
+
+ @override
+ void onInit() {
+ _initFirebase();
+ super.onInit();
+ }
+
+ @override
+ void onClose() {
+ _tokenController.close();
+ _foregroundMessageController.close();
+ super.onClose();
+ }
+
+ Future _initFirebase() async {
+ try {
+ await Firebase.initializeApp();
+
+ // Запрашиваем разрешение
+ await _requestPermission();
+
+ // Получаем токен
+ await _getToken();
+
+ // Настраиваем обработчики
+ _setupMessageHandling();
+ } catch (e) {
+ debugPrint('Ошибка инициализации FCM: $e');
+ }
+ }
+
+ Future _requestPermission() async {
+ final status = await _messaging.requestPermission(
+ alert: true,
+ badge: true,
+ sound: true,
+ // остальные по умолчанию false
+ );
+
+ if (status.authorizationStatus == AuthorizationStatus.authorized ||
+ status.authorizationStatus == AuthorizationStatus.provisional) {
+ debugPrint('Разрешение на уведомления получено');
+ } else {
+ debugPrint('Разрешение отклонено');
+ }
+ }
+
+ Future _getToken() async {
+ try {
+ final String? token = await _messaging.getToken();
+ if (token != null) {
+ _tokenController.add(token);
+ print('FCM Token: $token');
+
+ // Отправляем токен на сервер — но только если ApiService уже инициализирован
+ if (Get.isRegistered()) {
+ Get.find().setFcmToken(token);
+ } else {
+ // Опционально: сохраните токен локально и отправьте позже
+ print('ApiService не готов — токен временно не отправлен');
+ }
+ }
+ } catch (e) {
+ debugPrint('Ошибка получения токена FCM: $e');
+ }
+ }
+
+ void _setupMessageHandling() {
+ // Уведомления, пришедшие при открытом приложении
+ FirebaseMessaging.onMessage.listen((RemoteMessage message) {
+ debugPrint('Уведомление в foreground: ${message.notification?.title}');
+ Get.find().fetchTasks();
+ _foregroundMessageController.add(message); // ← Передаём дальше
+ });
+
+ // Пользователь нажал на уведомление из фонового режима
+ FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
+ debugPrint('Уведомление открыто из фона: ${message.notification?.title}');
+ _handleMessageAction(message);
+ });
+
+ // Фоновый обработчик (для закрытого приложения)
+ FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
+ }
+
+ // Обработка действия из уведомления
+ void _handleMessageAction(RemoteMessage message) {
+ final data = message.data;
+
+ // Пример: действие зависит от поля "action" в payload
+ final action = data['action'];
+
+ switch (action) {
+ case 'open_task':
+ final taskId = data['task_id'];
+ if (taskId != null) {
+ // Здесь можно вызвать навигацию или обновить состояние
+ // Например, через Get.toNamed('/task/$taskId');
+ // Но лучше делегировать это MainController или другому слушателю
+ Get.find().openTaskFromNotification(taskId);
+ }
+ break;
+
+ case 'refresh_tasks':
+ if (Get.isRegistered()) {
+ Get.find().fetchTasks();
+ }
+ break;
+
+ // Добавьте другие действия по мере необходимости
+ default:
+ debugPrint('Неизвестное действие: $action');
+ }
+ }
+
+ // Статический фоновый обработчик (ограниченный функционал)
+ static Future _firebaseMessagingBackgroundHandler(
+ RemoteMessage message,
+ ) async {
+ // В фоне нельзя использовать GetX, Dio с авторизацией и т.п.
+ // Только простые операции: логирование, запись в Hive и т.д.
+ debugPrint('Фоновое уведомление: ${message.messageId}');
+ // Пример: сохранить в локальную БД, чтобы обработать при запуске
+ }
+}
diff --git a/lib/services/api/socket_service.dart0 b/lib/services/api/socket_service.dart0
new file mode 100644
index 0000000..827f0ee
--- /dev/null
+++ b/lib/services/api/socket_service.dart0
@@ -0,0 +1,88 @@
+// lib/services/api/socket_service.dart
+import 'dart:convert';
+import 'package:flutter_stocky/utils/constants.dart';
+import 'package:get/get.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+class SocketService extends GetxService {
+ final String _baseUrl = Constants.BASE_SOCKET_URL;
+ WebSocketChannel? _channel;
+ bool _connected = false;
+
+ // Поток для получения сообщений
+ StreamController