Add per-user notification time preferences
Allow users to customize when they receive notification reminders: - Add task_due_soon_hour, task_overdue_hour, warranty_expiring_hour fields - Store times in UTC, clients convert to/from local timezone - Worker runs hourly, queries only users scheduled for that hour - Early exit optimization when no users need notifications - Admin UI displays custom notification times 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,11 @@ type NotificationPrefResponse struct {
|
||||
// Email preferences
|
||||
EmailTaskCompleted bool `json:"email_task_completed"`
|
||||
|
||||
// Custom notification times (UTC hour 0-23, null means use system default)
|
||||
TaskDueSoonHour *int `json:"task_due_soon_hour"`
|
||||
TaskOverdueHour *int `json:"task_overdue_hour"`
|
||||
WarrantyExpiringHour *int `json:"warranty_expiring_hour"`
|
||||
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
@@ -102,19 +107,22 @@ func (h *AdminNotificationPrefsHandler) List(c *gin.Context) {
|
||||
for i, pref := range prefs {
|
||||
user := userMap[pref.UserID]
|
||||
responses[i] = NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
TaskDueSoonHour: pref.TaskDueSoonHour,
|
||||
TaskOverdueHour: pref.TaskOverdueHour,
|
||||
WarrantyExpiringHour: pref.WarrantyExpiringHour,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,19 +151,22 @@ func (h *AdminNotificationPrefsHandler) Get(c *gin.Context) {
|
||||
h.db.First(&user, pref.UserID)
|
||||
|
||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
TaskDueSoonHour: pref.TaskDueSoonHour,
|
||||
TaskOverdueHour: pref.TaskOverdueHour,
|
||||
WarrantyExpiringHour: pref.WarrantyExpiringHour,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,6 +181,11 @@ type UpdateNotificationPrefRequest struct {
|
||||
|
||||
// Email preferences
|
||||
EmailTaskCompleted *bool `json:"email_task_completed"`
|
||||
|
||||
// Custom notification times (UTC hour 0-23)
|
||||
TaskDueSoonHour *int `json:"task_due_soon_hour"`
|
||||
TaskOverdueHour *int `json:"task_overdue_hour"`
|
||||
WarrantyExpiringHour *int `json:"warranty_expiring_hour"`
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/notification-prefs/:id
|
||||
@@ -219,6 +235,17 @@ func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
|
||||
pref.EmailTaskCompleted = *req.EmailTaskCompleted
|
||||
}
|
||||
|
||||
// Apply notification time updates
|
||||
if req.TaskDueSoonHour != nil {
|
||||
pref.TaskDueSoonHour = req.TaskDueSoonHour
|
||||
}
|
||||
if req.TaskOverdueHour != nil {
|
||||
pref.TaskOverdueHour = req.TaskOverdueHour
|
||||
}
|
||||
if req.WarrantyExpiringHour != nil {
|
||||
pref.WarrantyExpiringHour = req.WarrantyExpiringHour
|
||||
}
|
||||
|
||||
if err := h.db.Save(&pref).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification preference"})
|
||||
return
|
||||
@@ -228,19 +255,22 @@ func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
|
||||
h.db.First(&user, pref.UserID)
|
||||
|
||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
TaskDueSoonHour: pref.TaskDueSoonHour,
|
||||
TaskOverdueHour: pref.TaskOverdueHour,
|
||||
WarrantyExpiringHour: pref.WarrantyExpiringHour,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -287,18 +317,21 @@ func (h *AdminNotificationPrefsHandler) GetByUser(c *gin.Context) {
|
||||
h.db.First(&user, pref.UserID)
|
||||
|
||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||
TaskDueSoonHour: pref.TaskDueSoonHour,
|
||||
TaskOverdueHour: pref.TaskOverdueHour,
|
||||
WarrantyExpiringHour: pref.WarrantyExpiringHour,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user