Начальный коммит
This commit is contained in:
75
lib/Pages/controllers/AuthController.dart
Normal file
75
lib/Pages/controllers/AuthController.dart
Normal file
@@ -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<ApiService>();
|
||||
Rx<String> phoneNumber = ''.obs;
|
||||
Rx<String> 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<void> 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<void> 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<LocalStorageService>().putNumber(phoneNumber.value);
|
||||
Get.snackbar('Успешно', 'Вы вошли!');
|
||||
Get.offAllNamed('/home');
|
||||
}
|
||||
}
|
||||
}
|
||||
233
lib/Pages/controllers/MainController.dart
Normal file
233
lib/Pages/controllers/MainController.dart
Normal file
@@ -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 = <Task>[].obs;
|
||||
final documents = <Document>[].obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
final RxBool isTask = true.obs;
|
||||
final RxBool isProj = true.obs;
|
||||
|
||||
bool _isSyncingTasks = false;
|
||||
|
||||
Future<void> getSettings() async {
|
||||
username.value = await Get.find<LocalStorageService>().getUsername();
|
||||
}
|
||||
|
||||
Future<void> fetchTasks() async {
|
||||
if (_isSyncingTasks) return;
|
||||
_isSyncingTasks = true;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final localTasks = Get.find<LocalStorageService>().getTasks();
|
||||
|
||||
if (localTasks.isEmpty) {
|
||||
await _fetchTasksFromServer();
|
||||
} else {
|
||||
await _syncTasksWithServer(localTasks);
|
||||
}
|
||||
|
||||
final activeTasks = await Get.find<LocalStorageService>()
|
||||
.getCompletedTasks();
|
||||
|
||||
tasks.assignAll(activeTasks);
|
||||
} catch (e) {
|
||||
print('Ошибка при синхронизации задач: $e');
|
||||
} finally {
|
||||
_isSyncingTasks = false;
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncTasksWithServer(List<Task> 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<ApiService>().deleteTask(
|
||||
task.id.toString(),
|
||||
);
|
||||
if (deleted) {
|
||||
Get.find<LocalStorageService>().removeTask(task.id);
|
||||
} else {
|
||||
// Не удалось — оставляем помеченной, попробуем позже
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Создание или обновление
|
||||
final syncedTask = await Get.find<ApiService>().upsertTask(task);
|
||||
if (syncedTask != null) {
|
||||
syncedTask.isSynced = true;
|
||||
syncedTask.isDeletedLocally = false;
|
||||
Get.find<LocalStorageService>().updateTask(syncedTask);
|
||||
}
|
||||
// Если не удалось — остаётся с isSynced = false
|
||||
}
|
||||
}
|
||||
|
||||
// Шаг 2: Получаем актуальный список с сервера
|
||||
final serverTasks = await Get.find<ApiService>().fetchTasks();
|
||||
await Get.find<LocalStorageService>().saveTasks(serverTasks);
|
||||
} catch (e) {
|
||||
print('Ошибка синхронизации: $e');
|
||||
// В случае ошибки — восстанавливаем локальный список
|
||||
tasks.assignAll(localTasks);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchTasksFromServer() async {
|
||||
try {
|
||||
final serverTasks = await Get.find<ApiService>().fetchTasks();
|
||||
await Get.find<LocalStorageService>().saveTasks(serverTasks);
|
||||
tasks.assignAll(serverTasks);
|
||||
} catch (e) {
|
||||
print('Ошибка загрузки задач с сервера: $e');
|
||||
// Остаёмся с пустым списком
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> assignTaskToCurrentUser(Task task) async {
|
||||
task.executor = username.value;
|
||||
task.isSynced = false;
|
||||
updateTask(task);
|
||||
}
|
||||
|
||||
Future<void> completeTask(Task task) async {
|
||||
task.isCompleted = true;
|
||||
task.endTime = DateTime.now();
|
||||
task.isSynced = false;
|
||||
updateTask(task);
|
||||
final activeTasks = await Get.find<LocalStorageService>()
|
||||
.getCompletedTasks();
|
||||
|
||||
tasks.assignAll(activeTasks);
|
||||
}
|
||||
|
||||
Future<void> addTask(Task task) async {
|
||||
task.isSynced = false;
|
||||
task.isDeletedLocally = false;
|
||||
|
||||
Get.find<LocalStorageService>().addTask(task);
|
||||
tasks.add(task);
|
||||
|
||||
_syncTaskToServer(task);
|
||||
}
|
||||
|
||||
Future<void> 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<LocalStorageService>().updateTask(updatedTask);
|
||||
tasks[index] = updatedTask;
|
||||
|
||||
_syncTaskToServer(updatedTask);
|
||||
}
|
||||
|
||||
Future<void> deleteTask(Task task) async {
|
||||
final index = tasks.indexWhere((t) => t.id == task.id);
|
||||
if (index == -1) return;
|
||||
|
||||
try {
|
||||
final success = await Get.find<ApiService>().deleteTask(
|
||||
task.id.toString(),
|
||||
);
|
||||
if (success) {
|
||||
Get.find<LocalStorageService>().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<LocalStorageService>().updateTask(task);
|
||||
}
|
||||
|
||||
Future<void> _syncTaskToServer(Task task) async {
|
||||
final storage = Get.find<LocalStorageService>();
|
||||
|
||||
if (task.isDeletedLocally == true) {
|
||||
final success = await Get.find<ApiService>().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<ApiService>().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<PushNotificationService>().foregroundMessageStream.listen((
|
||||
message,
|
||||
) {
|
||||
// _handleMessageAction(message); // тот же метод, что и выше
|
||||
});
|
||||
|
||||
getSettings();
|
||||
|
||||
fetchTasks();
|
||||
}
|
||||
|
||||
void openTaskFromNotification(String taskId) {
|
||||
Get.toNamed('/task', arguments: {'id': taskId});
|
||||
}
|
||||
}
|
||||
32
lib/Pages/controllers/SplashScreenController.dart
Normal file
32
lib/Pages/controllers/SplashScreenController.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:Stocky/services/local/StorageService.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class SplashScreenController extends GetxController {
|
||||
late Future<bool> _authCheck;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_authCheck = _checkAuthentication();
|
||||
}
|
||||
|
||||
Future<bool> _checkAuthentication() async {
|
||||
final LocalStorageService settings = Get.find<LocalStorageService>();
|
||||
final accessToken = settings.getAccessToken();
|
||||
if (accessToken != null && accessToken.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> navigateBasedOnAuth() async {
|
||||
final isAuthenticated = await _authCheck;
|
||||
|
||||
if (isAuthenticated) {
|
||||
Get.offAllNamed('/home');
|
||||
} else {
|
||||
Get.offAllNamed('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
148
lib/Pages/views/login_screen.dart
Normal file
148
lib/Pages/views/login_screen.dart
Normal file
@@ -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, // ← Стиль ссылки
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
596
lib/Pages/views/main_screen.dart
Normal file
596
lib/Pages/views/main_screen.dart
Normal file
@@ -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<MainController> {
|
||||
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('Закрыть'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/Pages/views/splash_screen.dart
Normal file
32
lib/Pages/views/splash_screen.dart
Normal file
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
211
lib/Pages/views/task_screen.dart
Normal file
211
lib/Pages/views/task_screen.dart
Normal file
@@ -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<TaskFormScreen> createState() => _TaskFormScreenState();
|
||||
}
|
||||
|
||||
class _TaskFormScreenState extends State<TaskFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _titleController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _itemsController = TextEditingController();
|
||||
|
||||
DateTime? _selectedDate;
|
||||
|
||||
List<Subtask> _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<MainController>();
|
||||
|
||||
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')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
313
lib/Pages/views/tasks_screen.dart
Normal file
313
lib/Pages/views/tasks_screen.dart
Normal file
@@ -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<MainController> {
|
||||
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('Закрыть')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
77
lib/Pages/widgets/phone_input_formatter.dart
Normal file
77
lib/Pages/widgets/phone_input_formatter.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
114
lib/classes/styles.dart
Normal file
114
lib/classes/styles.dart
Normal file
@@ -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);
|
||||
}
|
||||
74
lib/firebase_options.dart
Normal file
74
lib/firebase_options.dart
Normal file
@@ -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',
|
||||
);
|
||||
}
|
||||
49
lib/main.dart
Normal file
49
lib/main.dart
Normal file
@@ -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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
5
lib/models/Documents.dart
Normal file
5
lib/models/Documents.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
class Document {
|
||||
int? id;
|
||||
|
||||
Document({this.id});
|
||||
}
|
||||
117
lib/models/task_adapter.dart
Normal file
117
lib/models/task_adapter.dart
Normal file
@@ -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<Subtask> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toMap() {
|
||||
return {'id': id, 'text': text, 'isDone': isDone};
|
||||
}
|
||||
|
||||
factory Subtask.fromMap(Map<String, dynamic> map) {
|
||||
return Subtask(id: map['id'], text: map['text'], isDone: map['isDone']);
|
||||
}
|
||||
}
|
||||
114
lib/models/task_adapter.g.dart
Normal file
114
lib/models/task_adapter.g.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'task_adapter.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class TaskAdapter extends TypeAdapter<Task> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
Task read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
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<Subtask>(),
|
||||
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<Subtask> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
Subtask read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
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;
|
||||
}
|
||||
170
lib/services/api/api_service.dart
Normal file
170
lib/services/api/api_service.dart
Normal file
@@ -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<LocalStorageService>().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<bool> 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<LocalStorageService>().saveAccessToken(token);
|
||||
Get.find<LocalStorageService>().putUsername(username);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Логин по телефону: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<Task>> fetchTasks() async {
|
||||
try {
|
||||
final username = await Get.find<LocalStorageService>().getUsername();
|
||||
final response = await _dio.get('/warehouse/tasks/$username');
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> list = response.data;
|
||||
return list.map((item) => Task.fromMap(item)).toList();
|
||||
} else {
|
||||
debugPrint(
|
||||
'Некорректный статус при загрузке задач: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка загрузки задач: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<Task?> 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<Task?> 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<bool> 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<bool> 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<LocalStorageService>().clearAuth();
|
||||
}
|
||||
|
||||
static Future<ApiService> init() async {
|
||||
final service = ApiService();
|
||||
Get.put(service, permanent: true);
|
||||
return service;
|
||||
}
|
||||
|
||||
Future<bool> setFcmToken(String token) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
'/warehouse/user',
|
||||
data: {
|
||||
"number": await Get.find<LocalStorageService>().getNumber(),
|
||||
"notification": {
|
||||
"token": token.toString(),
|
||||
"enable": true,
|
||||
"type": "FCM",
|
||||
},
|
||||
},
|
||||
);
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка обновление токена: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
151
lib/services/api/push_notification_service.dart
Normal file
151
lib/services/api/push_notification_service.dart
Normal file
@@ -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<String?> _tokenController =
|
||||
StreamController<String?>.broadcast();
|
||||
|
||||
final StreamController<RemoteMessage> _foregroundMessageController =
|
||||
StreamController<RemoteMessage>.broadcast();
|
||||
|
||||
// Поток для уведомлений, пришедших при открытом приложении
|
||||
Stream<RemoteMessage> get foregroundMessageStream =>
|
||||
_foregroundMessageController.stream;
|
||||
|
||||
// Поток FCM-токена
|
||||
Stream<String?> get tokenStream => _tokenController.stream;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
_initFirebase();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_tokenController.close();
|
||||
_foregroundMessageController.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> _initFirebase() async {
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
|
||||
// Запрашиваем разрешение
|
||||
await _requestPermission();
|
||||
|
||||
// Получаем токен
|
||||
await _getToken();
|
||||
|
||||
// Настраиваем обработчики
|
||||
_setupMessageHandling();
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка инициализации FCM: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<void> _getToken() async {
|
||||
try {
|
||||
final String? token = await _messaging.getToken();
|
||||
if (token != null) {
|
||||
_tokenController.add(token);
|
||||
print('FCM Token: $token');
|
||||
|
||||
// Отправляем токен на сервер — но только если ApiService уже инициализирован
|
||||
if (Get.isRegistered<ApiService>()) {
|
||||
Get.find<ApiService>().setFcmToken(token);
|
||||
} else {
|
||||
// Опционально: сохраните токен локально и отправьте позже
|
||||
print('ApiService не готов — токен временно не отправлен');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка получения токена FCM: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _setupMessageHandling() {
|
||||
// Уведомления, пришедшие при открытом приложении
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
debugPrint('Уведомление в foreground: ${message.notification?.title}');
|
||||
Get.find<MainController>().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<MainController>().openTaskFromNotification(taskId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'refresh_tasks':
|
||||
if (Get.isRegistered<MainController>()) {
|
||||
Get.find<MainController>().fetchTasks();
|
||||
}
|
||||
break;
|
||||
|
||||
// Добавьте другие действия по мере необходимости
|
||||
default:
|
||||
debugPrint('Неизвестное действие: $action');
|
||||
}
|
||||
}
|
||||
|
||||
// Статический фоновый обработчик (ограниченный функционал)
|
||||
static Future<void> _firebaseMessagingBackgroundHandler(
|
||||
RemoteMessage message,
|
||||
) async {
|
||||
// В фоне нельзя использовать GetX, Dio с авторизацией и т.п.
|
||||
// Только простые операции: логирование, запись в Hive и т.д.
|
||||
debugPrint('Фоновое уведомление: ${message.messageId}');
|
||||
// Пример: сохранить в локальную БД, чтобы обработать при запуске
|
||||
}
|
||||
}
|
||||
88
lib/services/api/socket_service.dart0
Normal file
88
lib/services/api/socket_service.dart0
Normal file
@@ -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<Map<String, dynamic>> _messageController =
|
||||
StreamController.broadcast();
|
||||
|
||||
Stream<Map<String, dynamic>> get messages => _messageController.stream;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
connect();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_disconnect();
|
||||
_messageController.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void connect() async {
|
||||
if (_connected) return;
|
||||
|
||||
try {
|
||||
_channel = WebSocketChannel.connect(Uri.parse('$_baseUrl/ws'));
|
||||
_channel!.stream.listen(
|
||||
(message) {
|
||||
final data = json.decode(message);
|
||||
_messageController.add(data);
|
||||
},
|
||||
onDone: () {
|
||||
_connected = false;
|
||||
debugPrint('WebSocket отключен');
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('WebSocket ошибка: $error');
|
||||
_connected = false;
|
||||
// Автоподключение через 3 сек
|
||||
Future.delayed(const Duration(seconds: 3), connect);
|
||||
},
|
||||
);
|
||||
|
||||
_connected = true;
|
||||
debugPrint('WebSocket подключен');
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка подключения к WebSocket: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _disconnect() {
|
||||
_channel?.close();
|
||||
_channel = null;
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
void sendMessage(Map<String, dynamic> message) {
|
||||
if (_connected && _channel != null) {
|
||||
_channel?.sink.add(json.encode(message));
|
||||
}
|
||||
}
|
||||
|
||||
// Пример: отправить обновление задачи
|
||||
void notifyTaskUpdate(String taskId) {
|
||||
sendMessage({'type': 'task_updated', 'task_id': taskId});
|
||||
}
|
||||
|
||||
// Пример: подписаться на изменения задач
|
||||
void subscribeToTasks() {
|
||||
sendMessage({'type': 'subscribe', 'entity': 'tasks'});
|
||||
}
|
||||
|
||||
// Пример: уведомить о создании задачи
|
||||
void notifyTaskCreated(Task task) {
|
||||
sendMessage({'type': 'task_created', 'data': task.toJson()});
|
||||
}
|
||||
|
||||
bool get isConnected => _connected;
|
||||
}
|
||||
106
lib/services/local/StorageService.dart
Normal file
106
lib/services/local/StorageService.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
// lib/services/local/local_storage_service.dart
|
||||
import 'package:Stocky/models/task_adapter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
class LocalStorageService extends GetxService {
|
||||
static final LocalStorageService _instance = LocalStorageService._internal();
|
||||
|
||||
late Box _settBox;
|
||||
late Box<Task> _tasksBox;
|
||||
late Box _docsBox;
|
||||
|
||||
LocalStorageService._internal();
|
||||
|
||||
factory LocalStorageService() => _instance;
|
||||
|
||||
Box get settingsBox => _settBox;
|
||||
Box<Task> get tasksBox => _tasksBox;
|
||||
Box get docsBox => _docsBox;
|
||||
|
||||
List<Task> getTasks() {
|
||||
return tasksBox.values.toList();
|
||||
}
|
||||
|
||||
Future<List<Task>> getCompletedTasks() async {
|
||||
return tasksBox.values.where((task) => !task.isCompleted).toList();
|
||||
}
|
||||
|
||||
Future<void> putNumber(String number) async {
|
||||
await settingsBox.put('number', number);
|
||||
}
|
||||
|
||||
Future<String> getNumber() async {
|
||||
return settingsBox.get('number');
|
||||
}
|
||||
|
||||
Future<void> putUsername(String name) async {
|
||||
await settingsBox.put('username', name);
|
||||
}
|
||||
|
||||
Future<String> getUsername() async {
|
||||
return settingsBox.get('username', defaultValue: 'john doe');
|
||||
}
|
||||
|
||||
Future<void> saveTasks(List<Task> tasks) async {
|
||||
await tasksBox.clear();
|
||||
for (final task in tasks) {
|
||||
await tasksBox.put(task.id.toString(), task);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addTask(Task task) async {
|
||||
await tasksBox.put(task.id.toString(), task);
|
||||
}
|
||||
|
||||
Future<void> updateTask(Task task) async {
|
||||
await tasksBox.put(task.id.toString(), task);
|
||||
}
|
||||
|
||||
Future<void> replaceTask(String oldId, Task newTask) async {
|
||||
await tasksBox.delete(oldId);
|
||||
await tasksBox.put(newTask.id.toString(), newTask);
|
||||
}
|
||||
|
||||
Future<void> removeTask(dynamic taskId) async {
|
||||
await tasksBox.delete(taskId.toString());
|
||||
}
|
||||
|
||||
// --- Токены ---
|
||||
Future<void> saveAccessToken(String token) async {
|
||||
await settingsBox.put('access_token', token);
|
||||
}
|
||||
|
||||
Future<void> clearAuth() async {
|
||||
await settingsBox.delete('access_token');
|
||||
}
|
||||
|
||||
Future<void> saveFcmToken(String token) async {
|
||||
await settingsBox.put('fcm_token', token);
|
||||
}
|
||||
|
||||
String? getAccessToken() => settingsBox.get('access_token');
|
||||
String? getFcmToken() => settingsBox.get('fcm_token');
|
||||
|
||||
static Future<LocalStorageService> init() async {
|
||||
await Hive.initFlutter();
|
||||
|
||||
Hive.registerAdapter(TaskAdapter());
|
||||
Hive.registerAdapter(SubtaskAdapter());
|
||||
|
||||
// Открываем боксы
|
||||
await Hive.openBox('settings');
|
||||
await Hive.openBox<Task>('tasks');
|
||||
await Hive.openBox('docs');
|
||||
|
||||
// Теперь можно безопасно получить ссылки
|
||||
final service = _instance;
|
||||
service._settBox = Hive.box('settings');
|
||||
service._tasksBox = Hive.box<Task>('tasks');
|
||||
service._docsBox = Hive.box('docs');
|
||||
|
||||
Get.put(service, permanent: true);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
5
lib/utils/constants.dart
Normal file
5
lib/utils/constants.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
// lib/utils/constants.dart
|
||||
class Constants {
|
||||
static const String BASE_URL = 'http://service.74033.ru:3000/';
|
||||
// static const String BASE_SOCKET_URL = 'ws://service.74033.ru:3000/warehouse';
|
||||
}
|
||||
Reference in New Issue
Block a user