Files
honeyDueAPI/internal/admin/handlers/notification_prefs_handler.go
Trey t c464ee093e Add timezone to admin interface user and notification prefs views
Shows the user's auto-captured timezone (IANA format) in:
- Notification preferences list/detail/update responses
- User detail response

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 22:55:38 -06:00

347 lines
12 KiB
Go

package handlers
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
"github.com/treytartt/casera-api/internal/models"
)
// AdminNotificationPrefsHandler handles notification preference management
type AdminNotificationPrefsHandler struct {
db *gorm.DB
}
// NewAdminNotificationPrefsHandler creates a new handler
func NewAdminNotificationPrefsHandler(db *gorm.DB) *AdminNotificationPrefsHandler {
return &AdminNotificationPrefsHandler{db: db}
}
// NotificationPrefResponse represents a notification preference in the admin API
type NotificationPrefResponse struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
TaskDueSoon bool `json:"task_due_soon"`
TaskOverdue bool `json:"task_overdue"`
TaskCompleted bool `json:"task_completed"`
TaskAssigned bool `json:"task_assigned"`
ResidenceShared bool `json:"residence_shared"`
WarrantyExpiring bool `json:"warranty_expiring"`
DailyDigest bool `json:"daily_digest"`
// 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"`
DailyDigestHour *int `json:"daily_digest_hour"`
// User timezone (IANA name, auto-captured from X-Timezone header)
Timezone *string `json:"timezone"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// List handles GET /api/admin/notification-prefs
func (h *AdminNotificationPrefsHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var prefs []models.NotificationPreference
var total int64
query := h.db.Model(&models.NotificationPreference{})
// Apply search on user data via subquery
if filters.Search != "" {
search := "%" + filters.Search + "%"
query = query.Where("user_id IN (?)",
h.db.Model(&models.User{}).Select("id").Where(
"username ILIKE ? OR email ILIKE ?", search, search,
),
)
}
// Get total count
query.Count(&total)
// Apply sorting
sortBy := "created_at"
if filters.SortBy != "" {
sortBy = filters.SortBy
}
query = query.Order(sortBy + " " + filters.GetSortDir())
// Apply pagination
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&prefs).Error; err != nil {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preferences"})
}
// Get user info for each preference
userIDs := make([]uint, len(prefs))
for i, p := range prefs {
userIDs[i] = p.UserID
}
var users []models.User
userMap := make(map[uint]models.User)
if len(userIDs) > 0 {
h.db.Where("id IN ?", userIDs).Find(&users)
for _, u := range users {
userMap[u.ID] = u
}
}
// Build response
responses := make([]NotificationPrefResponse, len(prefs))
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,
DailyDigest: pref.DailyDigest,
EmailTaskCompleted: pref.EmailTaskCompleted,
TaskDueSoonHour: pref.TaskDueSoonHour,
TaskOverdueHour: pref.TaskOverdueHour,
WarrantyExpiringHour: pref.WarrantyExpiringHour,
DailyDigestHour: pref.DailyDigestHour,
Timezone: pref.Timezone,
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/notification-prefs/:id
func (h *AdminNotificationPrefsHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var pref models.NotificationPreference
if err := h.db.First(&pref, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preference"})
}
var user models.User
h.db.First(&user, pref.UserID)
return 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,
DailyDigest: pref.DailyDigest,
EmailTaskCompleted: pref.EmailTaskCompleted,
TaskDueSoonHour: pref.TaskDueSoonHour,
TaskOverdueHour: pref.TaskOverdueHour,
WarrantyExpiringHour: pref.WarrantyExpiringHour,
DailyDigestHour: pref.DailyDigestHour,
Timezone: pref.Timezone,
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
})
}
// UpdateNotificationPrefRequest represents an update request
type UpdateNotificationPrefRequest struct {
TaskDueSoon *bool `json:"task_due_soon"`
TaskOverdue *bool `json:"task_overdue"`
TaskCompleted *bool `json:"task_completed"`
TaskAssigned *bool `json:"task_assigned"`
ResidenceShared *bool `json:"residence_shared"`
WarrantyExpiring *bool `json:"warranty_expiring"`
DailyDigest *bool `json:"daily_digest"`
// 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"`
DailyDigestHour *int `json:"daily_digest_hour"`
}
// Update handles PUT /api/admin/notification-prefs/:id
func (h *AdminNotificationPrefsHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var pref models.NotificationPreference
if err := h.db.First(&pref, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preference"})
}
var req UpdateNotificationPrefRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Apply updates
if req.TaskDueSoon != nil {
pref.TaskDueSoon = *req.TaskDueSoon
}
if req.TaskOverdue != nil {
pref.TaskOverdue = *req.TaskOverdue
}
if req.TaskCompleted != nil {
pref.TaskCompleted = *req.TaskCompleted
}
if req.TaskAssigned != nil {
pref.TaskAssigned = *req.TaskAssigned
}
if req.ResidenceShared != nil {
pref.ResidenceShared = *req.ResidenceShared
}
if req.WarrantyExpiring != nil {
pref.WarrantyExpiring = *req.WarrantyExpiring
}
if req.DailyDigest != nil {
pref.DailyDigest = *req.DailyDigest
}
if req.EmailTaskCompleted != nil {
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 req.DailyDigestHour != nil {
pref.DailyDigestHour = req.DailyDigestHour
}
if err := h.db.Save(&pref).Error; err != nil {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update notification preference"})
}
var user models.User
h.db.First(&user, pref.UserID)
return 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,
DailyDigest: pref.DailyDigest,
EmailTaskCompleted: pref.EmailTaskCompleted,
TaskDueSoonHour: pref.TaskDueSoonHour,
TaskOverdueHour: pref.TaskOverdueHour,
WarrantyExpiringHour: pref.WarrantyExpiringHour,
DailyDigestHour: pref.DailyDigestHour,
Timezone: pref.Timezone,
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
})
}
// Delete handles DELETE /api/admin/notification-prefs/:id
func (h *AdminNotificationPrefsHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.NotificationPreference{}, id)
if result.Error != nil {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete notification preference"})
}
if result.RowsAffected == 0 {
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found"})
}
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Notification preference deleted"})
}
// GetByUser handles GET /api/admin/notification-prefs/user/:user_id
func (h *AdminNotificationPrefsHandler) GetByUser(c echo.Context) error {
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var pref models.NotificationPreference
if err := h.db.Where("user_id = ?", userID).First(&pref).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found for this user"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preference"})
}
var user models.User
h.db.First(&user, pref.UserID)
return 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,
DailyDigest: pref.DailyDigest,
EmailTaskCompleted: pref.EmailTaskCompleted,
TaskDueSoonHour: pref.TaskDueSoonHour,
TaskOverdueHour: pref.TaskOverdueHour,
WarrantyExpiringHour: pref.WarrantyExpiringHour,
DailyDigestHour: pref.DailyDigestHour,
Timezone: pref.Timezone,
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
})
}