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>
347 lines
12 KiB
Go
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"),
|
|
})
|
|
}
|