tududi/app/services/task_summary_service.rb
2025-06-09 07:30:00 +03:00

288 lines
10 KiB
Ruby

# app/services/task_summary_service.rb
require 'yaml'
class TaskSummaryService
# Helper method to escape special characters for MarkdownV2
def self.escape_markdown(text)
# Characters that need to be escaped in MarkdownV2: _*[]()~`>#+-=|{}.!
text.to_s.gsub(/([_*\[\]()~`>#+\-=|{}.!])/, '\\\\\1')
end
def self.generate_summary_for_user(user_id)
user = User.find_by(id: user_id)
return nil unless user
# Get today's tasks, in progress tasks, etc.
tasks = user.tasks
today = Date.today
due_today = tasks.where('DATE(due_date) = ?', today).where.not(status: 'done')
in_progress = tasks.where(status: 'in_progress')
completed_today = tasks.where(status: 'done').where('DATE(updated_at) = ?', today)
# Generate summary message
message = "📋 *Today's Task Summary*\n\n"
# Add a header divider
message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
# Start Today's Plan section
message += "✏️ *Today's Plan*\n\n"
# Add due today tasks to Today's Plan
# Add due today tasks to Today's Plan
if due_today.any?
message += "🚀 *Tasks Due Today:*\n"
due_today.order(:name).each_with_index do |task, index|
priority_emoji =
case task.priority
when 'high' then '🔴'
when 'medium' then '🟠'
when 'low' then '🟢'
else '⚪'
end
# Escape special characters in task name and project name
task_name = escape_markdown(task.name)
project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
message += "#{index + 1}\\. #{priority_emoji} #{task_name}#{project_info}\n"
end
message += "\n"
end
# Add in progress tasks to Today's Plan
if in_progress.any?
message += "⚙️ *In Progress Tasks:*\n"
in_progress.order(:name).each_with_index do |task, index|
priority_emoji =
case task.priority
when 'high' then '🔴'
when 'medium' then '🟠'
when 'low' then '🟢'
else '⚪'
end
# Escape special characters in task name and project name
task_name = escape_markdown(task.name)
project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
message += "#{index + 1}\\. #{priority_emoji} #{task_name}#{project_info}\n"
end
message += "\n"
end
# Add suggested tasks (not done, not in due today or in progress)
suggested_task_ids = due_today.pluck(:id) + in_progress.pluck(:id)
# Get tasks in expiring projects - same logic as Task.compute_metrics
tasks_in_expiring_projects = tasks
.where.not(status: 'done')
.where.not(id: suggested_task_ids)
.joins(:project)
.where('projects.due_date_at >= ?', today)
.where(projects: { active: true }) # Only active projects
.order(Arel.sql('projects.due_date_at ASC, tasks.priority DESC'))
# Get tasks not assigned to projects - same logic as Task.compute_metrics
tasks_without_projects = tasks
.where.not(status: 'done')
.where.not(id: suggested_task_ids)
.where(project_id: nil, status: 'not_started')
.order(priority: :desc)
# Combine both sets of tasks
combined_tasks = (tasks_in_expiring_projects + tasks_without_projects)
# Sort using same logic as Task.sort_suggested_tasks
suggested_tasks = combined_tasks.sort_by do |task|
# Parse or default the task due date
task_due_date = if task.due_date.is_a?(String)
Date.parse(task.due_date)
else
task.due_date || Date.new(9999, 12, 31)
end
# Parse or default the project due date
project_due_date = if task.project&.due_date_at.is_a?(String)
Date.parse(task&.project&.due_date_at)
else
task.project&.due_date_at || Date.new(9999, 12, 31)
end
# Priority in descending order (sorted values should be negative for sort_by)
priority_value = -Task.priorities.fetch(task.priority, -1)
# Determine sorting flags based on various criteria
is_high_priority_proj_with_due_date = task.priority == 'high' && task.project&.due_date_at ? 0 : 1
is_high_priority_with_due_date = task.priority == 'high' && task.due_date ? 0 : 1
is_high_priority = task.priority == 'high' && !task.due_date && !task.project&.due_date_at ? 0 : 1
is_medium_priority_proj_with_due_date = task.priority == 'medium' && task.project&.due_date_at ? 0 : 1
is_medium_priority_with_due_date = task.priority == 'medium' && task.due_date ? 0 : 1
is_medium_priority = task.priority == 'medium' && !task.due_date && !task.project&.due_date_at ? 0 : 1
is_low_priority_proj_with_due_date = task.priority == 'low' && task.project&.due_date_at ? 0 : 1
is_low_priority_with_due_date = task.priority == 'low' && task.due_date ? 0 : 1
is_low_priority = task.priority == 'low' && !task.due_date && !task.project&.due_date_at ? 0 : 1
# Primary sorting criteria - same as Task.sort_suggested_tasks
[
is_high_priority_proj_with_due_date,
is_high_priority_with_due_date,
is_high_priority,
is_medium_priority_proj_with_due_date,
is_medium_priority_with_due_date,
is_medium_priority,
is_low_priority_proj_with_due_date,
is_low_priority_with_due_date,
is_low_priority,
task_due_date,
project_due_date,
priority_value
]
end.first(5)
if suggested_tasks.any?
message += "💡 *Suggested Tasks \\(Top 3\\):*\n"
# Only display the top 3 suggested tasks
suggested_tasks.first(5).each_with_index do |task, index|
priority_emoji =
case task.priority
when 'high' then '🔴'
when 'medium' then '🟠'
when 'low' then '🟢'
else '⚪'
end
# Escape special characters in task name and project name
task_name = escape_markdown(task.name)
project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
due_date = task.due_date ? " \\(Due: #{escape_markdown(task.due_date.strftime('%b %d'))}\\)" : ''
message += "#{index + 1}\\. #{priority_emoji} #{task_name}#{project_info}#{due_date}\n"
end
message += "\n"
end
# Add a section divider
message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
# Add completed tasks for today if any
if completed_today.any?
message += "✅ *Completed Today:*\n"
completed_today.order(updated_at: :desc).each_with_index do |task, index|
# Escape special characters in task name and project name
task_name = escape_markdown(task.name)
project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
message += "#{index + 1}\\. #{task_name}#{project_info}\n"
end
message += "\n"
end
# Add inbox count if available
inbox_items_count = user.inbox_items.where(status: 'added').count
if inbox_items_count > 0
message += "*Inbox:*\n"
message += "• You have #{inbox_items_count} item\\(s\\) in your inbox to process\\.\n\n"
end
# Add a section divider
message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
# Add a motivational note from the YAML file
begin
quotes_file = Rails.root.join('config', 'quotes.yml')
quotes_data = YAML.load_file(quotes_file)['quotes']
message += "💪 *Today's Motivation:*\n"
quote = quotes_data.sample
# Escape special characters in the quote
message += escape_markdown(quote)
rescue StandardError => e
# Fallback to default quotes if there's an issue loading from YAML
default_quotes = [
'Focus on progress, not perfection.',
'One task at a time leads to great accomplishments.',
"Today's effort is tomorrow's success.",
'Small steps every day lead to big results.'
]
message += "💪 *Today's Motivation:*\n"
quote = default_quotes.sample
# Escape special characters in the quote
message += escape_markdown(quote)
end
message
end
def self.send_summary_to_user(user_id)
user = User.find_by(id: user_id)
return false unless user && user.telegram_bot_token && user.telegram_chat_id
summary = generate_summary_for_user(user_id)
return false unless summary
# Send the message via Telegram
begin
TelegramPoller.instance.send_telegram_message(
user.telegram_bot_token,
user.telegram_chat_id,
summary
)
# Update the last run time and calculate the next run time
now = Time.now
next_run = calculate_next_run_time(user, now)
# Update the user's tracking fields
user.update(
task_summary_last_run: now,
task_summary_next_run: next_run
)
true
rescue StandardError => e
puts "Error sending task summary to user #{user_id}: #{e.message}"
false
end
end
# Calculate when the next task summary should run based on frequency
def self.calculate_next_run_time(user, from_time = Time.now)
case user.task_summary_frequency
when 'daily'
# Next day at 7 AM
from_time.tomorrow.change(hour: 7, min: 0, sec: 0)
when 'weekdays'
# If it's Friday, next is Monday, otherwise next day (if it's a weekday)
days_until_next_weekday =
if from_time.wday == 5 # Friday
3 # Next Monday
elsif from_time.wday == 6 # Saturday
2 # Next Monday
else
1 # Next day
end
from_time.advance(days: days_until_next_weekday).change(hour: 7, min: 0, sec: 0)
when 'weekly'
# Next week same day, or next Monday if we're being specific
from_time.advance(days: 7).change(hour: 7, min: 0, sec: 0)
when '1h'
from_time + 1.hour
when '2h'
from_time + 2.hours
when '4h'
from_time + 4.hours
when '8h'
from_time + 8.hours
when '12h'
from_time + 12.hours
else
# Default to daily at 7 AM
from_time.tomorrow.change(hour: 7, min: 0, sec: 0)
end
end
end