Add alarms to tasks, admin role from DB
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# 📝 Настройка админов в Firestore
|
||||
|
||||
## Админы NANAXE CRM
|
||||
|
||||
### Email админов:
|
||||
1. `almalexyz@gmail.com`
|
||||
2. `ormix16@gmail.com`
|
||||
|
||||
## Как назначить админа:
|
||||
|
||||
### Способ 1: Через Firebase Console
|
||||
|
||||
1. Открой https://console.firebase.google.com/project/nanax26-1f33b/firestore
|
||||
2. Найди коллекцию `users`
|
||||
3. Найди документ пользователя по email
|
||||
4. Измени поле `role` на `"admin"`
|
||||
|
||||
### Способ 2: Через Firestore запрос
|
||||
|
||||
```javascript
|
||||
// В Firebase Console → Firestore → Start collection
|
||||
db.collection('users').where('email', '==', 'almalexyz@gmail.com').get()
|
||||
.then(snapshot => {
|
||||
snapshot.docs.forEach(doc => {
|
||||
doc.ref.update({ role: 'admin' });
|
||||
});
|
||||
});
|
||||
|
||||
db.collection('users').where('email', '==', 'ormix16@gmail.com').get()
|
||||
.then(snapshot => {
|
||||
snapshot.docs.forEach(doc => {
|
||||
doc.ref.update({ role: 'admin' });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Способ 3: Через Cloud Functions (автоматически)
|
||||
|
||||
```javascript
|
||||
exports.setOnAdminLogin = functions.auth.user().onCreate(async (user) => {
|
||||
const adminEmails = ['almalexyz@gmail.com', 'ormix16@gmail.com'];
|
||||
|
||||
if (adminEmails.includes(user.email)) {
|
||||
await admin.firestore().collection('users').doc(user.uid).update({
|
||||
role: 'admin'
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Проверка роли:
|
||||
|
||||
В коде приложения:
|
||||
```dart
|
||||
final user = ref.watch(currentUserProvider);
|
||||
if (user?.isAdmin ?? false) {
|
||||
// Показать админку
|
||||
}
|
||||
```
|
||||
|
||||
## Роли:
|
||||
|
||||
- `admin` - полный доступ ко всем функциям
|
||||
- `member` - создание задач, редактирование своих
|
||||
- `viewer` - только просмотр
|
||||
@@ -1,5 +1,46 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
// Данные будильника
|
||||
class AlarmData {
|
||||
final String id;
|
||||
final DateTime dateTime;
|
||||
final bool isRepeating;
|
||||
final String repeatPattern;
|
||||
final List<int> repeatDays;
|
||||
final bool isEnabled;
|
||||
|
||||
AlarmData({
|
||||
required this.id,
|
||||
required this.dateTime,
|
||||
this.isRepeating = false,
|
||||
this.repeatPattern = 'daily',
|
||||
this.repeatDays = const [],
|
||||
this.isEnabled = true,
|
||||
});
|
||||
|
||||
factory AlarmData.fromMap(Map<String, dynamic> map) {
|
||||
return AlarmData(
|
||||
id: map['id'] ?? '',
|
||||
dateTime: (map['dateTime'] as Timestamp).toDate(),
|
||||
isRepeating: map['isRepeating'] ?? false,
|
||||
repeatPattern: map['repeatPattern'] ?? 'daily',
|
||||
repeatDays: List<int>.from(map['repeatDays'] ?? []),
|
||||
isEnabled: map['isEnabled'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'dateTime': Timestamp.fromDate(dateTime),
|
||||
'isRepeating': isRepeating,
|
||||
'repeatPattern': repeatPattern,
|
||||
'repeatDays': repeatDays,
|
||||
'isEnabled': isEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum TaskStatus {
|
||||
todo,
|
||||
inProgress,
|
||||
@@ -29,6 +70,7 @@ class TaskModel {
|
||||
final DateTime? dueDate;
|
||||
final DateTime? startDate;
|
||||
final DateTime? completedDate;
|
||||
final List<AlarmData> alarms; // Будильники для задачи
|
||||
final int estimatedHours;
|
||||
final int spentHours;
|
||||
final Map<String, dynamic> aiMetadata; // AI-generated suggestions
|
||||
@@ -49,6 +91,7 @@ class TaskModel {
|
||||
this.dueDate,
|
||||
this.startDate,
|
||||
this.completedDate,
|
||||
this.alarms = const [],
|
||||
this.estimatedHours = 0,
|
||||
this.spentHours = 0,
|
||||
this.aiMetadata = const {},
|
||||
@@ -84,6 +127,10 @@ class TaskModel {
|
||||
completedDate: data['completedDate'] != null
|
||||
? (data['completedDate'] as Timestamp).toDate()
|
||||
: null,
|
||||
alarms: (data['alarms'] as List<dynamic>?)
|
||||
?.map((a) => AlarmData.fromMap(a as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
estimatedHours: data['estimatedHours'] ?? 0,
|
||||
spentHours: data['spentHours'] ?? 0,
|
||||
aiMetadata: data['aiMetadata'] ?? {},
|
||||
@@ -105,9 +152,9 @@ class TaskModel {
|
||||
'tags': tags,
|
||||
'dueDate': dueDate != null ? Timestamp.fromDate(dueDate!) : null,
|
||||
'startDate': startDate != null ? Timestamp.fromDate(startDate!) : null,
|
||||
'completedDate': completedDate != null
|
||||
? Timestamp.fromDate(completedDate!)
|
||||
: null,
|
||||
'completedDate':
|
||||
completedDate != null ? Timestamp.fromDate(completedDate!) : null,
|
||||
'alarms': alarms.map((a) => a.toMap()).toList(),
|
||||
'estimatedHours': estimatedHours,
|
||||
'spentHours': spentHours,
|
||||
'aiMetadata': aiMetadata,
|
||||
@@ -130,6 +177,7 @@ class TaskModel {
|
||||
DateTime? dueDate,
|
||||
DateTime? startDate,
|
||||
DateTime? completedDate,
|
||||
List<AlarmData>? alarms,
|
||||
int? estimatedHours,
|
||||
int? spentHours,
|
||||
Map<String, dynamic>? aiMetadata,
|
||||
@@ -150,6 +198,7 @@ class TaskModel {
|
||||
dueDate: dueDate ?? this.dueDate,
|
||||
startDate: startDate ?? this.startDate,
|
||||
completedDate: completedDate ?? this.completedDate,
|
||||
alarms: alarms ?? this.alarms,
|
||||
estimatedHours: estimatedHours ?? this.estimatedHours,
|
||||
spentHours: spentHours ?? this.spentHours,
|
||||
aiMetadata: aiMetadata ?? this.aiMetadata,
|
||||
|
||||
@@ -10,7 +10,7 @@ class AdminScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final isAdmin = user?.isAdmin ?? false;
|
||||
final isAdmin = user?.role == UserRole.admin;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 DashboardScreen extends StatelessWidget {
|
||||
const DashboardScreen({super.key});
|
||||
@@ -10,10 +12,20 @@ class DashboardScreen extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('NANAXE CRM'),
|
||||
actions: [
|
||||
IconButton(
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final isAdmin = user?.role == UserRole.admin;
|
||||
|
||||
if (isAdmin ?? false) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.admin_panel_settings),
|
||||
onPressed: () => context.push('/home/admin'),
|
||||
tooltip: 'Админка',
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.auto_awesome),
|
||||
|
||||
@@ -20,7 +20,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
const TasksScreen(),
|
||||
const MindMapScreen(),
|
||||
const CalendarScreen(),
|
||||
const AlarmsScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -55,11 +54,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
selectedIcon: Icon(Icons.calendar_today),
|
||||
label: 'Календарь',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.alarm_outlined),
|
||||
selectedIcon: Icon(Icons.alarm),
|
||||
label: 'Будильники',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
TaskPriority _priority = TaskPriority.medium;
|
||||
DateTime? _dueDate;
|
||||
int _estimatedHours = 0;
|
||||
List<AlarmData> _alarms = [];
|
||||
bool _isGenerating = false;
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user