Add email notifications for task completions
When a task is completed, now sends both: - Push notification (via Gorush) - Email notification (via SMTP) to all residence users except the person who completed the task. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -75,8 +75,9 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
|
|||||||
documentService := services.NewDocumentService(documentRepo, residenceRepo)
|
documentService := services.NewDocumentService(documentRepo, residenceRepo)
|
||||||
notificationService := services.NewNotificationService(notificationRepo, gorushClient)
|
notificationService := services.NewNotificationService(notificationRepo, gorushClient)
|
||||||
|
|
||||||
// Wire up notification service to task service (for task completion notifications)
|
// Wire up notification and email services to task service (for task completion notifications)
|
||||||
taskService.SetNotificationService(notificationService)
|
taskService.SetNotificationService(notificationService)
|
||||||
|
taskService.SetEmailService(deps.EmailService)
|
||||||
subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
|
subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
|
||||||
|
|
||||||
// Initialize middleware
|
// Initialize middleware
|
||||||
|
|||||||
@@ -280,6 +280,68 @@ The MyCrib Team
|
|||||||
return s.SendEmail(to, subject, htmlBody, textBody)
|
return s.SendEmail(to, subject, htmlBody, textBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendTaskCompletedEmail sends an email notification when a task is completed
|
||||||
|
func (s *EmailService) SendTaskCompletedEmail(to, recipientName, taskTitle, completedByName, residenceName string) error {
|
||||||
|
subject := fmt.Sprintf("MyCrib - Task Completed: %s", taskTitle)
|
||||||
|
|
||||||
|
name := recipientName
|
||||||
|
if name == "" {
|
||||||
|
name = "there"
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody := fmt.Sprintf(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { text-align: center; padding: 20px 0; }
|
||||||
|
.task-box { background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||||
|
.task-title { font-size: 18px; font-weight: bold; color: #155724; margin: 0; }
|
||||||
|
.task-meta { color: #666; font-size: 14px; margin-top: 10px; }
|
||||||
|
.footer { text-align: center; color: #666; font-size: 12px; margin-top: 40px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Task Completed!</h1>
|
||||||
|
</div>
|
||||||
|
<p>Hi %s,</p>
|
||||||
|
<p>A task has been completed at <strong>%s</strong>:</p>
|
||||||
|
<div class="task-box">
|
||||||
|
<p class="task-title">%s</p>
|
||||||
|
<p class="task-meta">Completed by: %s<br>Completed on: %s</p>
|
||||||
|
</div>
|
||||||
|
<p>Best regards,<br>The MyCrib Team</p>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© %d MyCrib. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, name, residenceName, taskTitle, completedByName, time.Now().UTC().Format("January 2, 2006 at 3:04 PM"), time.Now().Year())
|
||||||
|
|
||||||
|
textBody := fmt.Sprintf(`
|
||||||
|
Task Completed!
|
||||||
|
|
||||||
|
Hi %s,
|
||||||
|
|
||||||
|
A task has been completed at %s:
|
||||||
|
|
||||||
|
Task: %s
|
||||||
|
Completed by: %s
|
||||||
|
Completed on: %s
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
The MyCrib Team
|
||||||
|
`, name, residenceName, taskTitle, completedByName, time.Now().UTC().Format("January 2, 2006 at 3:04 PM"))
|
||||||
|
|
||||||
|
return s.SendEmail(to, subject, htmlBody, textBody)
|
||||||
|
}
|
||||||
|
|
||||||
// EmailTemplate represents an email template
|
// EmailTemplate represents an email template
|
||||||
type EmailTemplate struct {
|
type EmailTemplate struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type TaskService struct {
|
|||||||
taskRepo *repositories.TaskRepository
|
taskRepo *repositories.TaskRepository
|
||||||
residenceRepo *repositories.ResidenceRepository
|
residenceRepo *repositories.ResidenceRepository
|
||||||
notificationService *NotificationService
|
notificationService *NotificationService
|
||||||
|
emailService *EmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTaskService creates a new task service
|
// NewTaskService creates a new task service
|
||||||
@@ -44,6 +45,11 @@ func (s *TaskService) SetNotificationService(ns *NotificationService) {
|
|||||||
s.notificationService = ns
|
s.notificationService = ns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetEmailService sets the email service
|
||||||
|
func (s *TaskService) SetEmailService(es *EmailService) {
|
||||||
|
s.emailService = es
|
||||||
|
}
|
||||||
|
|
||||||
// === Task CRUD ===
|
// === Task CRUD ===
|
||||||
|
|
||||||
// GetTask gets a task by ID with access check
|
// GetTask gets a task by ID with access check
|
||||||
@@ -485,11 +491,6 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
|||||||
|
|
||||||
// sendTaskCompletedNotification sends notifications when a task is completed
|
// sendTaskCompletedNotification sends notifications when a task is completed
|
||||||
func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) {
|
func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) {
|
||||||
if s.notificationService == nil {
|
|
||||||
log.Debug().Msg("Notification service not configured, skipping task completion notification")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all users with access to this residence
|
// Get all users with access to this residence
|
||||||
users, err := s.residenceRepo.GetResidenceUsers(task.ResidenceID)
|
users, err := s.residenceRepo.GetResidenceUsers(task.ResidenceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -497,6 +498,13 @@ func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completio
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get residence name
|
||||||
|
residence, err := s.residenceRepo.FindByIDSimple(task.ResidenceID)
|
||||||
|
residenceName := "your property"
|
||||||
|
if err == nil && residence != nil {
|
||||||
|
residenceName = residence.Name
|
||||||
|
}
|
||||||
|
|
||||||
completedByName := "Someone"
|
completedByName := "Someone"
|
||||||
if completion.CompletedBy.ID > 0 {
|
if completion.CompletedBy.ID > 0 {
|
||||||
completedByName = completion.CompletedBy.GetFullName()
|
completedByName = completion.CompletedBy.GetFullName()
|
||||||
@@ -517,19 +525,39 @@ func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completio
|
|||||||
continue // Don't notify the person who completed it
|
continue // Don't notify the person who completed it
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(userID uint) {
|
// Send push notification
|
||||||
ctx := context.Background()
|
if s.notificationService != nil {
|
||||||
if err := s.notificationService.CreateAndSendNotification(
|
go func(userID uint) {
|
||||||
ctx,
|
ctx := context.Background()
|
||||||
userID,
|
if err := s.notificationService.CreateAndSendNotification(
|
||||||
models.NotificationTaskCompleted,
|
ctx,
|
||||||
title,
|
userID,
|
||||||
body,
|
models.NotificationTaskCompleted,
|
||||||
data,
|
title,
|
||||||
); err != nil {
|
body,
|
||||||
log.Error().Err(err).Uint("user_id", userID).Uint("task_id", task.ID).Msg("Failed to send task completion notification")
|
data,
|
||||||
}
|
); err != nil {
|
||||||
}(user.ID)
|
log.Error().Err(err).Uint("user_id", userID).Uint("task_id", task.ID).Msg("Failed to send task completion push notification")
|
||||||
|
}
|
||||||
|
}(user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send email notification
|
||||||
|
if s.emailService != nil && user.Email != "" {
|
||||||
|
go func(u models.User) {
|
||||||
|
if err := s.emailService.SendTaskCompletedEmail(
|
||||||
|
u.Email,
|
||||||
|
u.GetFullName(),
|
||||||
|
task.Title,
|
||||||
|
completedByName,
|
||||||
|
residenceName,
|
||||||
|
); err != nil {
|
||||||
|
log.Error().Err(err).Str("email", u.Email).Uint("task_id", task.ID).Msg("Failed to send task completion email")
|
||||||
|
} else {
|
||||||
|
log.Info().Str("email", u.Email).Uint("task_id", task.ID).Msg("Task completion email sent")
|
||||||
|
}
|
||||||
|
}(user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user