Files
honeyDueAPI/internal/admin/handlers/task_template_handler.go
Trey t 4976eafc6c Rebrand from Casera/MyCrib to honeyDue
Total rebrand across all Go API source files:
- Go module path: casera-api -> honeydue-api
- All imports updated (130+ files)
- Docker: containers, images, networks renamed
- Email templates: support email, noreply, icon URL
- Domains: casera.app/mycrib.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- IAP product IDs updated
- Landing page, admin panel, config defaults
- Seeds, CI workflows, Makefile, docs
- Database table names preserved (no migration needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:38 -06:00

347 lines
12 KiB
Go

package handlers
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-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 {
}
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")
}
if err := cache.CacheTaskTemplates(ctx, templates); err != nil {
log.Warn().Err(err).Msg("Failed to cache task templates")
}
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")
}
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 echo.Context) error {
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.QueryParam("is_active"); activeParam != "" {
isActive := activeParam == "true"
query = query.Where("is_active = ?", isActive)
}
// Optional filter by category
if categoryID := c.QueryParam("category_id"); categoryID != "" {
query = query.Where("category_id = ?", categoryID)
}
// Optional filter by frequency
if frequencyID := c.QueryParam("frequency_id"); frequencyID != "" {
query = query.Where("frequency_id = ?", frequencyID)
}
// Optional search
if search := c.QueryParam("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 {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch templates"})
}
responses := make([]TaskTemplateResponse, len(templates))
for i, t := range templates {
responses[i] = h.toResponse(&t)
}
return c.JSON(http.StatusOK, map[string]interface{}{"data": responses, "total": len(responses)})
}
// GetTemplate handles GET /admin/api/task-templates/:id/
func (h *AdminTaskTemplateHandler) GetTemplate(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 template ID"})
}
var template models.TaskTemplate
if err := h.db.Preload("Category").Preload("Frequency").First(&template, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Template not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"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 echo.Context) error {
var req CreateUpdateTaskTemplateRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
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 {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create template"})
}
// Reload with preloads
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
// Refresh cache after creating
h.refreshTaskTemplatesCache(c.Request().Context())
return c.JSON(http.StatusCreated, h.toResponse(&template))
}
// UpdateTemplate handles PUT /admin/api/task-templates/:id/
func (h *AdminTaskTemplateHandler) UpdateTemplate(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 template ID"})
}
var template models.TaskTemplate
if err := h.db.First(&template, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Template not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch template"})
}
var req CreateUpdateTaskTemplateRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
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 {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update template"})
}
// Reload with preloads
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
// Refresh cache after updating
h.refreshTaskTemplatesCache(c.Request().Context())
return c.JSON(http.StatusOK, h.toResponse(&template))
}
// DeleteTemplate handles DELETE /admin/api/task-templates/:id/
func (h *AdminTaskTemplateHandler) DeleteTemplate(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 template ID"})
}
if err := h.db.Delete(&models.TaskTemplate{}, id).Error; err != nil {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete template"})
}
// Refresh cache after deleting
h.refreshTaskTemplatesCache(c.Request().Context())
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Template deleted successfully"})
}
// ToggleActive handles POST /admin/api/task-templates/:id/toggle-active/
func (h *AdminTaskTemplateHandler) ToggleActive(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 template ID"})
}
var template models.TaskTemplate
if err := h.db.First(&template, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Template not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch template"})
}
template.IsActive = !template.IsActive
if err := h.db.Save(&template).Error; err != nil {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update template"})
}
// Reload with preloads
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
// Refresh cache after toggling active status
h.refreshTaskTemplatesCache(c.Request().Context())
return c.JSON(http.StatusOK, h.toResponse(&template))
}
// BulkCreate handles POST /admin/api/task-templates/bulk/
func (h *AdminTaskTemplateHandler) BulkCreate(c echo.Context) error {
var req struct {
Templates []CreateUpdateTaskTemplateRequest `json:"templates" binding:"required,dive"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
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 {
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create templates"})
}
// Refresh cache after bulk creating
h.refreshTaskTemplatesCache(c.Request().Context())
return c.JSON(http.StatusCreated, map[string]interface{}{"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
}