Add email notification preference for task completion
- Add EmailTaskCompleted field to NotificationPreference model - Update notification repository to include email preference in queries - Check email preference before sending task completion emails - Add email preference toggle to admin panel notification-prefs page - Update API types for email preference support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -156,6 +156,16 @@ export default function NotificationPrefsPage() {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'email_task_completed',
|
||||||
|
header: 'Email: Completed',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Switch
|
||||||
|
checked={row.original.email_task_completed}
|
||||||
|
onCheckedChange={() => handleToggle(row.original.id, 'email_task_completed', row.original.email_task_completed)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -194,7 +204,7 @@ export default function NotificationPrefsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">Notification Preferences</h1>
|
<h1 className="text-2xl font-bold">Notification Preferences</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Manage user notification preferences for push notifications
|
Manage user notification preferences for push notifications and emails
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -597,6 +597,8 @@ export interface NotificationPreference {
|
|||||||
task_assigned: boolean;
|
task_assigned: boolean;
|
||||||
residence_shared: boolean;
|
residence_shared: boolean;
|
||||||
warranty_expiring: boolean;
|
warranty_expiring: boolean;
|
||||||
|
// Email preferences
|
||||||
|
email_task_completed: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -616,6 +618,8 @@ export interface UpdateNotificationPrefRequest {
|
|||||||
task_assigned?: boolean;
|
task_assigned?: boolean;
|
||||||
residence_shared?: boolean;
|
residence_shared?: boolean;
|
||||||
warranty_expiring?: boolean;
|
warranty_expiring?: boolean;
|
||||||
|
// Email preferences
|
||||||
|
email_task_completed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification Preferences API
|
// Notification Preferences API
|
||||||
|
|||||||
@@ -33,8 +33,12 @@ type NotificationPrefResponse struct {
|
|||||||
TaskAssigned bool `json:"task_assigned"`
|
TaskAssigned bool `json:"task_assigned"`
|
||||||
ResidenceShared bool `json:"residence_shared"`
|
ResidenceShared bool `json:"residence_shared"`
|
||||||
WarrantyExpiring bool `json:"warranty_expiring"`
|
WarrantyExpiring bool `json:"warranty_expiring"`
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
UpdatedAt string `json:"updated_at"`
|
// Email preferences
|
||||||
|
EmailTaskCompleted bool `json:"email_task_completed"`
|
||||||
|
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// List handles GET /api/admin/notification-prefs
|
// List handles GET /api/admin/notification-prefs
|
||||||
@@ -98,18 +102,19 @@ func (h *AdminNotificationPrefsHandler) List(c *gin.Context) {
|
|||||||
for i, pref := range prefs {
|
for i, pref := range prefs {
|
||||||
user := userMap[pref.UserID]
|
user := userMap[pref.UserID]
|
||||||
responses[i] = NotificationPrefResponse{
|
responses[i] = NotificationPrefResponse{
|
||||||
ID: pref.ID,
|
ID: pref.ID,
|
||||||
UserID: pref.UserID,
|
UserID: pref.UserID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
TaskDueSoon: pref.TaskDueSoon,
|
TaskDueSoon: pref.TaskDueSoon,
|
||||||
TaskOverdue: pref.TaskOverdue,
|
TaskOverdue: pref.TaskOverdue,
|
||||||
TaskCompleted: pref.TaskCompleted,
|
TaskCompleted: pref.TaskCompleted,
|
||||||
TaskAssigned: pref.TaskAssigned,
|
TaskAssigned: pref.TaskAssigned,
|
||||||
ResidenceShared: pref.ResidenceShared,
|
ResidenceShared: pref.ResidenceShared,
|
||||||
WarrantyExpiring: pref.WarrantyExpiring,
|
WarrantyExpiring: pref.WarrantyExpiring,
|
||||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,18 +143,19 @@ func (h *AdminNotificationPrefsHandler) Get(c *gin.Context) {
|
|||||||
h.db.First(&user, pref.UserID)
|
h.db.First(&user, pref.UserID)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||||
ID: pref.ID,
|
ID: pref.ID,
|
||||||
UserID: pref.UserID,
|
UserID: pref.UserID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
TaskDueSoon: pref.TaskDueSoon,
|
TaskDueSoon: pref.TaskDueSoon,
|
||||||
TaskOverdue: pref.TaskOverdue,
|
TaskOverdue: pref.TaskOverdue,
|
||||||
TaskCompleted: pref.TaskCompleted,
|
TaskCompleted: pref.TaskCompleted,
|
||||||
TaskAssigned: pref.TaskAssigned,
|
TaskAssigned: pref.TaskAssigned,
|
||||||
ResidenceShared: pref.ResidenceShared,
|
ResidenceShared: pref.ResidenceShared,
|
||||||
WarrantyExpiring: pref.WarrantyExpiring,
|
WarrantyExpiring: pref.WarrantyExpiring,
|
||||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +167,9 @@ type UpdateNotificationPrefRequest struct {
|
|||||||
TaskAssigned *bool `json:"task_assigned"`
|
TaskAssigned *bool `json:"task_assigned"`
|
||||||
ResidenceShared *bool `json:"residence_shared"`
|
ResidenceShared *bool `json:"residence_shared"`
|
||||||
WarrantyExpiring *bool `json:"warranty_expiring"`
|
WarrantyExpiring *bool `json:"warranty_expiring"`
|
||||||
|
|
||||||
|
// Email preferences
|
||||||
|
EmailTaskCompleted *bool `json:"email_task_completed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update handles PUT /api/admin/notification-prefs/:id
|
// Update handles PUT /api/admin/notification-prefs/:id
|
||||||
@@ -206,6 +215,9 @@ func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
|
|||||||
if req.WarrantyExpiring != nil {
|
if req.WarrantyExpiring != nil {
|
||||||
pref.WarrantyExpiring = *req.WarrantyExpiring
|
pref.WarrantyExpiring = *req.WarrantyExpiring
|
||||||
}
|
}
|
||||||
|
if req.EmailTaskCompleted != nil {
|
||||||
|
pref.EmailTaskCompleted = *req.EmailTaskCompleted
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.db.Save(&pref).Error; err != nil {
|
if err := h.db.Save(&pref).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification preference"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification preference"})
|
||||||
@@ -216,18 +228,19 @@ func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
|
|||||||
h.db.First(&user, pref.UserID)
|
h.db.First(&user, pref.UserID)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||||
ID: pref.ID,
|
ID: pref.ID,
|
||||||
UserID: pref.UserID,
|
UserID: pref.UserID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
TaskDueSoon: pref.TaskDueSoon,
|
TaskDueSoon: pref.TaskDueSoon,
|
||||||
TaskOverdue: pref.TaskOverdue,
|
TaskOverdue: pref.TaskOverdue,
|
||||||
TaskCompleted: pref.TaskCompleted,
|
TaskCompleted: pref.TaskCompleted,
|
||||||
TaskAssigned: pref.TaskAssigned,
|
TaskAssigned: pref.TaskAssigned,
|
||||||
ResidenceShared: pref.ResidenceShared,
|
ResidenceShared: pref.ResidenceShared,
|
||||||
WarrantyExpiring: pref.WarrantyExpiring,
|
WarrantyExpiring: pref.WarrantyExpiring,
|
||||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,17 +287,18 @@ func (h *AdminNotificationPrefsHandler) GetByUser(c *gin.Context) {
|
|||||||
h.db.First(&user, pref.UserID)
|
h.db.First(&user, pref.UserID)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||||
ID: pref.ID,
|
ID: pref.ID,
|
||||||
UserID: pref.UserID,
|
UserID: pref.UserID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
TaskDueSoon: pref.TaskDueSoon,
|
TaskDueSoon: pref.TaskDueSoon,
|
||||||
TaskOverdue: pref.TaskOverdue,
|
TaskOverdue: pref.TaskOverdue,
|
||||||
TaskCompleted: pref.TaskCompleted,
|
TaskCompleted: pref.TaskCompleted,
|
||||||
TaskAssigned: pref.TaskAssigned,
|
TaskAssigned: pref.TaskAssigned,
|
||||||
ResidenceShared: pref.ResidenceShared,
|
ResidenceShared: pref.ResidenceShared,
|
||||||
WarrantyExpiring: pref.WarrantyExpiring,
|
WarrantyExpiring: pref.WarrantyExpiring,
|
||||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
EmailTaskCompleted: pref.EmailTaskCompleted,
|
||||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ type NotificationPreference struct {
|
|||||||
|
|
||||||
// Document notifications
|
// Document notifications
|
||||||
WarrantyExpiring bool `gorm:"column:warranty_expiring;default:true" json:"warranty_expiring"`
|
WarrantyExpiring bool `gorm:"column:warranty_expiring;default:true" json:"warranty_expiring"`
|
||||||
|
|
||||||
|
// Email preferences
|
||||||
|
EmailTaskCompleted bool `gorm:"column:email_task_completed;default:true" json:"email_task_completed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for GORM
|
// TableName returns the table name for GORM
|
||||||
|
|||||||
@@ -142,13 +142,14 @@ func (r *NotificationRepository) GetOrCreatePreferences(userID uint) (*models.No
|
|||||||
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
prefs = &models.NotificationPreference{
|
prefs = &models.NotificationPreference{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
TaskDueSoon: true,
|
TaskDueSoon: true,
|
||||||
TaskOverdue: true,
|
TaskOverdue: true,
|
||||||
TaskCompleted: true,
|
TaskCompleted: true,
|
||||||
TaskAssigned: true,
|
TaskAssigned: true,
|
||||||
ResidenceShared: true,
|
ResidenceShared: true,
|
||||||
WarrantyExpiring: true,
|
WarrantyExpiring: true,
|
||||||
|
EmailTaskCompleted: true,
|
||||||
}
|
}
|
||||||
if err := r.CreatePreferences(prefs); err != nil {
|
if err := r.CreatePreferences(prefs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -190,6 +190,9 @@ func (s *NotificationService) UpdatePreferences(userID uint, req *UpdatePreferen
|
|||||||
if req.WarrantyExpiring != nil {
|
if req.WarrantyExpiring != nil {
|
||||||
prefs.WarrantyExpiring = *req.WarrantyExpiring
|
prefs.WarrantyExpiring = *req.WarrantyExpiring
|
||||||
}
|
}
|
||||||
|
if req.EmailTaskCompleted != nil {
|
||||||
|
prefs.EmailTaskCompleted = *req.EmailTaskCompleted
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.notificationRepo.UpdatePreferences(prefs); err != nil {
|
if err := s.notificationRepo.UpdatePreferences(prefs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -358,17 +361,21 @@ type NotificationPreferencesResponse struct {
|
|||||||
TaskAssigned bool `json:"task_assigned"`
|
TaskAssigned bool `json:"task_assigned"`
|
||||||
ResidenceShared bool `json:"residence_shared"`
|
ResidenceShared bool `json:"residence_shared"`
|
||||||
WarrantyExpiring bool `json:"warranty_expiring"`
|
WarrantyExpiring bool `json:"warranty_expiring"`
|
||||||
|
|
||||||
|
// Email preferences
|
||||||
|
EmailTaskCompleted bool `json:"email_task_completed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNotificationPreferencesResponse creates a NotificationPreferencesResponse
|
// NewNotificationPreferencesResponse creates a NotificationPreferencesResponse
|
||||||
func NewNotificationPreferencesResponse(p *models.NotificationPreference) *NotificationPreferencesResponse {
|
func NewNotificationPreferencesResponse(p *models.NotificationPreference) *NotificationPreferencesResponse {
|
||||||
return &NotificationPreferencesResponse{
|
return &NotificationPreferencesResponse{
|
||||||
TaskDueSoon: p.TaskDueSoon,
|
TaskDueSoon: p.TaskDueSoon,
|
||||||
TaskOverdue: p.TaskOverdue,
|
TaskOverdue: p.TaskOverdue,
|
||||||
TaskCompleted: p.TaskCompleted,
|
TaskCompleted: p.TaskCompleted,
|
||||||
TaskAssigned: p.TaskAssigned,
|
TaskAssigned: p.TaskAssigned,
|
||||||
ResidenceShared: p.ResidenceShared,
|
ResidenceShared: p.ResidenceShared,
|
||||||
WarrantyExpiring: p.WarrantyExpiring,
|
WarrantyExpiring: p.WarrantyExpiring,
|
||||||
|
EmailTaskCompleted: p.EmailTaskCompleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,6 +387,9 @@ type UpdatePreferencesRequest struct {
|
|||||||
TaskAssigned *bool `json:"task_assigned"`
|
TaskAssigned *bool `json:"task_assigned"`
|
||||||
ResidenceShared *bool `json:"residence_shared"`
|
ResidenceShared *bool `json:"residence_shared"`
|
||||||
WarrantyExpiring *bool `json:"warranty_expiring"`
|
WarrantyExpiring *bool `json:"warranty_expiring"`
|
||||||
|
|
||||||
|
// Email preferences
|
||||||
|
EmailTaskCompleted *bool `json:"email_task_completed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceResponse represents a device in API response
|
// DeviceResponse represents a device in API response
|
||||||
|
|||||||
@@ -570,20 +570,25 @@ func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send email notification (to everyone INCLUDING the person who completed it)
|
// Send email notification (to everyone INCLUDING the person who completed it)
|
||||||
if s.emailService != nil && user.Email != "" {
|
// Check user's email notification preferences first
|
||||||
go func(u models.User) {
|
if s.emailService != nil && user.Email != "" && s.notificationService != nil {
|
||||||
if err := s.emailService.SendTaskCompletedEmail(
|
prefs, err := s.notificationService.GetPreferences(user.ID)
|
||||||
u.Email,
|
if err != nil || (prefs != nil && prefs.EmailTaskCompleted) {
|
||||||
u.GetFullName(),
|
// Send email if we couldn't get prefs (fail-open) or if email notifications are enabled
|
||||||
task.Title,
|
go func(u models.User) {
|
||||||
completedByName,
|
if err := s.emailService.SendTaskCompletedEmail(
|
||||||
residenceName,
|
u.Email,
|
||||||
); err != nil {
|
u.GetFullName(),
|
||||||
log.Error().Err(err).Str("email", u.Email).Uint("task_id", task.ID).Msg("Failed to send task completion email")
|
task.Title,
|
||||||
} else {
|
completedByName,
|
||||||
log.Info().Str("email", u.Email).Uint("task_id", task.ID).Msg("Task completion email sent")
|
residenceName,
|
||||||
}
|
); err != nil {
|
||||||
}(user)
|
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