Initial commit: NANAHUI CRM - Flutter Firebase CRM assistant

This commit is contained in:
2026-03-27 14:38:10 +05:00
commit 78d15daa35
161 changed files with 9924 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/task_model.dart';
class AIService {
final String? openaiApiKey;
final String? anthropicApiKey;
final String? googleAiApiKey;
AIService({
this.openaiApiKey,
this.anthropicApiKey,
this.googleAiApiKey,
});
// Генерация подзадач из основной задачи
Future<List<Map<String, dynamic>>> generateSubtasks({
required String title,
required String description,
}) async {
if (openaiApiKey == null) {
return _mockGenerateSubtasks(title, description);
}
try {
final response = await http.post(
Uri.parse('https://api.openai.com/v1/chat/completions'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $openaiApiKey',
},
body: jsonEncode({
'model': 'gpt-4o-mini',
'messages': [
{
'role': 'system',
'content': 'You are a task planning assistant. Break down tasks into logical subtasks.',
},
{
'role': 'user',
'content': 'Break down this task into 3-7 subtasks:\n\nTitle: $title\nDescription: $description\n\nReturn ONLY a JSON array of subtasks with format: [{"title": "...", "estimatedHours": 0}]',
},
],
'temperature': 0.7,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final content = data['choices'][0]['message']['content'];
final List<dynamic> subtasks = jsonDecode(content);
return subtasks.cast<Map<String, dynamic>>();
}
} catch (e) {
print('AI Service Error: $e');
}
return _mockGenerateSubtasks(title, description);
}
// AI приоритизация задач
Future<TaskPriority> prioritizeTask({
required String title,
required String description,
DateTime? dueDate,
}) async {
if (openaiApiKey == null) {
return _mockPrioritizeTask(dueDate);
}
try {
final response = await http.post(
Uri.parse('https://api.openai.com/v1/chat/completions'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $openaiApiKey',
},
body: jsonEncode({
'model': 'gpt-4o-mini',
'messages': [
{
'role': 'system',
'content': 'You are a task prioritization assistant. Determine task priority: low, medium, high, or urgent.',
},
{
'role': 'user',
'content': 'What priority should this task have?\n\nTitle: $title\nDescription: $description\nDue: ${dueDate ?? "Not specified"}\n\nReply with ONE word: low, medium, high, or urgent',
},
],
'temperature': 0.3,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final content = data['choices'][0]['message']['content'].trim().toLowerCase();
switch (content) {
case 'urgent': return TaskPriority.urgent;
case 'high': return TaskPriority.high;
case 'low': return TaskPriority.low;
default: return TaskPriority.medium;
}
}
} catch (e) {
print('AI Service Error: $e');
}
return _mockPrioritizeTask(dueDate);
}
// Умное планирование - распределение задач по времени
Future<List<Map<String, dynamic>>> smartSchedule({
required List<TaskModel> tasks,
required DateTime startDate,
required DateTime endDate,
required int availableHoursPerDay,
}) async {
// Mock implementation для начала
final schedule = <Map<String, dynamic>>[];
var currentDate = startDate;
var remainingHours = availableHoursPerDay;
for (final task in tasks) {
if (currentDate.isAfter(endDate)) break;
if (task.estimatedHours <= remainingHours) {
schedule.add({
'taskId': task.id,
'date': currentDate,
'hours': task.estimatedHours,
});
remainingHours -= task.estimatedHours;
} else {
// Переходим на следующий день
currentDate = currentDate.add(const Duration(days: 1));
remainingHours = availableHoursPerDay;
if (currentDate.isAfter(endDate)) break;
schedule.add({
'taskId': task.id,
'date': currentDate,
'hours': task.estimatedHours > remainingHours
? remainingHours
: task.estimatedHours,
});
remainingHours -= task.estimatedHours > remainingHours
? remainingHours
: task.estimatedHours;
}
}
return schedule;
}
// Генерация описания задачи
Future<String> generateTaskDescription({
required String title,
String? context,
}) async {
if (openaiApiKey == null) {
return 'Описание для задачи: $title';
}
try {
final response = await http.post(
Uri.parse('https://api.openai.com/v1/chat/completions'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $openaiApiKey',
},
body: jsonEncode({
'model': 'gpt-4o-mini',
'messages': [
{
'role': 'system',
'content': 'You are a task description writer. Write clear, concise task descriptions.',
},
{
'role': 'user',
'content': 'Write a brief description for this task:\n\nTitle: $title\nContext: ${context ?? "No additional context"}\n\nKeep it under 100 words.',
},
],
'temperature': 0.5,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['choices'][0]['message']['content'];
}
} catch (e) {
print('AI Service Error: $e');
}
return 'Описание для задачи: $title';
}
// Mock данные для тестирования
List<Map<String, dynamic>> _mockGenerateSubtasks(String title, String description) {
return [
{'title': 'Анализ требований', 'estimatedHours': 2},
{'title': 'Проектирование', 'estimatedHours': 3},
{'title': 'Реализация', 'estimatedHours': 8},
{'title': 'Тестирование', 'estimatedHours': 3},
{'title': 'Документирование', 'estimatedHours': 2},
];
}
TaskPriority _mockPrioritizeTask(DateTime? dueDate) {
if (dueDate == null) return TaskPriority.medium;
final daysUntilDue = dueDate.difference(DateTime.now()).inDays;
if (daysUntilDue <= 1) return TaskPriority.urgent;
if (daysUntilDue <= 3) return TaskPriority.high;
if (daysUntilDue <= 7) return TaskPriority.medium;
return TaskPriority.low;
}
}
final aiService = AIService(
// API ключи лучше хранить в .env файле или Firebase Remote Config
openaiApiKey: null, // Добавить ключ здесь или через env
anthropicApiKey: null,
googleAiApiKey: null,
);
+14
View File
@@ -0,0 +1,14 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
class FirebaseConfig {
static Future<void> initialize() async {
await Firebase.initializeApp();
debugPrint('Firebase initialized successfully');
}
static Future<void> setupFirestore() async {
// Настройки Firestore для лучшей производительности
// Будут применены после инициализации
}
}
+201
View File
@@ -0,0 +1,201 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/task_model.dart';
import '../models/project_model.dart';
import '../models/mind_map_model.dart';
import '../models/calendar_event_model.dart';
import '../models/alarm_model.dart';
import '../models/user_model.dart';
class FirestoreService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
// === USERS ===
Future<void> createUser(UserModel user) async {
await _db.collection('users').doc(user.id).set(user.toFirestore());
}
Future<UserModel?> getUser(String userId) async {
final doc = await _db.collection('users').doc(userId).get();
return doc.exists ? UserModel.fromFirestore(doc) : null;
}
Future<void> updateUser(UserModel user) async {
await _db.collection('users').doc(user.id).update(user.toFirestore());
}
// === PROJECTS ===
Future<void> createProject(ProjectModel project) async {
await _db.collection('projects').doc(project.id).set(project.toFirestore());
}
Future<List<ProjectModel>> getProjectsByTeam(String teamId) async {
final snapshot = await _db
.collection('projects')
.where('teamId', isEqualTo: teamId)
.orderBy('createdAt', descending: true)
.get();
return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList();
}
Future<List<ProjectModel>> getProjectsByUser(String userId) async {
final snapshot = await _db
.collection('projects')
.where('memberIds', arrayContains: userId)
.get();
return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList();
}
// === TASKS ===
Future<void> createTask(TaskModel task) async {
await _db.collection('tasks').doc(task.id).set(task.toFirestore());
}
Future<List<TaskModel>> getTasksByProject(String projectId) async {
final snapshot = await _db
.collection('tasks')
.where('projectId', isEqualTo: projectId)
.orderBy('createdAt', descending: true)
.get();
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
}
Future<List<TaskModel>> getTasksByAssignee(String assigneeId) async {
final snapshot = await _db
.collection('tasks')
.where('assigneeId', isEqualTo: assigneeId)
.where('status', whereIn: ['todo', 'inProgress', 'review'])
.get();
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
}
Future<void> updateTask(TaskModel task) async {
await _db.collection('tasks').doc(task.id).update(task.toFirestore());
}
Future<void> deleteTask(String taskId) async {
await _db.collection('tasks').doc(taskId).delete();
}
// Задачи с поддержкой иерархии (для mind-map)
Future<List<TaskModel>> getTaskHierarchy(String projectId) async {
final snapshot = await _db
.collection('tasks')
.where('projectId', isEqualTo: projectId)
.get();
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
}
// === MIND MAPS ===
Future<void> createMindMapNode(MindMapNode node) async {
await _db.collection('mindmaps').doc(node.projectId).collection('nodes').doc(node.id).set(node.toFirestore());
}
Future<List<MindMapNode>> getMindMapNodes(String projectId) async {
final snapshot = await _db
.collection('mindmaps')
.doc(projectId)
.collection('nodes')
.orderBy('level')
.get();
return snapshot.docs.map((doc) => MindMapNode.fromFirestore(doc)).toList();
}
Future<void> updateMindMapNode(MindMapNode node) async {
await _db
.collection('mindmaps')
.doc(node.projectId)
.collection('nodes')
.doc(node.id)
.update(node.toFirestore());
}
// === CALENDAR EVENTS ===
Future<void> createEvent(CalendarEventModel event) async {
await _db.collection('events').doc(event.id).set(event.toFirestore());
}
Future<List<CalendarEventModel>> getEventsByDateRange({
required DateTime start,
required DateTime end,
String? userId,
}) async {
Query query = _db
.collection('events')
.where('startTime', isLessThanOrEqualTo: Timestamp.fromDate(end))
.where('endTime', isGreaterThanOrEqualTo: Timestamp.fromDate(start));
if (userId != null) {
query = query.where('attendeeIds', arrayContains: userId);
}
final snapshot = await query.get();
return snapshot.docs.map((doc) => CalendarEventModel.fromFirestore(doc)).toList();
}
Future<void> updateEvent(CalendarEventModel event) async {
await _db.collection('events').doc(event.id).update(event.toFirestore());
}
Future<void> deleteEvent(String eventId) async {
await _db.collection('events').doc(eventId).delete();
}
// === ALARMS ===
Future<void> createAlarm(AlarmModel alarm) async {
await _db.collection('alarms').doc(alarm.id).set(alarm.toFirestore());
}
Future<List<AlarmModel>> getAlarmsByUser(String userId) async {
final snapshot = await _db
.collection('alarms')
.where('isEnabled', isEqualTo: true)
.orderBy('dateTime')
.get();
return snapshot.docs.map((doc) => AlarmModel.fromFirestore(doc)).toList();
}
Future<void> updateAlarm(AlarmModel alarm) async {
await _db.collection('alarms').doc(alarm.id).update(alarm.toFirestore());
}
Future<void> deleteAlarm(String alarmId) async {
await _db.collection('alarms').doc(alarmId).delete();
}
// === BATCH OPERATIONS ===
Future<void> syncTasks(List<TaskModel> tasks) async {
final batch = _db.batch();
for (final task in tasks) {
batch.set(_db.collection('tasks').doc(task.id), task.toFirestore());
}
await batch.commit();
}
// === STREAMS (для реального времени) ===
Stream<List<TaskModel>> watchTasks(String projectId) {
return _db
.collection('tasks')
.where('projectId', isEqualTo: projectId)
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList());
}
Stream<List<CalendarEventModel>> watchEvents(String userId) {
return _db
.collection('events')
.where('attendeeIds', arrayContains: userId)
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => CalendarEventModel.fromFirestore(doc)).toList());
}
}
final firestoreService = FirestoreService();
+138
View File
@@ -0,0 +1,138 @@
import 'package:hive_flutter/hive_flutter.dart';
import '../models/task_model.dart';
import '../models/alarm_model.dart';
import '../models/mind_map_model.dart';
import '../models/calendar_event_model.dart';
class LocalStorageService {
static const String _tasksBoxName = 'tasks';
static const String _alarmsBoxName = 'alarms';
static const String _mindMapsBoxName = 'mindMaps';
static const String _eventsBoxName = 'events';
static const String _settingsBoxName = 'settings';
late Box<Map> _tasksBox;
late Box<Map> _alarmsBox;
late Box<Map> _mindMapsBox;
late Box<Map> _eventsBox;
late Box<Object?> _settingsBox;
Future<void> initialize() async {
await Hive.initFlutter();
_tasksBox = await Hive.openBox<Map>(_tasksBoxName);
_alarmsBox = await Hive.openBox<Map>(_alarmsBoxName);
_mindMapsBox = await Hive.openBox<Map>(_mindMapsBoxName);
_eventsBox = await Hive.openBox<Map>(_eventsBoxName);
_settingsBox = await Hive.openBox<Object?>(_settingsBoxName);
}
// Tasks
Future<void> saveTask(TaskModel task) async {
await _tasksBox.put(task.id, task.toFirestore());
}
Future<void> saveTasks(List<TaskModel> tasks) async {
for (final task in tasks) {
await _tasksBox.put(task.id, task.toFirestore());
}
}
List<TaskModel> getTasks() {
return _tasksBox.values.map((data) {
return TaskModel.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
}).toList();
}
Future<void> deleteTask(String taskId) async {
await _tasksBox.delete(taskId);
}
// Alarms
Future<void> saveAlarm(AlarmModel alarm) async {
await _alarmsBox.put(alarm.id, alarm.toFirestore());
}
List<AlarmModel> getAlarms() {
return _alarmsBox.values.map((data) {
return AlarmModel.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
}).toList();
}
Future<void> deleteAlarm(String alarmId) async {
await _alarmsBox.delete(alarmId);
}
// Mind Maps
Future<void> saveMindMapNodes(List<MindMapNode> nodes) async {
for (final node in nodes) {
await _mindMapsBox.put(node.id, node.toFirestore());
}
}
List<MindMapNode> getMindMapNodes() {
return _mindMapsBox.values.map((data) {
return MindMapNode.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
}).toList();
}
// Events
Future<void> saveEvent(CalendarEventModel event) async {
await _eventsBox.put(event.id, event.toFirestore());
}
List<CalendarEventModel> getEvents() {
return _eventsBox.values.map((data) {
return CalendarEventModel.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
}).toList();
}
// Settings
Future<void> saveSetting(String key, Object? value) async {
await _settingsBox.put(key, value);
}
T? getSetting<T>(String key, {T? defaultValue}) {
return _settingsBox.get(key, defaultValue: defaultValue) as T?;
}
// Clear all local data
Future<void> clearAll() async {
await _tasksBox.clear();
await _alarmsBox.clear();
await _mindMapsBox.clear();
await _eventsBox.clear();
await _settingsBox.clear();
}
// Sync with Firebase (merge remote and local)
Future<void> syncWithRemote(List<TaskModel> remoteTasks) async {
final localTasks = getTasks();
final localMap = {for (var t in localTasks) t.id: t};
final remoteMap = {for (var t in remoteTasks) t.id: t};
// Merge: remote takes precedence
final merged = {...localMap, ...remoteMap};
await _tasksBox.clear();
for (final task in merged.values) {
await _tasksBox.put(task.id, task.toFirestore());
}
}
// Helper method to create mock DocumentSnapshot
dynamic _createMockDocument(Map<String, dynamic> data) {
// Простая эмуляция для конвертации
return data;
}
}
final localStorageService = LocalStorageService();
+47
View File
@@ -0,0 +1,47 @@
import 'package:go_router/go_router.dart';
import '../screens/auth_screen.dart';
import '../screens/home_screen.dart';
import '../screens/tasks_screen.dart';
import '../screens/mind_map_screen.dart';
import '../screens/calendar_screen.dart';
import '../screens/alarms_screen.dart';
class AppRouter {
static final GoRouter router = GoRouter(
initialLocation: '/auth',
routes: [
GoRoute(
path: '/auth',
name: 'auth',
builder: (context, state) => const AuthScreen(),
),
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'tasks',
name: 'tasks',
builder: (context, state) => const TasksScreen(),
),
GoRoute(
path: 'mindmap',
name: 'mindmap',
builder: (context, state) => const MindMapScreen(),
),
GoRoute(
path: 'calendar',
name: 'calendar',
builder: (context, state) => const CalendarScreen(),
),
GoRoute(
path: 'alarms',
name: 'alarms',
builder: (context, state) => const AlarmsScreen(),
),
],
),
],
);
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
elevation: 4,
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
elevation: 4,
),
);
}
}
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/firebase_config.dart';
import 'core/local_storage_service.dart';
import 'core/theme.dart';
import 'core/router.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Инициализация Firebase
await FirebaseConfig.initialize();
// Инициализация локального хранилища
await localStorageService.initialize();
runApp(
const ProviderScope(
child: NanahuiCrmApp(),
),
);
}
class NanahuiCrmApp extends StatelessWidget {
const NanahuiCrmApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'NANAHUI CRM',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
routerConfig: AppRouter.router,
);
}
}
+132
View File
@@ -0,0 +1,132 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class AlarmModel {
final String id;
final String title;
final String? taskId;
final String? eventId;
final DateTime dateTime;
final bool isRepeating;
final String repeatPattern; // daily, weekly, monthly, custom
final List<int> repeatDays; // For custom (e.g., [1, 3, 5] for Mon, Wed, Fri)
final bool isEnabled;
final String sound;
final int snoozeMinutes;
final Map<String, dynamic> metadata;
final DateTime createdAt;
final DateTime updatedAt;
AlarmModel({
required this.id,
required this.title,
this.taskId,
this.eventId,
required this.dateTime,
this.isRepeating = false,
this.repeatPattern = 'daily',
this.repeatDays = const [],
this.isEnabled = true,
this.sound = 'default',
this.snoozeMinutes = 5,
this.metadata = const {},
required this.createdAt,
required this.updatedAt,
});
factory AlarmModel.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return AlarmModel(
id: doc.id,
title: data['title'] ?? '',
taskId: data['taskId'],
eventId: data['eventId'],
dateTime: (data['dateTime'] as Timestamp).toDate(),
isRepeating: data['isRepeating'] ?? false,
repeatPattern: data['repeatPattern'] ?? 'daily',
repeatDays: List<int>.from(data['repeatDays'] ?? []),
isEnabled: data['isEnabled'] ?? true,
sound: data['sound'] ?? 'default',
snoozeMinutes: data['snoozeMinutes'] ?? 5,
metadata: data['metadata'] ?? {},
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
);
}
Map<String, dynamic> toFirestore() {
return {
'title': title,
'taskId': taskId,
'eventId': eventId,
'dateTime': Timestamp.fromDate(dateTime),
'isRepeating': isRepeating,
'repeatPattern': repeatPattern,
'repeatDays': repeatDays,
'isEnabled': isEnabled,
'sound': sound,
'snoozeMinutes': snoozeMinutes,
'metadata': metadata,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
AlarmModel copyWith({
String? id,
String? title,
String? taskId,
String? eventId,
DateTime? dateTime,
bool? isRepeating,
String? repeatPattern,
List<int>? repeatDays,
bool? isEnabled,
String? sound,
int? snoozeMinutes,
Map<String, dynamic>? metadata,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return AlarmModel(
id: id ?? this.id,
title: title ?? this.title,
taskId: taskId ?? this.taskId,
eventId: eventId ?? this.eventId,
dateTime: dateTime ?? this.dateTime,
isRepeating: isRepeating ?? this.isRepeating,
repeatPattern: repeatPattern ?? this.repeatPattern,
repeatDays: repeatDays ?? this.repeatDays,
isEnabled: isEnabled ?? this.isEnabled,
sound: sound ?? this.sound,
snoozeMinutes: snoozeMinutes ?? this.snoozeMinutes,
metadata: metadata ?? this.metadata,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
DateTime get nextOccurrence {
if (!isRepeating) return dateTime;
final now = DateTime.now();
if (dateTime.isAfter(now)) return dateTime;
switch (repeatPattern) {
case 'daily':
return dateTime.add(const Duration(days: 1));
case 'weekly':
return dateTime.add(const Duration(days: 7));
case 'monthly':
return DateTime(dateTime.year, dateTime.month + 1, dateTime.day);
case 'custom':
// Find next weekday in repeatDays
var next = dateTime.add(const Duration(days: 1));
while (!repeatDays.contains(next.weekday)) {
next = next.add(const Duration(days: 1));
}
return DateTime(next.year, next.month, next.day, dateTime.hour, dateTime.minute);
default:
return dateTime;
}
}
}
+133
View File
@@ -0,0 +1,133 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class CalendarEventModel {
final String id;
final String title;
final String? description;
final String location;
final DateTime startTime;
final DateTime endTime;
final bool isAllDay;
final String? googleEventId;
final String? taskId;
final List<String> attendeeIds;
final List<String> attendeeEmails;
final String color;
final String? recurringRuleId;
final Map<String, dynamic> reminders;
final String status; // confirmed, tentative, cancelled
final String createdBy;
final DateTime createdAt;
final DateTime updatedAt;
CalendarEventModel({
required this.id,
required this.title,
this.description,
this.location = '',
required this.startTime,
required this.endTime,
this.isAllDay = false,
this.googleEventId,
this.taskId,
this.attendeeIds = const [],
this.attendeeEmails = const [],
this.color = '#2196F3',
this.recurringRuleId,
this.reminders = const {},
this.status = 'confirmed',
required this.createdBy,
required this.createdAt,
required this.updatedAt,
});
factory CalendarEventModel.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return CalendarEventModel(
id: doc.id,
title: data['title'] ?? '',
description: data['description'],
location: data['location'] ?? '',
startTime: (data['startTime'] as Timestamp).toDate(),
endTime: (data['endTime'] as Timestamp).toDate(),
isAllDay: data['isAllDay'] ?? false,
googleEventId: data['googleEventId'],
taskId: data['taskId'],
attendeeIds: List<String>.from(data['attendeeIds'] ?? []),
attendeeEmails: List<String>.from(data['attendeeEmails'] ?? []),
color: data['color'] ?? '#2196F3',
recurringRuleId: data['recurringRuleId'],
reminders: data['reminders'] ?? {},
status: data['status'] ?? 'confirmed',
createdBy: data['createdBy'] ?? '',
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
);
}
Map<String, dynamic> toFirestore() {
return {
'title': title,
'description': description,
'location': location,
'startTime': Timestamp.fromDate(startTime),
'endTime': Timestamp.fromDate(endTime),
'isAllDay': isAllDay,
'googleEventId': googleEventId,
'taskId': taskId,
'attendeeIds': attendeeIds,
'attendeeEmails': attendeeEmails,
'color': color,
'recurringRuleId': recurringRuleId,
'reminders': reminders,
'status': status,
'createdBy': createdBy,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
CalendarEventModel copyWith({
String? id,
String? title,
String? description,
String? location,
DateTime? startTime,
DateTime? endTime,
bool? isAllDay,
String? googleEventId,
String? taskId,
List<String>? attendeeIds,
List<String>? attendeeEmails,
String? color,
String? recurringRuleId,
Map<String, dynamic>? reminders,
String? status,
String? createdBy,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return CalendarEventModel(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
location: location ?? this.location,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
isAllDay: isAllDay ?? this.isAllDay,
googleEventId: googleEventId ?? this.googleEventId,
taskId: taskId ?? this.taskId,
attendeeIds: attendeeIds ?? this.attendeeIds,
attendeeEmails: attendeeEmails ?? this.attendeeEmails,
color: color ?? this.color,
recurringRuleId: recurringRuleId ?? this.recurringRuleId,
reminders: reminders ?? this.reminders,
status: status ?? this.status,
createdBy: createdBy ?? this.createdBy,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
Duration get duration => endTime.difference(startTime);
}
+165
View File
@@ -0,0 +1,165 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class MindMapNode {
final String id;
final String projectId;
final String? parentId;
final int level;
final String title;
final String? description;
final String? taskId; // Link to task
final String color;
final String icon;
final Map<String, dynamic> metadata;
final double x;
final double y;
final DateTime createdAt;
final DateTime updatedAt;
MindMapNode({
required this.id,
required this.projectId,
this.parentId,
required this.level,
required this.title,
this.description,
this.taskId,
this.color = '#2196F3',
this.icon = 'folder',
this.metadata = const {},
this.x = 0,
this.y = 0,
required this.createdAt,
required this.updatedAt,
});
factory MindMapNode.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return MindMapNode(
id: doc.id,
projectId: data['projectId'] ?? '',
parentId: data['parentId'],
level: data['level'] ?? 0,
title: data['title'] ?? '',
description: data['description'],
taskId: data['taskId'],
color: data['color'] ?? '#2196F3',
icon: data['icon'] ?? 'folder',
metadata: data['metadata'] ?? {},
x: (data['x'] ?? 0).toDouble(),
y: (data['y'] ?? 0).toDouble(),
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
);
}
Map<String, dynamic> toFirestore() {
return {
'projectId': projectId,
'parentId': parentId,
'level': level,
'title': title,
'description': description,
'taskId': taskId,
'color': color,
'icon': icon,
'metadata': metadata,
'x': x,
'y': y,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
MindMapNode copyWith({
String? id,
String? projectId,
String? parentId,
int? level,
String? title,
String? description,
String? taskId,
String? color,
String? icon,
Map<String, dynamic>? metadata,
double? x,
double? y,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return MindMapNode(
id: id ?? this.id,
projectId: projectId ?? this.projectId,
parentId: parentId ?? this.parentId,
level: level ?? this.level,
title: title ?? this.title,
description: description ?? this.description,
taskId: taskId ?? this.taskId,
color: color ?? this.color,
icon: icon ?? this.icon,
metadata: metadata ?? this.metadata,
x: x ?? this.x,
y: y ?? this.y,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
class MindMapModel {
final String id;
final String projectId;
final String title;
final String description;
final String rootNodeId;
final List<MindMapNode> nodes;
final Map<String, dynamic> layout;
final String createdBy;
final DateTime createdAt;
final DateTime updatedAt;
MindMapModel({
required this.id,
required this.projectId,
required this.title,
required this.description,
required this.rootNodeId,
required this.nodes,
required this.layout,
required this.createdBy,
required this.createdAt,
required this.updatedAt,
});
factory MindMapModel.fromFirestore({
required String id,
required Map<String, dynamic> data,
required List<MindMapNode> nodes,
}) {
return MindMapModel(
id: id,
projectId: data['projectId'] ?? '',
title: data['title'] ?? '',
description: data['description'] ?? '',
rootNodeId: data['rootNodeId'] ?? '',
nodes: nodes,
layout: data['layout'] ?? {},
createdBy: data['createdBy'] ?? '',
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
);
}
Map<String, dynamic> toFirestore() {
return {
'projectId': projectId,
'title': title,
'description': description,
'rootNodeId': rootNodeId,
'layout': layout,
'createdBy': createdBy,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
}
+117
View File
@@ -0,0 +1,117 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class ProjectModel {
final String id;
final String name;
final String description;
final String teamId;
final String ownerId;
final List<String> memberIds;
final String status;
final DateTime startDate;
final DateTime? endDate;
final String color;
final String icon;
final Map<String, dynamic> settings;
final DateTime createdAt;
final DateTime updatedAt;
ProjectModel({
required this.id,
required this.name,
required this.description,
required this.teamId,
required this.ownerId,
this.memberIds = const [],
this.status = 'active',
required this.startDate,
this.endDate,
this.color = '#1976D2',
this.icon = 'project',
this.settings = const {},
required this.createdAt,
required this.updatedAt,
});
factory ProjectModel.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return ProjectModel(
id: doc.id,
name: data['name'] ?? '',
description: data['description'] ?? '',
teamId: data['teamId'] ?? '',
ownerId: data['ownerId'] ?? '',
memberIds: List<String>.from(data['memberIds'] ?? []),
status: data['status'] ?? 'active',
startDate: (data['startDate'] as Timestamp).toDate(),
endDate: data['endDate'] != null
? (data['endDate'] as Timestamp).toDate()
: null,
color: data['color'] ?? '#1976D2',
icon: data['icon'] ?? 'project',
settings: data['settings'] ?? {},
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
);
}
Map<String, dynamic> toFirestore() {
return {
'name': name,
'description': description,
'teamId': teamId,
'ownerId': ownerId,
'memberIds': memberIds,
'status': status,
'startDate': Timestamp.fromDate(startDate),
'endDate': endDate != null ? Timestamp.fromDate(endDate!) : null,
'color': color,
'icon': icon,
'settings': settings,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
ProjectModel copyWith({
String? id,
String? name,
String? description,
String? teamId,
String? ownerId,
List<String>? memberIds,
String? status,
DateTime? startDate,
DateTime? endDate,
String? color,
String? icon,
Map<String, dynamic>? settings,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return ProjectModel(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
teamId: teamId ?? this.teamId,
ownerId: ownerId ?? this.ownerId,
memberIds: memberIds ?? this.memberIds,
status: status ?? this.status,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
color: color ?? this.color,
icon: icon ?? this.icon,
settings: settings ?? this.settings,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
bool isMember(String userId) {
return ownerId == userId || memberIds.contains(userId);
}
bool isOwner(String userId) {
return ownerId == userId;
}
}
+165
View File
@@ -0,0 +1,165 @@
import 'package:cloud_firestore/cloud_firestore.dart';
enum TaskStatus {
todo,
inProgress,
review,
done,
archived,
}
enum TaskPriority {
low,
medium,
high,
urgent,
}
class TaskModel {
final String id;
final String title;
final String? description;
final String projectId;
final String? parentTaskId; // For mind-map hierarchy
final TaskStatus status;
final TaskPriority priority;
final String? assigneeId;
final String? creatorId;
final List<String> tags;
final DateTime? dueDate;
final DateTime? startDate;
final DateTime? completedDate;
final int estimatedHours;
final int spentHours;
final Map<String, dynamic> aiMetadata; // AI-generated suggestions
final DateTime createdAt;
final DateTime updatedAt;
TaskModel({
required this.id,
required this.title,
this.description,
required this.projectId,
this.parentTaskId,
this.status = TaskStatus.todo,
this.priority = TaskPriority.medium,
this.assigneeId,
this.creatorId,
this.tags = const [],
this.dueDate,
this.startDate,
this.completedDate,
this.estimatedHours = 0,
this.spentHours = 0,
this.aiMetadata = const {},
required this.createdAt,
required this.updatedAt,
});
factory TaskModel.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return TaskModel(
id: doc.id,
title: data['title'] ?? '',
description: data['description'],
projectId: data['projectId'] ?? '',
parentTaskId: data['parentTaskId'],
status: TaskStatus.values.firstWhere(
(e) => e.name == data['status'],
orElse: () => TaskStatus.todo,
),
priority: TaskPriority.values.firstWhere(
(e) => e.name == data['priority'],
orElse: () => TaskPriority.medium,
),
assigneeId: data['assigneeId'],
creatorId: data['creatorId'],
tags: List<String>.from(data['tags'] ?? []),
dueDate: data['dueDate'] != null
? (data['dueDate'] as Timestamp).toDate()
: null,
startDate: data['startDate'] != null
? (data['startDate'] as Timestamp).toDate()
: null,
completedDate: data['completedDate'] != null
? (data['completedDate'] as Timestamp).toDate()
: null,
estimatedHours: data['estimatedHours'] ?? 0,
spentHours: data['spentHours'] ?? 0,
aiMetadata: data['aiMetadata'] ?? {},
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
);
}
Map<String, dynamic> toFirestore() {
return {
'title': title,
'description': description,
'projectId': projectId,
'parentTaskId': parentTaskId,
'status': status.name,
'priority': priority.name,
'assigneeId': assigneeId,
'creatorId': creatorId,
'tags': tags,
'dueDate': dueDate != null ? Timestamp.fromDate(dueDate!) : null,
'startDate': startDate != null ? Timestamp.fromDate(startDate!) : null,
'completedDate': completedDate != null
? Timestamp.fromDate(completedDate!)
: null,
'estimatedHours': estimatedHours,
'spentHours': spentHours,
'aiMetadata': aiMetadata,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
TaskModel copyWith({
String? id,
String? title,
String? description,
String? projectId,
String? parentTaskId,
TaskStatus? status,
TaskPriority? priority,
String? assigneeId,
String? creatorId,
List<String>? tags,
DateTime? dueDate,
DateTime? startDate,
DateTime? completedDate,
int? estimatedHours,
int? spentHours,
Map<String, dynamic>? aiMetadata,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return TaskModel(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
projectId: projectId ?? this.projectId,
parentTaskId: parentTaskId ?? this.parentTaskId,
status: status ?? this.status,
priority: priority ?? this.priority,
assigneeId: assigneeId ?? this.assigneeId,
creatorId: creatorId ?? this.creatorId,
tags: tags ?? this.tags,
dueDate: dueDate ?? this.dueDate,
startDate: startDate ?? this.startDate,
completedDate: completedDate ?? this.completedDate,
estimatedHours: estimatedHours ?? this.estimatedHours,
spentHours: spentHours ?? this.spentHours,
aiMetadata: aiMetadata ?? this.aiMetadata,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
bool get isOverdue {
if (dueDate == null) return false;
return DateTime.now().isAfter(dueDate!) && status != TaskStatus.done;
}
}
+90
View File
@@ -0,0 +1,90 @@
import 'package:cloud_firestore/cloud_firestore.dart';
enum UserRole {
admin,
member,
viewer,
}
class UserModel {
final String id;
final String email;
final String? displayName;
final String? photoUrl;
final UserRole role;
final List<String> teamIds;
final DateTime createdAt;
final DateTime updatedAt;
final Map<String, dynamic> settings;
UserModel({
required this.id,
required this.email,
this.displayName,
this.photoUrl,
this.role = UserRole.member,
this.teamIds = const [],
required this.createdAt,
required this.updatedAt,
this.settings = const {},
});
factory UserModel.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return UserModel(
id: doc.id,
email: data['email'] ?? '',
displayName: data['displayName'],
photoUrl: data['photoUrl'],
role: UserRole.values.firstWhere(
(e) => e.name == data['role'],
orElse: () => UserRole.member,
),
teamIds: List<String>.from(data['teamIds'] ?? []),
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
settings: data['settings'] ?? {},
);
}
Map<String, dynamic> toFirestore() {
return {
'email': email,
'displayName': displayName,
'photoUrl': photoUrl,
'role': role.name,
'teamIds': teamIds,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
'settings': settings,
};
}
UserModel copyWith({
String? id,
String? email,
String? displayName,
String? photoUrl,
UserRole? role,
List<String>? teamIds,
DateTime? createdAt,
DateTime? updatedAt,
Map<String, dynamic>? settings,
}) {
return UserModel(
id: id ?? this.id,
email: email ?? this.email,
displayName: displayName ?? this.displayName,
photoUrl: photoUrl ?? this.photoUrl,
role: role ?? this.role,
teamIds: teamIds ?? this.teamIds,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
settings: settings ?? this.settings,
);
}
bool get isAdmin => role == UserRole.admin;
bool get isMember => role == UserRole.member;
bool get isViewer => role == UserRole.viewer;
}
+84
View File
@@ -0,0 +1,84 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import '../models/user_model.dart';
class AuthNotifier extends StateNotifier<AsyncValue<UserModel?>> {
final FirebaseAuth _auth;
final GoogleSignIn _googleSignIn;
AuthNotifier(this._auth, this._googleSignIn) : super(const AsyncValue.data(null)) {
// Слушаем изменения состояния аутентификации
_auth.authStateChanges().listen((user) {
if (user != null) {
state = AsyncValue.data(UserModel(
id: user.uid,
email: user.email ?? '',
displayName: user.displayName,
photoUrl: user.photoURL,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
));
} else {
state = const AsyncValue.data(null);
}
});
}
Future<void> signInWithGoogle() async {
try {
state = const AsyncValue.loading();
final googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
state = const AsyncValue.data(null);
return;
}
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
final userCredential = await _auth.signInWithCredential(credential);
// Здесь можно создать/обновить пользователя в Firestore
state = AsyncValue.data(UserModel(
id: userCredential.user!.uid,
email: userCredential.user!.email ?? '',
displayName: userCredential.user!.displayName,
photoUrl: userCredential.user!.photoURL,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
));
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
Future<void> signOut() async {
try {
await _googleSignIn.signOut();
await _auth.signOut();
state = const AsyncValue.data(null);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
User? get currentUser => _auth.currentUser;
bool get isLoggedIn => _auth.currentUser != null;
}
final authProvider = StateNotifierProvider<AuthNotifier, AsyncValue<UserModel?>>((ref) {
return AuthNotifier(FirebaseAuth.instance, GoogleSignIn());
});
final authStateProvider = Provider<AuthNotifier>((ref) {
return ref.read(authProvider.notifier);
});
final isAuthenticatedProvider = Provider<bool>((ref) {
return ref.watch(authProvider).value != null;
});
+51
View File
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
class AlarmsScreen extends StatelessWidget {
const AlarmsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Будильники'),
),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) {
return _AlarmCard(index: index);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('Новый будильник'),
),
);
}
}
class _AlarmCard extends StatelessWidget {
final int index;
const _AlarmCard({required this.index});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: index % 2 == 0 ? Colors.blue : Colors.grey,
child: const Icon(Icons.alarm, color: Colors.white),
),
title: Text('Будильник #${index + 1}'),
subtitle: Text('${8 + index}:00'),
trailing: Switch(
value: index % 2 == 0,
onChanged: (value) {},
),
),
);
}
}
+79
View File
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../providers/auth_provider.dart';
class AuthScreen extends ConsumerWidget {
const AuthScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
return Scaffold(
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.business_center,
size: 120,
color: Colors.blue,
),
const SizedBox(height: 32),
const Text(
'NANAHUI CRM',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'CRM-ассистент для команд',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
const SizedBox(height: 48),
authState.when(
data: (user) {
if (user != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/home');
});
}
return const SizedBox.shrink();
},
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error',
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
ref.read(authProvider.notifier).signInWithGoogle();
},
icon: const Icon(Icons.login),
label: const Text('Войти через Google'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
),
],
),
),
),
),
);
}
}
+94
View File
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
class CalendarScreen extends StatelessWidget {
const CalendarScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Календарь'),
actions: [
IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
// Синхронизация с Google Calendar
},
),
],
),
body: Column(
children: [
_buildCalendarHeader(),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) {
return _CalendarEventCard(index: index);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
Widget _buildCalendarHeader() {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () {},
),
const Text(
'Март 2026',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () {},
),
],
),
);
}
}
class _CalendarEventCard extends StatelessWidget {
final int index;
const _CalendarEventCard({required this.index});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: Container(
width: 4,
height: 40,
decoration: BoxDecoration(
color: _getEventColor(index),
borderRadius: BorderRadius.circular(2),
),
),
title: Text('Событие #${index + 1}'),
subtitle: Text('10:00 - 11:00'),
trailing: const Icon(Icons.more_vert),
),
);
}
Color _getEventColor(int index) {
final colors = [Colors.blue, Colors.green, Colors.orange, Colors.purple];
return colors[index % colors.length];
}
}
+233
View File
@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('NANAHUI CRM'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeCard(),
const SizedBox(height: 24),
_buildQuickStats(),
const SizedBox(height: 24),
_buildQuickActions(context),
],
),
),
);
}
Widget _buildWelcomeCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Добро пожаловать!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Управляйте задачами команды и планируйте работу с помощью AI',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('Новая задача'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.auto_awesome),
label: const Text('AI План'),
),
),
],
),
],
),
),
);
}
Widget _buildQuickStats() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Быстрая статистика',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _StatCard(
icon: Icons.task,
label: 'Задачи',
value: '12',
color: Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _StatCard(
icon: Icons.pending_actions,
label: 'В работе',
value: '5',
color: Colors.orange,
),
),
const SizedBox(width: 12),
Expanded(
child: _StatCard(
icon: Icons.check_circle,
label: 'Готово',
value: '48',
color: Colors.green,
),
),
],
),
],
);
}
Widget _buildQuickActions(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Быстрые действия',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_ActionChip(
icon: Icons.calendar_today,
label: 'Планирование',
onTap: () => context.go('/home/calendar'),
),
_ActionChip(
icon: Icons.account_tree,
label: 'Mind-карта',
onTap: () => context.go('/home/mindmap'),
),
_ActionChip(
icon: Icons.insights,
label: 'Аналитика',
onTap: () {},
),
_ActionChip(
icon: Icons.people,
label: 'Команда',
onTap: () {},
),
],
),
],
);
}
}
class _StatCard extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final Color color;
const _StatCard({
required this.icon,
required this.label,
required this.value,
required this.color,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
);
}
}
class _ActionChip extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const _ActionChip({
required this.icon,
required this.label,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 20),
const SizedBox(width: 8),
Text(label),
],
),
),
);
}
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'dashboard_screen.dart';
import 'tasks_screen.dart';
import 'mind_map_screen.dart';
import 'calendar_screen.dart';
import 'alarms_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final List<Widget> _screens = [
const DashboardScreen(),
const TasksScreen(),
const MindMapScreen(),
const CalendarScreen(),
const AlarmsScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.dashboard_outlined),
selectedIcon: Icon(Icons.dashboard),
label: 'Главная',
),
NavigationDestination(
icon: Icon(Icons.task_outlined),
selectedIcon: Icon(Icons.task),
label: 'Задачи',
),
NavigationDestination(
icon: Icon(Icons.account_tree_outlined),
selectedIcon: Icon(Icons.account_tree),
label: 'Mind-карта',
),
NavigationDestination(
icon: Icon(Icons.calendar_today_outlined),
selectedIcon: Icon(Icons.calendar_today),
label: 'Календарь',
),
NavigationDestination(
icon: Icon(Icons.alarm_outlined),
selectedIcon: Icon(Icons.alarm),
label: 'Будильники',
),
],
),
);
}
}
+60
View File
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
class MindMapScreen extends StatelessWidget {
const MindMapScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Mind-карта'),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
// Edit mode - только для админов
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: () {},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.account_tree,
size: 100,
color: Colors.grey,
),
const SizedBox(height: 24),
const Text(
'Mind-карта проекта',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Создайте структуру задач в виде дерева',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('Создать mind-карту'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// AI генерация mind-карты
},
child: const Icon(Icons.auto_awesome),
),
);
}
}
+86
View File
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
class TasksScreen extends StatelessWidget {
const TasksScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Задачи'),
actions: [
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
],
),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 10,
itemBuilder: (context, index) {
return _TaskCard(index: index);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('Новая задача'),
),
);
}
}
class _TaskCard extends StatelessWidget {
final int index;
const _TaskCard({required this.index});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: _getPriorityColor(index),
child: Text('${index + 1}'),
),
title: Text('Задача #${index + 1}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text('Описание задачи...'),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.calendar_today, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Text(
'До: ${DateTime.now().add(Duration(days: index)).toString().split(' ')[0]}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
],
),
trailing: PopupMenuButton(
itemBuilder: (context) => [
const PopupMenuItem(value: 'edit', child: Text('Редактировать')),
const PopupMenuItem(value: 'delete', child: Text('Удалить')),
],
),
isThreeLine: true,
),
);
}
Color _getPriorityColor(int index) {
final colors = [Colors.red, Colors.orange, Colors.blue, Colors.green];
return colors[index % colors.length];
}
}