Add actionable push notifications and fix recurring task completion

Features:
- Add task action buttons to push notifications (complete, view, cancel, etc.)
- Add button types logic for different task states (overdue, in_progress, etc.)
- Implement Chain of Responsibility pattern for task categorization
- Add comprehensive kanban categorization documentation

Fixes:
- Reset recurring task status to Pending after completion so tasks appear
  in correct kanban column (was staying in "In Progress")
- Fix PostgreSQL EXTRACT function error in overdue notifications query
- Update seed data to properly set next_due_date for recurring tasks

Admin:
- Add tasks list to residence detail page
- Fix task edit page to properly handle all fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-05 14:23:14 -06:00
parent bbf3999c79
commit 1b06c0639c
22 changed files with 2715 additions and 142 deletions

View File

@@ -437,3 +437,90 @@ type RegisterDeviceRequest struct {
RegistrationID string `json:"registration_id" binding:"required"`
Platform string `json:"platform" binding:"required,oneof=ios android"`
}
// === Task Notifications with Actions ===
// CreateAndSendTaskNotification creates and sends a task notification with actionable buttons
// The backend always sends full notification data - the client decides how to display
// based on its locally cached subscription status
func (s *NotificationService) CreateAndSendTaskNotification(
ctx context.Context,
userID uint,
notificationType models.NotificationType,
task *models.Task,
) error {
// Check user notification preferences
prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
if err != nil {
return err
}
if !s.isNotificationEnabled(prefs, notificationType) {
return nil // Skip silently
}
// Build notification content - always send full data
title := GetTaskNotificationTitle(notificationType)
body := task.Title
// Get button types and iOS category based on task state
buttonTypes := GetButtonTypesForTask(task, 30) // 30 days threshold
iosCategoryID := GetIOSCategoryForTask(task)
// Build data payload - always includes full task info
// Client decides what to display based on local subscription status
data := map[string]interface{}{
"task_id": task.ID,
"task_name": task.Title,
"residence_id": task.ResidenceID,
"type": string(notificationType),
"button_types": buttonTypes,
"ios_category": iosCategoryID,
}
// Create notification record
dataJSON, _ := json.Marshal(data)
notification := &models.Notification{
UserID: userID,
NotificationType: notificationType,
Title: title,
Body: body,
Data: string(dataJSON),
TaskID: &task.ID,
}
if err := s.notificationRepo.Create(notification); err != nil {
return err
}
// Get device tokens
iosTokens, androidTokens, err := s.notificationRepo.GetActiveTokensForUser(userID)
if err != nil {
return err
}
// Convert data for push payload
pushData := make(map[string]string)
for k, v := range data {
switch val := v.(type) {
case string:
pushData[k] = val
case uint:
pushData[k] = strconv.FormatUint(uint64(val), 10)
default:
jsonVal, _ := json.Marshal(val)
pushData[k] = string(jsonVal)
}
}
pushData["notification_id"] = strconv.FormatUint(uint64(notification.ID), 10)
// Send push notification with actionable support
if s.pushClient != nil {
err = s.pushClient.SendActionableNotification(ctx, iosTokens, androidTokens, title, body, pushData, iosCategoryID)
if err != nil {
s.notificationRepo.SetError(notification.ID, err.Error())
return err
}
}
return s.notificationRepo.MarkAsSent(notification.ID)
}