From 45431ba6c256d11125a73ba38a5fbcaaadb250c7 Mon Sep 17 00:00:00 2001 From: Bivekich Date: Sun, 27 Jul 2025 21:00:37 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=BD=D0=B0=D0=BF=D0=BE?= =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=B8=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 Изменения: - Добавлено поле `reminderMinutesBefore` в сущность задачи для настройки времени напоминания. - Обновлен метод `shouldSendReminder` для учета нового поля при проверке необходимости отправки напоминания. - Реализован выбор времени напоминания в Telegram-боте при создании задачи. - Добавлен метод `smartSortTasks` для умной сортировки задач по статусу, приоритету и срокам. - Обновлены методы получения задач с учетом новой сортировки. ✅ Теперь пользователи могут настраивать время напоминания и получать задачи в более удобном порядке. --- docker-compose.yml | 2 - src/entities/task.entity.ts | 44 +++++++++-- src/services/task.service.ts | 83 +++++++++++++++++++- src/services/telegram-bot.service.ts | 108 ++++++++++++++++++++++++--- 4 files changed, 218 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b549a9c..144d90b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: bivekidaybot: build: diff --git a/src/entities/task.entity.ts b/src/entities/task.entity.ts index 93f143b..bc867dd 100644 --- a/src/entities/task.entity.ts +++ b/src/entities/task.entity.ts @@ -69,6 +69,10 @@ export class Task { @Column({ default: false }) reminderSent: boolean; + // Количество минут до дедлайна для напоминания (по умолчанию 60 минут) + @Column({ default: 60 }) + reminderMinutesBefore: number; + // Пользователь, которому назначена задача @ManyToOne(() => User, (user) => user.assignedTasks, { nullable: true }) @JoinColumn({ name: 'assigned_to_id' }) @@ -130,12 +134,38 @@ export class Task { // Проверка, нужно ли отправить напоминание shouldSendReminder(): boolean { - return ( - !this.reminderSent && - this.reminderDate && - new Date() >= this.reminderDate && - this.status !== TaskStatus.COMPLETED && - this.status !== TaskStatus.CANCELLED - ); + if (this.reminderSent || + this.status === TaskStatus.COMPLETED || + this.status === TaskStatus.CANCELLED) { + return false; + } + + const now = new Date(); + + // Если установлена конкретная дата напоминания, используем её + if (this.reminderDate) { + return now >= this.reminderDate; + } + + // Иначе проверяем по количеству минут до дедлайна + if (this.dueDate) { + const reminderTime = new Date(this.dueDate.getTime() - (this.reminderMinutesBefore * 60 * 1000)); + return now >= reminderTime; + } + + return false; + } + + // Получить время напоминания + getReminderTime(): Date | null { + if (this.reminderDate) { + return this.reminderDate; + } + + if (this.dueDate) { + return new Date(this.dueDate.getTime() - (this.reminderMinutesBefore * 60 * 1000)); + } + + return null; } } \ No newline at end of file diff --git a/src/services/task.service.ts b/src/services/task.service.ts index 5522876..c87185c 100644 --- a/src/services/task.service.ts +++ b/src/services/task.service.ts @@ -11,6 +11,7 @@ export interface CreateTaskDto { type?: TaskType; dueDate: Date; // Обязательное поле reminderDate?: Date; + reminderMinutesBefore?: number; // Напоминание за N минут до дедлайна assignedToId?: number; } @@ -128,7 +129,18 @@ export class TaskService { } async completeTask(id: number): Promise { - return this.updateTask(id, { status: TaskStatus.COMPLETED }); + const task = await this.updateTask(id, { status: TaskStatus.COMPLETED }); + + // Удаляем задачу после завершения (через 5 секунд для красивого эффекта) + setTimeout(async () => { + try { + await this.deleteTask(id); + } catch (error) { + // Игнорируем ошибки удаления (задача могла быть уже удалена) + } + }, 5000); + + return task; } async startTask(id: number): Promise { @@ -272,4 +284,73 @@ export class TaskService { } : null, }; } + + // Метод для умной сортировки задач + private smartSortTasks(tasks: Task[]): Task[] { + const priorityOrder = { + [TaskPriority.URGENT]: 4, + [TaskPriority.HIGH]: 3, + [TaskPriority.MEDIUM]: 2, + [TaskPriority.LOW]: 1, + }; + + const statusOrder = { + [TaskStatus.IN_PROGRESS]: 4, // В процессе - высший приоритет + [TaskStatus.PENDING]: 3, // Ожидающие - второй + [TaskStatus.CANCELLED]: 2, // Отмененные - третий + [TaskStatus.COMPLETED]: 1, // Завершенные - последние + }; + + return tasks.sort((a, b) => { + // 1. Сначала сортируем по статусу + const statusDiff = statusOrder[b.status] - statusOrder[a.status]; + if (statusDiff !== 0) return statusDiff; + + // 2. Затем по приоритету (для активных задач) + if (a.status !== TaskStatus.COMPLETED && b.status !== TaskStatus.COMPLETED) { + const priorityDiff = priorityOrder[b.priority] - priorityOrder[a.priority]; + if (priorityDiff !== 0) return priorityDiff; + } + + // 3. Затем по дате выполнения (просроченные и близкие по времени - первыми) + if (a.dueDate && b.dueDate) { + const now = new Date(); + const aDiff = Math.abs(a.dueDate.getTime() - now.getTime()); + const bDiff = Math.abs(b.dueDate.getTime() - now.getTime()); + + // Если одна из задач просрочена, она идет первой + if (a.dueDate < now && b.dueDate >= now) return -1; + if (b.dueDate < now && a.dueDate >= now) return 1; + + // Иначе сортируем по близости к текущему времени + return aDiff - bDiff; + } + + // 4. Если у одной задачи есть дата, а у другой нет + if (a.dueDate && !b.dueDate) return -1; + if (!a.dueDate && b.dueDate) return 1; + + // 5. Наконец, по дате создания (новые первыми) + return b.createdAt.getTime() - a.createdAt.getTime(); + }); + } + + // Обновляем методы для использования умной сортировки + async findTasksByUserSorted(userId: number): Promise { + const tasks = await this.taskRepository.find({ + where: [{ assignedToId: userId }, { createdById: userId }], + relations: ['assignedTo', 'createdBy'], + }); + return this.smartSortTasks(tasks); + } + + async findAllAccessibleTasksSorted(userId: number, partnerUserId?: number): Promise { + const tasks = await this.findAllAccessibleTasks(userId, partnerUserId); + return this.smartSortTasks(tasks); + } + + async findSharedTasksSorted(): Promise { + const tasks = await this.findSharedTasks(); + return this.smartSortTasks(tasks); + } } \ No newline at end of file diff --git a/src/services/telegram-bot.service.ts b/src/services/telegram-bot.service.ts index a81054d..a22486f 100644 --- a/src/services/telegram-bot.service.ts +++ b/src/services/telegram-bot.service.ts @@ -13,7 +13,11 @@ import { interface BotContext extends Context { session?: { step?: string; - taskData?: Partial; + taskData?: Partial; taskId?: number; [key: string]: any; }; @@ -183,6 +187,9 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} else if (data.startsWith('time_')) { await this.handleTimeSelection(ctx, data); } + else if (data.startsWith('reminder_')) { + await this.handleReminderSelection(ctx, data); + } // Просмотр задач else if (data === 'my_tasks') { await this.showMyTasks(ctx); @@ -459,6 +466,51 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} const user = await this.getCurrentUser(ctx); if (!user || !ctx.session?.taskData) return; + // Показываем выбор времени напоминания перед созданием задачи + ctx.session.step = 'task_reminder'; + + const keyboard = Markup.inlineKeyboard([ + [ + Markup.button.callback('🔔 За 15 мин', 'reminder_15'), + Markup.button.callback('🔔 За 30 мин', 'reminder_30'), + ], + [ + Markup.button.callback('🔔 За 1 час', 'reminder_60'), + Markup.button.callback('🔔 За 2 часа', 'reminder_120'), + ], + [ + Markup.button.callback('🔕 Без напоминания', 'reminder_none'), + ], + [Markup.button.callback('⬅️ Назад', 'task_time_back')], + ]); + + await ctx.reply( + `📝 *Создание задачи*\n\n` + + `📋 Название: ${ctx.session.taskData.title}\n` + + `📅 Дата: ${ctx.session.taskData.dueDateString}\n` + + `⏰ Время: ${ctx.session.taskData.dueTimeString}\n\n` + + '🔔 Когда напомнить о задаче?', + { + parse_mode: 'Markdown', + ...keyboard, + }, + ); + } + + private async handleReminderSelection(ctx: BotContext, data: string) { + const user = await this.getCurrentUser(ctx); + if (!user || !ctx.session?.taskData) return; + + const reminderMinutes = data === 'reminder_none' ? 0 : parseInt(data.replace('reminder_', '')); + ctx.session.taskData.reminderMinutesBefore = reminderMinutes; + + await this.finalizeTaskCreation(ctx); + } + + private async finalizeTaskCreation(ctx: BotContext) { + const user = await this.getCurrentUser(ctx); + if (!user || !ctx.session?.taskData) return; + try { // Парсим дату и время const dateStr = ctx.session.taskData.dueDateString!; @@ -474,6 +526,7 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} type: ctx.session.taskData.type!, priority: ctx.session.taskData.priority!, dueDate: dueDate, + reminderMinutesBefore: ctx.session.taskData.reminderMinutesBefore || 60, assignedToId: ctx.session.taskData.type === TaskType.PERSONAL ? user.id : undefined, }; @@ -491,6 +544,10 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} [Markup.button.callback('📋 Мои задачи', 'my_tasks')], ]); + const reminderText = task.reminderMinutesBefore > 0 + ? `🔔 Напоминание: за ${task.reminderMinutesBefore} мин\n` + : '🔕 Напоминание: отключено\n'; + await ctx.reply( `✅ *Задача создана!*\n\n` + `📝 ${task.title}\n` + @@ -498,6 +555,7 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} `${priorityEmojis[task.priority]} Приоритет: ${this.getPriorityText(task.priority)}\n` + `📅 Дата: ${dueDate.toLocaleDateString('ru-RU')}\n` + `⏰ Время: ${dueDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}\n` + + reminderText + `🆔 ID: ${task.id}`, { parse_mode: 'Markdown', @@ -538,7 +596,7 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} const user = await this.getCurrentUser(ctx); if (!user) return; - const tasks = await this.taskService.findTasksByUser(user.id); + const tasks = await this.taskService.findTasksByUserSorted(user.id); if (tasks.length === 0) { const keyboard = Markup.inlineKeyboard([ @@ -565,7 +623,7 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} const user = await this.getCurrentUser(ctx); if (!user) return; - const tasks = await this.taskService.findSharedTasks(); + const tasks = await this.taskService.findSharedTasksSorted(); if (tasks.length === 0) { const keyboard = Markup.inlineKeyboard([ @@ -608,8 +666,8 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} return; } - const tasks = await this.taskService.findAllAccessibleTasks(user.id, partner.id); - const partnerTasks = tasks.filter(t => + const allTasks = await this.taskService.findAllAccessibleTasksSorted(user.id, partner.id); + const partnerTasks = allTasks.filter(t => t.createdById === partner.id || (t.assignedToId === partner.id && t.type === TaskType.PERSONAL) ); @@ -654,9 +712,16 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} `${task.dueDate.toLocaleDateString('ru-RU')} в ${task.dueDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}` : 'Не установлен'; - message += `${task.getStatusEmoji()} *${task.title}* (ID: ${task.id})\n`; + // Проверяем, просрочена ли задача + const isOverdue = task.dueDate && task.dueDate < new Date() && + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.CANCELLED; + + const overdueIndicator = isOverdue ? '🚨 ' : ''; + + message += `${overdueIndicator}${task.getStatusEmoji()} *${task.title}* (ID: ${task.id})\n`; message += `${task.getPriorityEmoji()} ${task.getTypeEmoji()} Назначено: ${assignedTo}\n`; - message += `📅 Срок: ${dueDate}\n`; + message += `📅 Срок: ${dueDate}${isOverdue ? ' ⚠️ ПРОСРОЧЕНА' : ''}\n`; message += '\n'; // Добавляем кнопку для каждой задачи @@ -741,6 +806,14 @@ ${partner ? `👥 Партнер: ${partnerName} ${partnerEmoji}` : ''} message += `👤 *Назначено:* ${assignedTo}\n`; message += `👨‍💻 *Создал:* ${createdBy} ${task.createdBy.getRoleEmoji()}\n`; message += `📅 *Срок:* ${dueDate}\n`; + + const reminderTime = task.getReminderTime(); + if (reminderTime && task.reminderMinutesBefore > 0) { + message += `🔔 *Напоминание:* ${reminderTime.toLocaleDateString('ru-RU')} в ${reminderTime.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })} (за ${task.reminderMinutesBefore} мин)\n`; + } else { + message += `🔕 *Напоминание:* отключено\n`; + } + message += `🆔 *ID:* ${task.id}\n`; if (task.description) { @@ -1257,16 +1330,33 @@ ${extendedStats.shared.completed > 0 ? '🎉 Отличная командная [Markup.button.callback('📋 Подробнее', `task_view_${task.id}`)], ]); + // Вычисляем время до дедлайна + const now = new Date(); + const timeUntilDeadline = task.dueDate ? task.dueDate.getTime() - now.getTime() : 0; + const minutesUntilDeadline = Math.floor(timeUntilDeadline / (1000 * 60)); + + let timeText = ''; + if (minutesUntilDeadline <= 0) { + timeText = '⚠️ *Задача просрочена!*'; + } else if (minutesUntilDeadline < 60) { + timeText = `⏰ *Осталось: ${minutesUntilDeadline} мин*`; + } else { + const hours = Math.floor(minutesUntilDeadline / 60); + const mins = minutesUntilDeadline % 60; + timeText = `⏰ *Осталось: ${hours}ч ${mins}мин*`; + } + const message = ` -⏰ *Напоминание о задаче!* +🔔 *Напоминание о задаче!* 📝 ${task.title} ${task.getPriorityEmoji()} Приоритет: ${this.getPriorityText(task.priority)} ${task.getTypeEmoji()} Тип: ${task.type === TaskType.SHARED ? 'Общая' : 'Личная'} +${timeText} 🆔 ID: ${task.id} ${task.description ? `📄 ${task.description}\n` : ''} -Не забудьте выполнить задачу! 💪 +${minutesUntilDeadline <= 0 ? '🚨 Срочно!' : 'Не забудьте выполнить задачу! 💪'} `; try {