- Add lookup-specific cache keys and methods to CacheService - Add cache refresh on lookup CRUD operations in AdminLookupHandler - Add Redis caching after seed-lookups in AdminSettingsHandler - Add ETag generation for seeded data to support client-side caching - Update task template handler with cache invalidation - Fix route for clear-cache endpoint 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
370 lines
11 KiB
Go
370 lines
11 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rs/zerolog/log"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/treytartt/casera-api/internal/models"
|
|
"github.com/treytartt/casera-api/internal/services"
|
|
)
|
|
|
|
// AdminTaskTemplateHandler handles admin task template management endpoints
|
|
type AdminTaskTemplateHandler struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewAdminTaskTemplateHandler creates a new admin task template handler
|
|
func NewAdminTaskTemplateHandler(db *gorm.DB) *AdminTaskTemplateHandler {
|
|
return &AdminTaskTemplateHandler{db: db}
|
|
}
|
|
|
|
// refreshTaskTemplatesCache invalidates and refreshes the task templates cache
|
|
func (h *AdminTaskTemplateHandler) refreshTaskTemplatesCache(ctx context.Context) {
|
|
cache := services.GetCache()
|
|
if cache == nil {
|
|
return
|
|
}
|
|
|
|
var templates []models.TaskTemplate
|
|
if err := h.db.Preload("Category").Preload("Frequency").
|
|
Where("is_active = ?", true).
|
|
Order("display_order ASC, title ASC").
|
|
Find(&templates).Error; err != nil {
|
|
log.Warn().Err(err).Msg("Failed to fetch task templates for cache refresh")
|
|
return
|
|
}
|
|
|
|
if err := cache.CacheTaskTemplates(ctx, templates); err != nil {
|
|
log.Warn().Err(err).Msg("Failed to cache task templates")
|
|
return
|
|
}
|
|
log.Debug().Int("count", len(templates)).Msg("Refreshed task templates cache")
|
|
|
|
// Invalidate unified seeded data cache
|
|
if err := cache.InvalidateSeededData(ctx); err != nil {
|
|
log.Warn().Err(err).Msg("Failed to invalidate seeded data cache")
|
|
return
|
|
}
|
|
log.Debug().Msg("Invalidated seeded data cache")
|
|
}
|
|
|
|
// TaskTemplateResponse represents a task template in admin responses
|
|
type TaskTemplateResponse struct {
|
|
ID uint `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
CategoryID *uint `json:"category_id"`
|
|
Category *TaskCategoryResponse `json:"category,omitempty"`
|
|
FrequencyID *uint `json:"frequency_id"`
|
|
Frequency *TaskFrequencyResponse `json:"frequency,omitempty"`
|
|
IconIOS string `json:"icon_ios"`
|
|
IconAndroid string `json:"icon_android"`
|
|
Tags string `json:"tags"`
|
|
DisplayOrder int `json:"display_order"`
|
|
IsActive bool `json:"is_active"`
|
|
}
|
|
|
|
// CreateUpdateTaskTemplateRequest represents the request body for creating/updating templates
|
|
type CreateUpdateTaskTemplateRequest struct {
|
|
Title string `json:"title" binding:"required,max=200"`
|
|
Description string `json:"description"`
|
|
CategoryID *uint `json:"category_id"`
|
|
FrequencyID *uint `json:"frequency_id"`
|
|
IconIOS string `json:"icon_ios" binding:"max=100"`
|
|
IconAndroid string `json:"icon_android" binding:"max=100"`
|
|
Tags string `json:"tags"`
|
|
DisplayOrder *int `json:"display_order"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
// ListTemplates handles GET /admin/api/task-templates/
|
|
func (h *AdminTaskTemplateHandler) ListTemplates(c *gin.Context) {
|
|
var templates []models.TaskTemplate
|
|
query := h.db.Preload("Category").Preload("Frequency").Order("display_order ASC, title ASC")
|
|
|
|
// Optional filter by active status
|
|
if activeParam := c.Query("is_active"); activeParam != "" {
|
|
isActive := activeParam == "true"
|
|
query = query.Where("is_active = ?", isActive)
|
|
}
|
|
|
|
// Optional filter by category
|
|
if categoryID := c.Query("category_id"); categoryID != "" {
|
|
query = query.Where("category_id = ?", categoryID)
|
|
}
|
|
|
|
// Optional filter by frequency
|
|
if frequencyID := c.Query("frequency_id"); frequencyID != "" {
|
|
query = query.Where("frequency_id = ?", frequencyID)
|
|
}
|
|
|
|
// Optional search
|
|
if search := c.Query("search"); search != "" {
|
|
searchTerm := "%" + strings.ToLower(search) + "%"
|
|
query = query.Where("LOWER(title) LIKE ? OR LOWER(tags) LIKE ?", searchTerm, searchTerm)
|
|
}
|
|
|
|
if err := query.Find(&templates).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch templates"})
|
|
return
|
|
}
|
|
|
|
responses := make([]TaskTemplateResponse, len(templates))
|
|
for i, t := range templates {
|
|
responses[i] = h.toResponse(&t)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
|
|
}
|
|
|
|
// GetTemplate handles GET /admin/api/task-templates/:id/
|
|
func (h *AdminTaskTemplateHandler) GetTemplate(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
|
|
return
|
|
}
|
|
|
|
var template models.TaskTemplate
|
|
if err := h.db.Preload("Category").Preload("Frequency").First(&template, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch template"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, h.toResponse(&template))
|
|
}
|
|
|
|
// CreateTemplate handles POST /admin/api/task-templates/
|
|
func (h *AdminTaskTemplateHandler) CreateTemplate(c *gin.Context) {
|
|
var req CreateUpdateTaskTemplateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
template := models.TaskTemplate{
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
CategoryID: req.CategoryID,
|
|
FrequencyID: req.FrequencyID,
|
|
IconIOS: req.IconIOS,
|
|
IconAndroid: req.IconAndroid,
|
|
Tags: req.Tags,
|
|
IsActive: true, // Default to active
|
|
}
|
|
|
|
if req.DisplayOrder != nil {
|
|
template.DisplayOrder = *req.DisplayOrder
|
|
}
|
|
if req.IsActive != nil {
|
|
template.IsActive = *req.IsActive
|
|
}
|
|
|
|
if err := h.db.Create(&template).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create template"})
|
|
return
|
|
}
|
|
|
|
// Reload with preloads
|
|
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
|
|
|
|
// Refresh cache after creating
|
|
h.refreshTaskTemplatesCache(c.Request.Context())
|
|
|
|
c.JSON(http.StatusCreated, h.toResponse(&template))
|
|
}
|
|
|
|
// UpdateTemplate handles PUT /admin/api/task-templates/:id/
|
|
func (h *AdminTaskTemplateHandler) UpdateTemplate(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
|
|
return
|
|
}
|
|
|
|
var template models.TaskTemplate
|
|
if err := h.db.First(&template, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch template"})
|
|
return
|
|
}
|
|
|
|
var req CreateUpdateTaskTemplateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
template.Title = req.Title
|
|
template.Description = req.Description
|
|
template.CategoryID = req.CategoryID
|
|
template.FrequencyID = req.FrequencyID
|
|
template.IconIOS = req.IconIOS
|
|
template.IconAndroid = req.IconAndroid
|
|
template.Tags = req.Tags
|
|
|
|
if req.DisplayOrder != nil {
|
|
template.DisplayOrder = *req.DisplayOrder
|
|
}
|
|
if req.IsActive != nil {
|
|
template.IsActive = *req.IsActive
|
|
}
|
|
|
|
if err := h.db.Save(&template).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update template"})
|
|
return
|
|
}
|
|
|
|
// Reload with preloads
|
|
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
|
|
|
|
// Refresh cache after updating
|
|
h.refreshTaskTemplatesCache(c.Request.Context())
|
|
|
|
c.JSON(http.StatusOK, h.toResponse(&template))
|
|
}
|
|
|
|
// DeleteTemplate handles DELETE /admin/api/task-templates/:id/
|
|
func (h *AdminTaskTemplateHandler) DeleteTemplate(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.db.Delete(&models.TaskTemplate{}, id).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete template"})
|
|
return
|
|
}
|
|
|
|
// Refresh cache after deleting
|
|
h.refreshTaskTemplatesCache(c.Request.Context())
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Template deleted successfully"})
|
|
}
|
|
|
|
// ToggleActive handles POST /admin/api/task-templates/:id/toggle-active/
|
|
func (h *AdminTaskTemplateHandler) ToggleActive(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
|
|
return
|
|
}
|
|
|
|
var template models.TaskTemplate
|
|
if err := h.db.First(&template, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch template"})
|
|
return
|
|
}
|
|
|
|
template.IsActive = !template.IsActive
|
|
if err := h.db.Save(&template).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update template"})
|
|
return
|
|
}
|
|
|
|
// Reload with preloads
|
|
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
|
|
|
|
// Refresh cache after toggling active status
|
|
h.refreshTaskTemplatesCache(c.Request.Context())
|
|
|
|
c.JSON(http.StatusOK, h.toResponse(&template))
|
|
}
|
|
|
|
// BulkCreate handles POST /admin/api/task-templates/bulk/
|
|
func (h *AdminTaskTemplateHandler) BulkCreate(c *gin.Context) {
|
|
var req struct {
|
|
Templates []CreateUpdateTaskTemplateRequest `json:"templates" binding:"required,dive"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
templates := make([]models.TaskTemplate, len(req.Templates))
|
|
for i, t := range req.Templates {
|
|
templates[i] = models.TaskTemplate{
|
|
Title: t.Title,
|
|
Description: t.Description,
|
|
CategoryID: t.CategoryID,
|
|
FrequencyID: t.FrequencyID,
|
|
IconIOS: t.IconIOS,
|
|
IconAndroid: t.IconAndroid,
|
|
Tags: t.Tags,
|
|
IsActive: true,
|
|
}
|
|
if t.DisplayOrder != nil {
|
|
templates[i].DisplayOrder = *t.DisplayOrder
|
|
}
|
|
if t.IsActive != nil {
|
|
templates[i].IsActive = *t.IsActive
|
|
}
|
|
}
|
|
|
|
if err := h.db.Create(&templates).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create templates"})
|
|
return
|
|
}
|
|
|
|
// Refresh cache after bulk creating
|
|
h.refreshTaskTemplatesCache(c.Request.Context())
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"message": "Templates created successfully", "count": len(templates)})
|
|
}
|
|
|
|
// Helper to convert model to response
|
|
func (h *AdminTaskTemplateHandler) toResponse(t *models.TaskTemplate) TaskTemplateResponse {
|
|
resp := TaskTemplateResponse{
|
|
ID: t.ID,
|
|
Title: t.Title,
|
|
Description: t.Description,
|
|
CategoryID: t.CategoryID,
|
|
FrequencyID: t.FrequencyID,
|
|
IconIOS: t.IconIOS,
|
|
IconAndroid: t.IconAndroid,
|
|
Tags: t.Tags,
|
|
DisplayOrder: t.DisplayOrder,
|
|
IsActive: t.IsActive,
|
|
}
|
|
|
|
if t.Category != nil {
|
|
resp.Category = &TaskCategoryResponse{
|
|
ID: t.Category.ID,
|
|
Name: t.Category.Name,
|
|
Description: t.Category.Description,
|
|
Icon: t.Category.Icon,
|
|
Color: t.Category.Color,
|
|
DisplayOrder: t.Category.DisplayOrder,
|
|
}
|
|
}
|
|
|
|
if t.Frequency != nil {
|
|
resp.Frequency = &TaskFrequencyResponse{
|
|
ID: t.Frequency.ID,
|
|
Name: t.Frequency.Name,
|
|
Days: t.Frequency.Days,
|
|
DisplayOrder: t.Frequency.DisplayOrder,
|
|
}
|
|
}
|
|
|
|
return resp
|
|
}
|