288 lines
10 KiB
Ruby
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
|