✨ Добавление функционала напоминаний и улучшение сортировки задач
🔧 Изменения: - Добавлено поле `reminderMinutesBefore` в сущность задачи для настройки времени напоминания. - Обновлен метод `shouldSendReminder` для учета нового поля при проверке необходимости отправки напоминания. - Реализован выбор времени напоминания в Telegram-боте при создании задачи. - Добавлен метод `smartSortTasks` для умной сортировки задач по статусу, приоритету и срокам. - Обновлены методы получения задач с учетом новой сортировки. ✅ Теперь пользователи могут настраивать время напоминания и получать задачи в более удобном порядке.
This commit is contained in:
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bivekidaybot:
|
||||
build:
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<Task> {
|
||||
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<Task> {
|
||||
@ -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<Task[]> {
|
||||
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<Task[]> {
|
||||
const tasks = await this.findAllAccessibleTasks(userId, partnerUserId);
|
||||
return this.smartSortTasks(tasks);
|
||||
}
|
||||
|
||||
async findSharedTasksSorted(): Promise<Task[]> {
|
||||
const tasks = await this.findSharedTasks();
|
||||
return this.smartSortTasks(tasks);
|
||||
}
|
||||
}
|
@ -13,7 +13,11 @@ import {
|
||||
interface BotContext extends Context {
|
||||
session?: {
|
||||
step?: string;
|
||||
taskData?: Partial<CreateTaskDto & { dueDateString?: string; dueTimeString?: string }>;
|
||||
taskData?: Partial<CreateTaskDto & {
|
||||
dueDateString?: string;
|
||||
dueTimeString?: string;
|
||||
reminderMinutesBefore?: number;
|
||||
}>;
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user