feat: Add Nanaxe server with Docker, GitHub Actions CI/CD, and auto-deployment
- Add complete server infrastructure with Gitea, API, PostgreSQL - Implement Obsidian-compatible markdown mindmap converter - Add Flutter service for server synchronization - Configure GitHub Actions for automated build and deploy - Add production docker-compose with health checks - Include VPS setup and deployment scripts - Add comprehensive deployment documentation
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
/// Сервис для проверки прав доступа администраторов
|
||||
class AdminAccessService {
|
||||
// Список email'ов администраторов
|
||||
static const List<String> adminEmails = [
|
||||
'ormix16@gmail.com',
|
||||
'almalexyz@gmail.com',
|
||||
];
|
||||
|
||||
/// Проверка является ли пользователь администратором
|
||||
static Future<bool> isAdmin(String userEmail) async {
|
||||
return adminEmails.contains(userEmail.toLowerCase());
|
||||
}
|
||||
|
||||
/// Получить список всех администраторов
|
||||
static List<String> getAdminEmails() {
|
||||
return List.from(adminEmails);
|
||||
}
|
||||
|
||||
/// Добавить администратора (только для Firestore)
|
||||
static Future<void> addAdmin(String email) async {
|
||||
if (!adminEmails.contains(email.toLowerCase())) {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('admin_access')
|
||||
.doc(email.toLowerCase())
|
||||
.set({
|
||||
'email': email.toLowerCase(),
|
||||
'addedAt': Timestamp.now(),
|
||||
'role': 'admin',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Удалить администратора
|
||||
static Future<void> removeAdmin(String email) async {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('admin_access')
|
||||
.doc(email.toLowerCase())
|
||||
.delete();
|
||||
}
|
||||
|
||||
/// Получить список администраторов из Firestore
|
||||
static Future<List<String>> getAdminsFromFirestore() async {
|
||||
final snapshot =
|
||||
await FirebaseFirestore.instance.collection('admin_access').get();
|
||||
|
||||
return snapshot.docs.map((doc) => doc.id).toList();
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,95 @@ class DeepSeekService {
|
||||
final String? apiKey;
|
||||
final String baseUrl = 'https://api.deepseek.com';
|
||||
|
||||
// 🎯 РЕЖИМ MOCK - установите true для работы без API запросов
|
||||
// Когда пополните баланс DeepSeek, установите false
|
||||
final bool useMockMode = true;
|
||||
|
||||
DeepSeekService({this.apiKey});
|
||||
|
||||
// AI чат для mind maps
|
||||
Future<Map<String, dynamic>> chatCompletion({
|
||||
required List<Map<String, dynamic>> messages,
|
||||
}) async {
|
||||
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
||||
return _mockChatResponse(messages);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/v1/chat/completions'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $apiKey',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'model': 'deepseek-chat',
|
||||
'messages': messages,
|
||||
'temperature': 0.7,
|
||||
'max_tokens': 2000,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final content = data['choices'][0]['message']['content'];
|
||||
|
||||
// Пытаемся распарсить как JSON
|
||||
final jsonMatch = RegExp(r'\{.*\}', dotAll: true).firstMatch(content);
|
||||
if (jsonMatch != null) {
|
||||
return jsonDecode(jsonMatch.group(0)!) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
return {'text': content};
|
||||
} else {
|
||||
print('DeepSeek API Error: ${response.statusCode} - ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DeepSeek Service Error: $e');
|
||||
}
|
||||
|
||||
return _mockChatResponse(messages);
|
||||
}
|
||||
|
||||
// Mock ответ для чата
|
||||
Map<String, dynamic> _mockChatResponse(List<Map<String, dynamic>> messages) {
|
||||
final lastMessage = messages.lastWhere(
|
||||
(m) => m['role'] == 'user',
|
||||
orElse: () => {'content': ''},
|
||||
);
|
||||
final content = lastMessage['content'] as String;
|
||||
|
||||
// Простая логика для демонстрации
|
||||
if (content.toLowerCase().contains('добавь') ||
|
||||
content.toLowerCase().contains('add')) {
|
||||
return {
|
||||
'ACTION': 'add',
|
||||
'TARGET': null,
|
||||
'CHANGES': [
|
||||
{
|
||||
'action': 'add',
|
||||
'title': 'AI Предложение: $content',
|
||||
'level': 0,
|
||||
'parentId': null
|
||||
},
|
||||
],
|
||||
'EXPLANATION': 'Я добавил новый узел на основе вашего запроса',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
'text':
|
||||
'Понял ваш запрос: "$content". В реальном режиме я предложу изменения для mind map.',
|
||||
};
|
||||
}
|
||||
|
||||
// Генерация подзадач из основной задачи
|
||||
Future<List<Map<String, dynamic>>> generateSubtasks({
|
||||
required String title,
|
||||
required String description,
|
||||
}) async {
|
||||
if (apiKey == null || apiKey!.isEmpty) {
|
||||
// Возвращаем mock данные пока нет баланса на DeepSeek
|
||||
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
||||
return _mockGenerateSubtasks(title, description);
|
||||
}
|
||||
|
||||
@@ -56,9 +137,11 @@ class DeepSeekService {
|
||||
}
|
||||
} else {
|
||||
print('DeepSeek API Error: ${response.statusCode} - ${response.body}');
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DeepSeek Service Error: $e');
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
|
||||
return _mockGenerateSubtasks(title, description);
|
||||
@@ -70,7 +153,8 @@ class DeepSeekService {
|
||||
required String description,
|
||||
DateTime? dueDate,
|
||||
}) async {
|
||||
if (apiKey == null || apiKey!.isEmpty) {
|
||||
// Возвращаем mock данные пока нет баланса на DeepSeek
|
||||
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
||||
return _mockPrioritizeTask(dueDate);
|
||||
}
|
||||
|
||||
@@ -115,9 +199,12 @@ class DeepSeekService {
|
||||
default:
|
||||
return TaskPriority.medium;
|
||||
}
|
||||
} else {
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DeepSeek Service Error: $e');
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
|
||||
return _mockPrioritizeTask(dueDate);
|
||||
@@ -128,8 +215,9 @@ class DeepSeekService {
|
||||
required String title,
|
||||
String? context,
|
||||
}) async {
|
||||
if (apiKey == null || apiKey!.isEmpty) {
|
||||
return 'Описание для задачи: $title';
|
||||
// Возвращаем mock данные пока нет баланса на DeepSeek
|
||||
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
||||
return _mockGenerateDescription(title, context);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -161,12 +249,15 @@ class DeepSeekService {
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['choices'][0]['message']['content'];
|
||||
} else {
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DeepSeek Service Error: $e');
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
|
||||
return 'Описание для задачи: $title';
|
||||
return _mockGenerateDescription(title, context);
|
||||
}
|
||||
|
||||
// Умное планирование - распределение задач по времени
|
||||
@@ -176,7 +267,8 @@ class DeepSeekService {
|
||||
required DateTime endDate,
|
||||
required int availableHoursPerDay,
|
||||
}) async {
|
||||
if (apiKey == null || apiKey!.isEmpty) {
|
||||
// Возвращаем mock данные пока нет баланса на DeepSeek
|
||||
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
||||
return _mockSchedule(tasks, startDate, endDate, availableHoursPerDay);
|
||||
}
|
||||
|
||||
@@ -238,9 +330,12 @@ Return JSON array: [{"taskId": index, "date": "YYYY-MM-DD", "hours": 0}]
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
} else {
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DeepSeek Service Error: $e');
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
|
||||
return _mockSchedule(tasks, startDate, endDate, availableHoursPerDay);
|
||||
@@ -251,7 +346,8 @@ Return JSON array: [{"taskId": index, "date": "YYYY-MM-DD", "hours": 0}]
|
||||
required String projectName,
|
||||
required String description,
|
||||
}) async {
|
||||
if (apiKey == null || apiKey!.isEmpty) {
|
||||
// Возвращаем mock данные пока нет баланса на DeepSeek
|
||||
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
||||
return _mockMindMap(projectName);
|
||||
}
|
||||
|
||||
@@ -297,9 +393,12 @@ Return JSON array of nodes with format:
|
||||
final List<dynamic> nodes = jsonDecode(jsonMatch.group(0)!);
|
||||
return nodes.cast<Map<String, dynamic>>();
|
||||
}
|
||||
} else {
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DeepSeek Service Error: $e');
|
||||
print('⚠️ Используется mock режим как fallback');
|
||||
}
|
||||
|
||||
return _mockMindMap(projectName);
|
||||
@@ -348,6 +447,24 @@ Return JSON array of nodes with format:
|
||||
return TaskPriority.low;
|
||||
}
|
||||
|
||||
String _mockGenerateDescription(String title, String? context) {
|
||||
final descriptions = {
|
||||
'Анализ требований':
|
||||
'Изучить и проанализировать все требования проекта. Подготовить документацию с детальным описанием функционала и ожиданий пользователей.',
|
||||
'Проектирование':
|
||||
'Разработать архитектуру решения, создать схемы базы данных и определить основные компоненты системы.',
|
||||
'Реализация':
|
||||
'Написать чистый, поддерживаемый код согласно лучшим практикам. Реализовать весь запланированный функционал.',
|
||||
'Тестирование':
|
||||
'Провести комплексное тестирование: модульные, интеграционные и end-to-end тесты для обеспечения качества.',
|
||||
'Деплой':
|
||||
'Подготовить инфраструктуру, настроить CI/CD пайплайн и развернуть приложение на production сервере.',
|
||||
};
|
||||
|
||||
return descriptions[title] ??
|
||||
'Задача: $title. ${context ?? "Выполнить в срок с надлежащим качеством."}';
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _mockSchedule(
|
||||
List<TaskModel> tasks,
|
||||
DateTime startDate,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../models/task_model.dart';
|
||||
import '../models/alarm_model.dart';
|
||||
import '../models/mind_map_model.dart';
|
||||
@@ -29,20 +30,23 @@ class LocalStorageService {
|
||||
|
||||
// Tasks
|
||||
Future<void> saveTask(TaskModel task) async {
|
||||
await _tasksBox.put(task.id, task.toMap()); // Используем toMap() для Hive
|
||||
final data = _convertTimestamps(task.toFirestore());
|
||||
data['id'] = task.id; // Добавляем id для fromMap
|
||||
await _tasksBox.put(task.id, data);
|
||||
}
|
||||
|
||||
Future<void> saveTasks(List<TaskModel> tasks) async {
|
||||
for (final task in tasks) {
|
||||
await _tasksBox.put(task.id, task.toMap());
|
||||
final data = _convertTimestamps(task.toFirestore());
|
||||
data['id'] = task.id;
|
||||
await _tasksBox.put(task.id, data);
|
||||
}
|
||||
}
|
||||
|
||||
List<TaskModel> getTasks() {
|
||||
return _tasksBox.values.map((data) {
|
||||
return TaskModel.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
final restoredData = _restoreTimestamps(data);
|
||||
return TaskModel.fromMap(restoredData);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -52,14 +56,14 @@ class LocalStorageService {
|
||||
|
||||
// Alarms
|
||||
Future<void> saveAlarm(AlarmModel alarm) async {
|
||||
await _alarmsBox.put(alarm.id, alarm.toFirestore());
|
||||
await _alarmsBox.put(alarm.id, _convertTimestamps(alarm.toFirestore()));
|
||||
}
|
||||
|
||||
List<AlarmModel> getAlarms() {
|
||||
return _alarmsBox.values.map((data) {
|
||||
return AlarmModel.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
return _alarmsBox.keys.map((id) {
|
||||
final data = _alarmsBox.get(id) as Map<dynamic, dynamic>;
|
||||
final restoredData = _restoreTimestamps(data);
|
||||
return AlarmModel.fromMap(id.toString(), restoredData);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -70,28 +74,28 @@ class LocalStorageService {
|
||||
// Mind Maps
|
||||
Future<void> saveMindMapNodes(List<MindMapNode> nodes) async {
|
||||
for (final node in nodes) {
|
||||
await _mindMapsBox.put(node.id, node.toFirestore());
|
||||
await _mindMapsBox.put(node.id, _convertTimestamps(node.toFirestore()));
|
||||
}
|
||||
}
|
||||
|
||||
List<MindMapNode> getMindMapNodes() {
|
||||
return _mindMapsBox.values.map((data) {
|
||||
return MindMapNode.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
return _mindMapsBox.keys.map((id) {
|
||||
final data = _mindMapsBox.get(id) as Map<dynamic, dynamic>;
|
||||
final restoredData = _restoreTimestamps(data);
|
||||
return MindMapNode.fromMap(id.toString(), restoredData);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Events
|
||||
Future<void> saveEvent(CalendarEventModel event) async {
|
||||
await _eventsBox.put(event.id, event.toFirestore());
|
||||
await _eventsBox.put(event.id, _convertTimestamps(event.toFirestore()));
|
||||
}
|
||||
|
||||
List<CalendarEventModel> getEvents() {
|
||||
return _eventsBox.values.map((data) {
|
||||
return CalendarEventModel.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
return _eventsBox.keys.map((id) {
|
||||
final data = _eventsBox.get(id) as Map<dynamic, dynamic>;
|
||||
final restoredData = _restoreTimestamps(data);
|
||||
return CalendarEventModel.fromMap(id.toString(), restoredData);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -124,14 +128,49 @@ class LocalStorageService {
|
||||
|
||||
await _tasksBox.clear();
|
||||
for (final task in merged.values) {
|
||||
await _tasksBox.put(task.id, task.toFirestore());
|
||||
await _tasksBox.put(task.id, _convertTimestamps(task.toFirestore()));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to create mock DocumentSnapshot
|
||||
dynamic _createMockDocument(Map<String, dynamic> data) {
|
||||
// Простая эмуляция для конвертации
|
||||
return data;
|
||||
// Конвертирует Firestore Timestamp в ISO строки для Hive
|
||||
Map<String, dynamic> _convertTimestamps(Map<String, dynamic> data) {
|
||||
final converted = <String, dynamic>{};
|
||||
for (final entry in data.entries) {
|
||||
if (entry.value is Timestamp) {
|
||||
// Конвертируем Timestamp → ISO строка
|
||||
converted[entry.key] = {
|
||||
'__type__': 'timestamp',
|
||||
'value': (entry.value as Timestamp).toDate().toIso8601String(),
|
||||
};
|
||||
} else if (entry.value is Map<String, dynamic>) {
|
||||
converted[entry.key] =
|
||||
_convertTimestamps(entry.value as Map<String, dynamic>);
|
||||
} else {
|
||||
converted[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
// Восстанавливает ISO строки обратно в Timestamp из Hive
|
||||
Map<String, dynamic> _restoreTimestamps(Map<dynamic, dynamic> data) {
|
||||
final converted = <String, dynamic>{};
|
||||
for (final entry in data.entries) {
|
||||
final key = entry.key.toString();
|
||||
if (entry.value is Map &&
|
||||
(entry.value as Map).containsKey('__type__') &&
|
||||
(entry.value as Map)['__type__'] == 'timestamp') {
|
||||
// Конвертируем ISO строку → Timestamp
|
||||
final dateStr = (entry.value as Map)['value'] as String;
|
||||
converted[key] = Timestamp.fromDate(DateTime.parse(dateStr));
|
||||
} else if (entry.value is Map) {
|
||||
converted[key] =
|
||||
_restoreTimestamps(entry.value as Map<dynamic, dynamic>);
|
||||
} else {
|
||||
converted[key] = entry.value;
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:reactive_mind_map/reactive_mind_map.dart' as rm;
|
||||
import '../models/mind_map_model.dart';
|
||||
|
||||
/// Адаптер для конвертации MindMapNode в формат reactive_mind_map
|
||||
class MindMapAdapter {
|
||||
/// Конвертирует список MindMapNode в rm.MindMapData для reactive_mind_map
|
||||
static rm.MindMapData nodesToMindMapData(List<MindMapNode> nodes) {
|
||||
if (nodes.isEmpty) {
|
||||
return rm.MindMapData(
|
||||
id: 'empty',
|
||||
title: 'Пустая карта',
|
||||
children: [],
|
||||
);
|
||||
}
|
||||
|
||||
// Находим корневой узел (level 0)
|
||||
final rootNodes = nodes.where((n) => n.level == 0).toList();
|
||||
|
||||
if (rootNodes.isEmpty) {
|
||||
// Если нет корневого узла, создаём его
|
||||
return rm.MindMapData(
|
||||
id: 'auto_root',
|
||||
title: 'Автоматический корень',
|
||||
children: _buildChildren(nodes, null, nodes),
|
||||
);
|
||||
}
|
||||
|
||||
final root = rootNodes.first;
|
||||
return rm.MindMapData(
|
||||
id: root.id,
|
||||
title: root.title,
|
||||
children: _buildChildren(nodes, root.id, nodes),
|
||||
);
|
||||
}
|
||||
|
||||
/// Рекурсивно строит дочерние узлы
|
||||
static List<rm.MindMapData> _buildChildren(
|
||||
List<MindMapNode> allNodes,
|
||||
String? parentId,
|
||||
List<MindMapNode> nodes,
|
||||
) {
|
||||
final children = allNodes.where((n) => n.parentId == parentId).toList();
|
||||
|
||||
return children.map((node) {
|
||||
return rm.MindMapData(
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
children: _buildChildren(allNodes, node.id, nodes),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Конвертирует rm.MindMapData обратно в список MindMapNode
|
||||
static List<MindMapNode> mindMapDataToNodes(
|
||||
rm.MindMapData data,
|
||||
String projectId, {
|
||||
String? parentId,
|
||||
int level = 0,
|
||||
}) {
|
||||
final nodes = <MindMapNode>[];
|
||||
final now = DateTime.now();
|
||||
|
||||
nodes.add(MindMapNode(
|
||||
id: data.id,
|
||||
projectId: projectId,
|
||||
parentId: parentId,
|
||||
level: level,
|
||||
title: data.title,
|
||||
color: '#2196F3', // Default color, можно улучшить хранить цвет в metadata
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
));
|
||||
|
||||
// Рекурсивно добавляем детей
|
||||
for (final child in data.children!) {
|
||||
nodes.addAll(mindMapDataToNodes(
|
||||
child,
|
||||
projectId,
|
||||
parentId: data.id,
|
||||
level: level + 1,
|
||||
));
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/// HEX → int (для reactive_mind_map)
|
||||
static int _hexToColorInt(String hex) {
|
||||
hex = hex.replaceAll('#', '');
|
||||
if (hex.length == 6) {
|
||||
return int.parse('FF$hex', radix: 16);
|
||||
}
|
||||
return 0xFF2196F3; // Default blue
|
||||
}
|
||||
|
||||
/// int → HEX (для MindMapNode)
|
||||
static String _colorIntToHex(int color) {
|
||||
return '#${(color & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toUpperCase()}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Сервис для синхронизации с Nanaxe Server
|
||||
/// Поддерживает работу с mindmaps через REST API
|
||||
class NanaxeServerService {
|
||||
static final NanaxeServerService _instance = NanaxeServerService._internal();
|
||||
factory NanaxeServerService() => _instance;
|
||||
NanaxeServerService._internal();
|
||||
|
||||
String? _baseUrl;
|
||||
String? _authToken;
|
||||
|
||||
static const String _prefsUrlKey = 'nanaxe_server_url';
|
||||
static const String _prefsTokenKey = 'nanaxe_server_token';
|
||||
|
||||
/// Инициализация сервиса
|
||||
Future<void> initialize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_baseUrl = prefs.getString(_prefsUrlKey);
|
||||
_authToken = prefs.getString(_prefsTokenKey);
|
||||
}
|
||||
|
||||
/// Настройка сервера
|
||||
Future<void> configure(String baseUrl, {String? token}) async {
|
||||
_baseUrl = baseUrl.replaceAll(RegExp(r'/+$'), ''); // Remove trailing slashes
|
||||
_authToken = token;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsUrlKey, _baseUrl!);
|
||||
if (token != null) {
|
||||
await prefs.setString(_prefsTokenKey, token);
|
||||
}
|
||||
}
|
||||
|
||||
/// Очистка конфигурации
|
||||
Future<void> clearConfiguration() async {
|
||||
_baseUrl = null;
|
||||
_authToken = null;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_prefsUrlKey);
|
||||
await prefs.remove(_prefsTokenKey);
|
||||
}
|
||||
|
||||
/// Проверка, настроен ли сервер
|
||||
bool get isConfigured => _baseUrl != null;
|
||||
|
||||
/// Получение базового URL
|
||||
String? get baseUrl => _baseUrl;
|
||||
|
||||
// ==================== AUTH ====================
|
||||
|
||||
/// Регистрация нового пользователя
|
||||
Future<Map<String, dynamic>> register(String email, String password, String username) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/auth/register'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'email': email,
|
||||
'password': password,
|
||||
'username': username,
|
||||
}),
|
||||
);
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
_authToken = data['token'];
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsTokenKey, _authToken!);
|
||||
return data;
|
||||
} else {
|
||||
throw Exception(data['error'] ?? 'Registration failed');
|
||||
}
|
||||
}
|
||||
|
||||
/// Вход в систему
|
||||
Future<Map<String, dynamic>> login(String email, String password) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/auth/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'email': email,
|
||||
'password': password,
|
||||
}),
|
||||
);
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_authToken = data['token'];
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsTokenKey, _authToken!);
|
||||
return data;
|
||||
} else {
|
||||
throw Exception(data['error'] ?? 'Login failed');
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверка валидности токена
|
||||
Future<bool> validateToken() async {
|
||||
if (_baseUrl == null || _authToken == null) return false;
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/auth/validate'),
|
||||
headers: _getHeaders(),
|
||||
);
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
return data['valid'] == true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== MINDMAPS ====================
|
||||
|
||||
/// Получение списка всех mindmaps
|
||||
Future<List<Map<String, dynamic>>> getMindMaps() async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/mindmaps'),
|
||||
headers: _getHeaders(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return List<Map<String, dynamic>>.from(data['mindmaps'] ?? []);
|
||||
} else {
|
||||
throw Exception('Failed to load mindmaps');
|
||||
}
|
||||
}
|
||||
|
||||
/// Получение одной mindmap
|
||||
Future<Map<String, dynamic>> getMindMap(String id) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/mindmaps/$id'),
|
||||
headers: _getHeaders(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to load mindmap');
|
||||
}
|
||||
}
|
||||
|
||||
/// Создание новой mindmap
|
||||
Future<Map<String, dynamic>> createMindMap({
|
||||
required String title,
|
||||
String? description,
|
||||
List<Map<String, dynamic>>? nodes,
|
||||
Map<String, dynamic>? settings,
|
||||
}) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/mindmaps'),
|
||||
headers: _getHeaders(),
|
||||
body: jsonEncode({
|
||||
'title': title,
|
||||
'description': description ?? '',
|
||||
'nodes': nodes ?? [],
|
||||
'settings': settings ?? {},
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
final data = jsonDecode(response.body);
|
||||
throw Exception(data['error'] ?? 'Failed to create mindmap');
|
||||
}
|
||||
}
|
||||
|
||||
/// Обновление mindmap
|
||||
Future<Map<String, dynamic>> updateMindMap(
|
||||
String id, {
|
||||
String? title,
|
||||
String? description,
|
||||
List<Map<String, dynamic>>? nodes,
|
||||
Map<String, dynamic>? settings,
|
||||
}) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final body = <String, dynamic>{};
|
||||
if (title != null) body['title'] = title;
|
||||
if (description != null) body['description'] = description;
|
||||
if (nodes != null) body['nodes'] = nodes;
|
||||
if (settings != null) body['settings'] = settings;
|
||||
|
||||
final response = await http.put(
|
||||
Uri.parse('$_baseUrl/api/mindmaps/$id'),
|
||||
headers: _getHeaders(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to update mindmap');
|
||||
}
|
||||
}
|
||||
|
||||
/// Удаление mindmap
|
||||
Future<void> deleteMindMap(String id) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.delete(
|
||||
Uri.parse('$_baseUrl/api/mindmaps/$id'),
|
||||
headers: _getHeaders(),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to delete mindmap');
|
||||
}
|
||||
}
|
||||
|
||||
/// Получение mindmap как Markdown
|
||||
Future<String> getMindMapAsMarkdown(String id) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'),
|
||||
headers: _getHeaders(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.body;
|
||||
} else {
|
||||
throw Exception('Failed to load markdown');
|
||||
}
|
||||
}
|
||||
|
||||
/// Сохранение mindmap из Markdown
|
||||
Future<Map<String, dynamic>> saveMindMapFromMarkdown(
|
||||
String id,
|
||||
String markdown,
|
||||
) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'),
|
||||
headers: _getHeaders(),
|
||||
body: jsonEncode({'markdown': markdown}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to save markdown');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SYNC ====================
|
||||
|
||||
/// Получение статуса синхронизации
|
||||
Future<Map<String, dynamic>> getSyncStatus() async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/sync/status'),
|
||||
headers: _getHeaders(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to get sync status');
|
||||
}
|
||||
}
|
||||
|
||||
/// Push изменений в Gitea
|
||||
Future<Map<String, dynamic>> pushToGitea({String? commitMessage}) async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/sync/gitea/push'),
|
||||
headers: _getHeaders(),
|
||||
body: jsonEncode({
|
||||
'commitMessage': commitMessage ?? 'Sync from Nanaxe CRM',
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to push to Gitea');
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull изменений из Gitea
|
||||
Future<Map<String, dynamic>> pullFromGitea() async {
|
||||
if (_baseUrl == null) throw Exception('Server not configured');
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/sync/gitea/pull'),
|
||||
headers: _getHeaders(),
|
||||
body: jsonEncode({}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to pull from Gitea');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== HELPERS ====================
|
||||
|
||||
Map<String, String> _getHeaders() {
|
||||
final headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (_authToken != null) {
|
||||
headers['Authorization'] = 'Bearer $_authToken';
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/// Проверка соединения с сервером
|
||||
Future<bool> checkConnection() async {
|
||||
if (_baseUrl == null) return false;
|
||||
|
||||
try {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/health'))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Провайдер для работы с сервером
|
||||
class ServerConfig {
|
||||
final String baseUrl;
|
||||
final String? token;
|
||||
final bool isConnected;
|
||||
|
||||
ServerConfig({
|
||||
required this.baseUrl,
|
||||
this.token,
|
||||
this.isConnected = false,
|
||||
});
|
||||
}
|
||||
@@ -6,13 +6,17 @@ import '../screens/home_screen.dart';
|
||||
import '../screens/tasks_screen.dart';
|
||||
import '../screens/task_form_screen.dart';
|
||||
import '../screens/mind_map_screen.dart';
|
||||
import '../screens/mind_map_settings_screen.dart';
|
||||
import '../screens/calendar_screen.dart';
|
||||
import '../screens/alarms_screen.dart';
|
||||
import '../screens/ai_test_screen.dart';
|
||||
import '../screens/profile_screen.dart';
|
||||
import '../screens/admin/admin_screen.dart';
|
||||
import '../screens/admin/team_management_screen.dart';
|
||||
import '../screens/admin/projects_management_screen.dart';
|
||||
import '../screens/admin/notifications_settings_screen.dart';
|
||||
import '../screens/admin/mind_map_admin_screen.dart';
|
||||
import '../screens/admin/ai_stats_screen.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
// Глобальный ключ для навигации
|
||||
@@ -67,6 +71,17 @@ class AppRouter {
|
||||
path: 'mindmap',
|
||||
name: 'mindmap',
|
||||
builder: (context, state) => const MindMapScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'settings',
|
||||
name: 'mindmap-settings',
|
||||
builder: (context, state) {
|
||||
final projectId =
|
||||
state.uri.queryParameters['projectId'] ?? 'default';
|
||||
return MindMapSettingsScreen(projectId: projectId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'calendar',
|
||||
@@ -78,6 +93,11 @@ class AppRouter {
|
||||
name: 'alarms',
|
||||
builder: (context, state) => const AlarmsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'profile',
|
||||
name: 'profile',
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'ai-test',
|
||||
name: 'ai-test',
|
||||
@@ -104,6 +124,16 @@ class AppRouter {
|
||||
builder: (context, state) =>
|
||||
const NotificationsSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'mindmaps',
|
||||
name: 'admin-mindmaps',
|
||||
builder: (context, state) => const MindMapAdminScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'ai-stats',
|
||||
name: 'admin-ai-stats',
|
||||
builder: (context, state) => const AIStatsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user