Add Redis caching for lookup data and admin cache management
- 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>
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
// AdminLookupHandler handles admin lookup table management endpoints
|
||||
@@ -21,6 +24,159 @@ func NewAdminLookupHandler(db *gorm.DB) *AdminLookupHandler {
|
||||
return &AdminLookupHandler{db: db}
|
||||
}
|
||||
|
||||
// refreshCategoriesCache invalidates and refreshes the categories cache
|
||||
func (h *AdminLookupHandler) refreshCategoriesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var categories []models.TaskCategory
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&categories).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch categories for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheCategories(ctx, categories); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache categories")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(categories)).Msg("Refreshed categories cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshPrioritiesCache invalidates and refreshes the priorities cache
|
||||
func (h *AdminLookupHandler) refreshPrioritiesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var priorities []models.TaskPriority
|
||||
if err := h.db.Order("display_order ASC, level ASC").Find(&priorities).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch priorities for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CachePriorities(ctx, priorities); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache priorities")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(priorities)).Msg("Refreshed priorities cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshStatusesCache invalidates and refreshes the statuses cache
|
||||
func (h *AdminLookupHandler) refreshStatusesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var statuses []models.TaskStatus
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch statuses for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheStatuses(ctx, statuses); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache statuses")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(statuses)).Msg("Refreshed statuses cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshFrequenciesCache invalidates and refreshes the frequencies cache
|
||||
func (h *AdminLookupHandler) refreshFrequenciesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var frequencies []models.TaskFrequency
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&frequencies).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch frequencies for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheFrequencies(ctx, frequencies); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache frequencies")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(frequencies)).Msg("Refreshed frequencies cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshResidenceTypesCache invalidates and refreshes the residence types cache
|
||||
func (h *AdminLookupHandler) refreshResidenceTypesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var types []models.ResidenceType
|
||||
if err := h.db.Order("name ASC").Find(&types).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch residence types for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheResidenceTypes(ctx, types); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache residence types")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(types)).Msg("Refreshed residence types cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshSpecialtiesCache invalidates and refreshes the specialties cache
|
||||
func (h *AdminLookupHandler) refreshSpecialtiesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var specialties []models.ContractorSpecialty
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&specialties).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch specialties for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheSpecialties(ctx, specialties); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache specialties")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(specialties)).Msg("Refreshed specialties cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// invalidateSeededDataCache invalidates the unified seeded data cache
|
||||
// This should be called whenever any lookup data changes
|
||||
func (h *AdminLookupHandler) invalidateSeededDataCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// ========== Task Categories ==========
|
||||
|
||||
type TaskCategoryResponse struct {
|
||||
@@ -84,6 +240,9 @@ func (h *AdminLookupHandler) CreateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshCategoriesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskCategoryResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
@@ -130,6 +289,9 @@ func (h *AdminLookupHandler) UpdateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshCategoriesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskCategoryResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
@@ -160,6 +322,9 @@ func (h *AdminLookupHandler) DeleteCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshCategoriesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -222,6 +387,9 @@ func (h *AdminLookupHandler) CreatePriority(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshPrioritiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskPriorityResponse{
|
||||
ID: priority.ID,
|
||||
Name: priority.Name,
|
||||
@@ -266,6 +434,9 @@ func (h *AdminLookupHandler) UpdatePriority(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshPrioritiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskPriorityResponse{
|
||||
ID: priority.ID,
|
||||
Name: priority.Name,
|
||||
@@ -294,6 +465,9 @@ func (h *AdminLookupHandler) DeletePriority(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshPrioritiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Priority deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -356,6 +530,9 @@ func (h *AdminLookupHandler) CreateStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshStatusesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskStatusResponse{
|
||||
ID: status.ID,
|
||||
Name: status.Name,
|
||||
@@ -400,6 +577,9 @@ func (h *AdminLookupHandler) UpdateStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshStatusesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskStatusResponse{
|
||||
ID: status.ID,
|
||||
Name: status.Name,
|
||||
@@ -428,6 +608,9 @@ func (h *AdminLookupHandler) DeleteStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshStatusesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Status deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -486,6 +669,9 @@ func (h *AdminLookupHandler) CreateFrequency(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshFrequenciesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskFrequencyResponse{
|
||||
ID: frequency.ID,
|
||||
Name: frequency.Name,
|
||||
@@ -528,6 +714,9 @@ func (h *AdminLookupHandler) UpdateFrequency(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshFrequenciesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskFrequencyResponse{
|
||||
ID: frequency.ID,
|
||||
Name: frequency.Name,
|
||||
@@ -555,6 +744,9 @@ func (h *AdminLookupHandler) DeleteFrequency(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshFrequenciesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Frequency deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -600,6 +792,9 @@ func (h *AdminLookupHandler) CreateResidenceType(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshResidenceTypesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, ResidenceTypeResponse{
|
||||
ID: residenceType.ID,
|
||||
Name: residenceType.Name,
|
||||
@@ -635,6 +830,9 @@ func (h *AdminLookupHandler) UpdateResidenceType(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshResidenceTypesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, ResidenceTypeResponse{
|
||||
ID: residenceType.ID,
|
||||
Name: residenceType.Name,
|
||||
@@ -660,6 +858,9 @@ func (h *AdminLookupHandler) DeleteResidenceType(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshResidenceTypesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Residence type deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -722,6 +923,9 @@ func (h *AdminLookupHandler) CreateSpecialty(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshSpecialtiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, ContractorSpecialtyResponse{
|
||||
ID: specialty.ID,
|
||||
Name: specialty.Name,
|
||||
@@ -766,6 +970,9 @@ func (h *AdminLookupHandler) UpdateSpecialty(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshSpecialtiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, ContractorSpecialtyResponse{
|
||||
ID: specialty.ID,
|
||||
Name: specialty.Name,
|
||||
@@ -795,6 +1002,9 @@ func (h *AdminLookupHandler) DeleteSpecialty(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshSpecialtiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Specialty deleted successfully"})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -8,9 +9,11 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// AdminSettingsHandler handles system settings management
|
||||
@@ -85,7 +88,7 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// SeedLookups handles POST /api/admin/settings/seed-lookups
|
||||
// Seeds both lookup tables AND task templates
|
||||
// Seeds both lookup tables AND task templates, then caches all lookups in Redis
|
||||
func (h *AdminSettingsHandler) SeedLookups(c *gin.Context) {
|
||||
// First seed lookup tables
|
||||
if err := h.runSeedFile("001_lookups.sql"); err != nil {
|
||||
@@ -99,7 +102,230 @@ func (h *AdminSettingsHandler) SeedLookups(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Lookup data and task templates seeded successfully"})
|
||||
// Cache all lookups in Redis
|
||||
cached, cacheErr := h.cacheAllLookups(c.Request.Context())
|
||||
if cacheErr != nil {
|
||||
log.Warn().Err(cacheErr).Msg("Failed to cache lookups in Redis, but seed was successful")
|
||||
}
|
||||
|
||||
response := gin.H{
|
||||
"message": "Lookup data and task templates seeded successfully",
|
||||
"redis_cached": cached,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// cacheAllLookups fetches all lookup data from the database and caches it in Redis
|
||||
func (h *AdminSettingsHandler) cacheAllLookups(ctx context.Context) (bool, error) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return false, fmt.Errorf("Redis cache not available")
|
||||
}
|
||||
|
||||
// Fetch and cache task categories
|
||||
var categories []models.TaskCategory
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&categories).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch categories: %w", err)
|
||||
}
|
||||
if err := cache.CacheCategories(ctx, categories); err != nil {
|
||||
return false, fmt.Errorf("failed to cache categories: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(categories)).Msg("Cached task categories")
|
||||
|
||||
// Fetch and cache task priorities
|
||||
var priorities []models.TaskPriority
|
||||
if err := h.db.Order("display_order ASC, level ASC").Find(&priorities).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch priorities: %w", err)
|
||||
}
|
||||
if err := cache.CachePriorities(ctx, priorities); err != nil {
|
||||
return false, fmt.Errorf("failed to cache priorities: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(priorities)).Msg("Cached task priorities")
|
||||
|
||||
// Fetch and cache task statuses
|
||||
var statuses []models.TaskStatus
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch statuses: %w", err)
|
||||
}
|
||||
if err := cache.CacheStatuses(ctx, statuses); err != nil {
|
||||
return false, fmt.Errorf("failed to cache statuses: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(statuses)).Msg("Cached task statuses")
|
||||
|
||||
// Fetch and cache task frequencies
|
||||
var frequencies []models.TaskFrequency
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&frequencies).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch frequencies: %w", err)
|
||||
}
|
||||
if err := cache.CacheFrequencies(ctx, frequencies); err != nil {
|
||||
return false, fmt.Errorf("failed to cache frequencies: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(frequencies)).Msg("Cached task frequencies")
|
||||
|
||||
// Fetch and cache residence types
|
||||
var residenceTypes []models.ResidenceType
|
||||
if err := h.db.Order("name ASC").Find(&residenceTypes).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch residence types: %w", err)
|
||||
}
|
||||
if err := cache.CacheResidenceTypes(ctx, residenceTypes); err != nil {
|
||||
return false, fmt.Errorf("failed to cache residence types: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(residenceTypes)).Msg("Cached residence types")
|
||||
|
||||
// Fetch and cache contractor specialties
|
||||
var specialties []models.ContractorSpecialty
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&specialties).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch specialties: %w", err)
|
||||
}
|
||||
if err := cache.CacheSpecialties(ctx, specialties); err != nil {
|
||||
return false, fmt.Errorf("failed to cache specialties: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(specialties)).Msg("Cached contractor specialties")
|
||||
|
||||
// Fetch and cache task templates (only active ones)
|
||||
var taskTemplates []models.TaskTemplate
|
||||
if err := h.db.Preload("Category").Preload("Frequency").
|
||||
Where("is_active = ?", true).
|
||||
Order("display_order ASC, title ASC").
|
||||
Find(&taskTemplates).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch task templates: %w", err)
|
||||
}
|
||||
if err := cache.CacheTaskTemplates(ctx, taskTemplates); err != nil {
|
||||
return false, fmt.Errorf("failed to cache task templates: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(taskTemplates)).Msg("Cached task templates")
|
||||
|
||||
// Build and cache the unified seeded data response
|
||||
// Import the grouped response type
|
||||
seededData := map[string]interface{}{
|
||||
"residence_types": residenceTypes,
|
||||
"task_categories": categories,
|
||||
"task_priorities": priorities,
|
||||
"task_frequencies": frequencies,
|
||||
"task_statuses": statuses,
|
||||
"contractor_specialties": specialties,
|
||||
"task_templates": buildGroupedTemplates(taskTemplates),
|
||||
}
|
||||
|
||||
etag, err := cache.CacheSeededData(ctx, seededData)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to cache seeded data: %w", err)
|
||||
}
|
||||
log.Debug().Str("etag", etag).Msg("Cached unified seeded data")
|
||||
|
||||
log.Info().Msg("All lookup data cached in Redis successfully")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// buildGroupedTemplates groups task templates by category for the seeded data response
|
||||
func buildGroupedTemplates(templates []models.TaskTemplate) map[string]interface{} {
|
||||
type templateResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
Category map[string]interface{} `json:"category,omitempty"`
|
||||
FrequencyID *uint `json:"frequency_id"`
|
||||
Frequency map[string]interface{} `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"`
|
||||
}
|
||||
|
||||
type categoryGroup struct {
|
||||
CategoryName string `json:"category_name"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
Templates []templateResponse `json:"templates"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
categoryMap := make(map[string]*categoryGroup)
|
||||
categoryOrder := []string{}
|
||||
|
||||
for _, t := range templates {
|
||||
categoryName := "Uncategorized"
|
||||
var categoryID *uint
|
||||
if t.Category != nil {
|
||||
categoryName = t.Category.Name
|
||||
categoryID = &t.Category.ID
|
||||
}
|
||||
|
||||
if _, exists := categoryMap[categoryName]; !exists {
|
||||
categoryMap[categoryName] = &categoryGroup{
|
||||
CategoryName: categoryName,
|
||||
CategoryID: categoryID,
|
||||
Templates: []templateResponse{},
|
||||
}
|
||||
categoryOrder = append(categoryOrder, categoryName)
|
||||
}
|
||||
|
||||
resp := templateResponse{
|
||||
ID: t.ID,
|
||||
Title: t.Title,
|
||||
Description: t.Description,
|
||||
CategoryID: t.CategoryID,
|
||||
FrequencyID: t.FrequencyID,
|
||||
IconIOS: t.IconIOS,
|
||||
IconAndroid: t.IconAndroid,
|
||||
Tags: parseTags(t.Tags),
|
||||
DisplayOrder: t.DisplayOrder,
|
||||
IsActive: t.IsActive,
|
||||
}
|
||||
|
||||
if t.Category != nil {
|
||||
resp.Category = map[string]interface{}{
|
||||
"id": t.Category.ID,
|
||||
"name": t.Category.Name,
|
||||
"description": t.Category.Description,
|
||||
"icon": t.Category.Icon,
|
||||
"color": t.Category.Color,
|
||||
"display_order": t.Category.DisplayOrder,
|
||||
}
|
||||
}
|
||||
if t.Frequency != nil {
|
||||
resp.Frequency = map[string]interface{}{
|
||||
"id": t.Frequency.ID,
|
||||
"name": t.Frequency.Name,
|
||||
"days": t.Frequency.Days,
|
||||
"display_order": t.Frequency.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
categoryMap[categoryName].Templates = append(categoryMap[categoryName].Templates, resp)
|
||||
}
|
||||
|
||||
categories := make([]categoryGroup, len(categoryOrder))
|
||||
totalCount := 0
|
||||
for i, name := range categoryOrder {
|
||||
group := categoryMap[name]
|
||||
group.Count = len(group.Templates)
|
||||
totalCount += group.Count
|
||||
categories[i] = *group
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"categories": categories,
|
||||
"total_count": totalCount,
|
||||
}
|
||||
}
|
||||
|
||||
// parseTags splits a comma-separated tags string into a slice
|
||||
func parseTags(tags string) []string {
|
||||
if tags == "" {
|
||||
return []string{}
|
||||
}
|
||||
parts := strings.Split(tags, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
trimmed := strings.TrimSpace(p)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SeedTestData handles POST /api/admin/settings/seed-test-data
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
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
|
||||
@@ -21,6 +24,36 @@ 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"`
|
||||
@@ -145,6 +178,9 @@ func (h *AdminTaskTemplateHandler) CreateTemplate(c *gin.Context) {
|
||||
// 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))
|
||||
}
|
||||
|
||||
@@ -195,6 +231,9 @@ func (h *AdminTaskTemplateHandler) UpdateTemplate(c *gin.Context) {
|
||||
// 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))
|
||||
}
|
||||
|
||||
@@ -211,6 +250,9 @@ func (h *AdminTaskTemplateHandler) DeleteTemplate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshTaskTemplatesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Template deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -241,6 +283,9 @@ func (h *AdminTaskTemplateHandler) ToggleActive(c *gin.Context) {
|
||||
// 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))
|
||||
}
|
||||
|
||||
@@ -279,6 +324,9 @@ func (h *AdminTaskTemplateHandler) BulkCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after bulk creating
|
||||
h.refreshTaskTemplatesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"message": "Templates created successfully", "count": len(templates)})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user