Migrate from Gin to Echo framework and add comprehensive integration tests

Major changes:
- Migrate all handlers from Gin to Echo framework
- Add new apperrors, echohelpers, and validator packages
- Update middleware for Echo compatibility
- Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column)
- Add 6 new integration tests:
  - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks
  - MultiUserSharing: Complex sharing with user removal
  - TaskStateTransitions: All state transitions and kanban column changes
  - DateBoundaryEdgeCases: Threshold boundary testing
  - CascadeOperations: Residence deletion cascade effects
  - MultiUserOperations: Shared residence collaboration
- Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.)
- Fix RemoveUser route param mismatch (userId -> user_id)
- Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-16 13:52:08 -06:00
parent c51f1ce34a
commit 6dac34e373
98 changed files with 8209 additions and 4425 deletions

View File

@@ -2,11 +2,11 @@ package dto
// PaginationParams holds pagination query parameters
type PaginationParams struct {
Page int `form:"page" binding:"omitempty,min=1"`
PerPage int `form:"per_page" binding:"omitempty,min=1,max=10000"`
Page int `form:"page" validate:"omitempty,min=1"`
PerPage int `form:"per_page" validate:"omitempty,min=1,max=10000"`
Search string `form:"search"`
SortBy string `form:"sort_by"`
SortDir string `form:"sort_dir" binding:"omitempty,oneof=asc desc"`
SortDir string `form:"sort_dir" validate:"omitempty,oneof=asc desc"`
}
// GetPage returns the page number with default
@@ -52,12 +52,12 @@ type UserFilters struct {
// CreateUserRequest for creating a new user
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=150"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
FirstName string `json:"first_name" binding:"max=150"`
LastName string `json:"last_name" binding:"max=150"`
PhoneNumber string `json:"phone_number" binding:"max=20"`
Username string `json:"username" validate:"required,min=3,max=150"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
FirstName string `json:"first_name" validate:"max=150"`
LastName string `json:"last_name" validate:"max=150"`
PhoneNumber string `json:"phone_number" validate:"max=20"`
IsActive *bool `json:"is_active"`
IsStaff *bool `json:"is_staff"`
IsSuperuser *bool `json:"is_superuser"`
@@ -65,12 +65,12 @@ type CreateUserRequest struct {
// UpdateUserRequest for updating a user
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=150"`
Email *string `json:"email" binding:"omitempty,email"`
Password *string `json:"password" binding:"omitempty,min=8"`
FirstName *string `json:"first_name" binding:"omitempty,max=150"`
LastName *string `json:"last_name" binding:"omitempty,max=150"`
PhoneNumber *string `json:"phone_number" binding:"omitempty,max=20"`
Username *string `json:"username" validate:"omitempty,min=3,max=150"`
Email *string `json:"email" validate:"omitempty,email"`
Password *string `json:"password" validate:"omitempty,min=8"`
FirstName *string `json:"first_name" validate:"omitempty,max=150"`
LastName *string `json:"last_name" validate:"omitempty,max=150"`
PhoneNumber *string `json:"phone_number" validate:"omitempty,max=20"`
IsActive *bool `json:"is_active"`
IsStaff *bool `json:"is_staff"`
IsSuperuser *bool `json:"is_superuser"`
@@ -79,7 +79,7 @@ type UpdateUserRequest struct {
// BulkDeleteRequest for bulk delete operations
type BulkDeleteRequest struct {
IDs []uint `json:"ids" binding:"required,min=1"`
IDs []uint `json:"ids" validate:"required,min=1"`
}
// ResidenceFilters holds residence-specific filter parameters
@@ -92,14 +92,14 @@ type ResidenceFilters struct {
// UpdateResidenceRequest for updating a residence
type UpdateResidenceRequest struct {
OwnerID *uint `json:"owner_id"`
Name *string `json:"name" binding:"omitempty,max=200"`
Name *string `json:"name" validate:"omitempty,max=200"`
PropertyTypeID *uint `json:"property_type_id"`
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
ApartmentUnit *string `json:"apartment_unit" binding:"omitempty,max=50"`
City *string `json:"city" binding:"omitempty,max=100"`
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
Country *string `json:"country" binding:"omitempty,max=100"`
StreetAddress *string `json:"street_address" validate:"omitempty,max=255"`
ApartmentUnit *string `json:"apartment_unit" validate:"omitempty,max=50"`
City *string `json:"city" validate:"omitempty,max=100"`
StateProvince *string `json:"state_province" validate:"omitempty,max=100"`
PostalCode *string `json:"postal_code" validate:"omitempty,max=20"`
Country *string `json:"country" validate:"omitempty,max=100"`
Bedrooms *int `json:"bedrooms"`
Bathrooms *float64 `json:"bathrooms"`
SquareFootage *int `json:"square_footage"`
@@ -128,7 +128,7 @@ type UpdateTaskRequest struct {
ResidenceID *uint `json:"residence_id"`
CreatedByID *uint `json:"created_by_id"`
AssignedToID *uint `json:"assigned_to_id"`
Title *string `json:"title" binding:"omitempty,max=200"`
Title *string `json:"title" validate:"omitempty,max=200"`
Description *string `json:"description"`
CategoryID *uint `json:"category_id"`
PriorityID *uint `json:"priority_id"`
@@ -156,16 +156,16 @@ type ContractorFilters struct {
type UpdateContractorRequest struct {
ResidenceID *uint `json:"residence_id"`
CreatedByID *uint `json:"created_by_id"`
Name *string `json:"name" binding:"omitempty,max=200"`
Company *string `json:"company" binding:"omitempty,max=200"`
Phone *string `json:"phone" binding:"omitempty,max=20"`
Email *string `json:"email" binding:"omitempty,email"`
Website *string `json:"website" binding:"omitempty,max=200"`
Name *string `json:"name" validate:"omitempty,max=200"`
Company *string `json:"company" validate:"omitempty,max=200"`
Phone *string `json:"phone" validate:"omitempty,max=20"`
Email *string `json:"email" validate:"omitempty,email"`
Website *string `json:"website" validate:"omitempty,max=200"`
Notes *string `json:"notes"`
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
City *string `json:"city" binding:"omitempty,max=100"`
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
StreetAddress *string `json:"street_address" validate:"omitempty,max=255"`
City *string `json:"city" validate:"omitempty,max=100"`
StateProvince *string `json:"state_province" validate:"omitempty,max=100"`
PostalCode *string `json:"postal_code" validate:"omitempty,max=20"`
Rating *float64 `json:"rating"`
IsFavorite *bool `json:"is_favorite"`
IsActive *bool `json:"is_active"`
@@ -184,24 +184,24 @@ type DocumentFilters struct {
type UpdateDocumentRequest struct {
ResidenceID *uint `json:"residence_id"`
CreatedByID *uint `json:"created_by_id"`
Title *string `json:"title" binding:"omitempty,max=200"`
Title *string `json:"title" validate:"omitempty,max=200"`
Description *string `json:"description"`
DocumentType *string `json:"document_type"`
FileURL *string `json:"file_url" binding:"omitempty,max=500"`
FileName *string `json:"file_name" binding:"omitempty,max=255"`
FileURL *string `json:"file_url" validate:"omitempty,max=500"`
FileName *string `json:"file_name" validate:"omitempty,max=255"`
FileSize *int64 `json:"file_size"`
MimeType *string `json:"mime_type" binding:"omitempty,max=100"`
MimeType *string `json:"mime_type" validate:"omitempty,max=100"`
PurchaseDate *string `json:"purchase_date"`
ExpiryDate *string `json:"expiry_date"`
PurchasePrice *float64 `json:"purchase_price"`
Vendor *string `json:"vendor" binding:"omitempty,max=200"`
SerialNumber *string `json:"serial_number" binding:"omitempty,max=100"`
ModelNumber *string `json:"model_number" binding:"omitempty,max=100"`
Provider *string `json:"provider" binding:"omitempty,max=200"`
ProviderContact *string `json:"provider_contact" binding:"omitempty,max=200"`
ClaimPhone *string `json:"claim_phone" binding:"omitempty,max=50"`
ClaimEmail *string `json:"claim_email" binding:"omitempty,email"`
ClaimWebsite *string `json:"claim_website" binding:"omitempty,max=500"`
Vendor *string `json:"vendor" validate:"omitempty,max=200"`
SerialNumber *string `json:"serial_number" validate:"omitempty,max=100"`
ModelNumber *string `json:"model_number" validate:"omitempty,max=100"`
Provider *string `json:"provider" validate:"omitempty,max=200"`
ProviderContact *string `json:"provider_contact" validate:"omitempty,max=200"`
ClaimPhone *string `json:"claim_phone" validate:"omitempty,max=50"`
ClaimEmail *string `json:"claim_email" validate:"omitempty,email"`
ClaimWebsite *string `json:"claim_website" validate:"omitempty,max=500"`
Notes *string `json:"notes"`
TaskID *uint `json:"task_id"`
IsActive *bool `json:"is_active"`
@@ -218,8 +218,8 @@ type NotificationFilters struct {
// UpdateNotificationRequest for updating a notification
type UpdateNotificationRequest struct {
Title *string `json:"title" binding:"omitempty,max=200"`
Body *string `json:"body" binding:"omitempty,max=1000"`
Title *string `json:"title" validate:"omitempty,max=200"`
Body *string `json:"body" validate:"omitempty,max=1000"`
Read *bool `json:"read"`
}
@@ -235,10 +235,10 @@ type SubscriptionFilters struct {
// UpdateSubscriptionRequest for updating a subscription
type UpdateSubscriptionRequest struct {
Tier *string `json:"tier" binding:"omitempty,oneof=free premium pro"`
Tier *string `json:"tier" validate:"omitempty,oneof=free premium pro"`
AutoRenew *bool `json:"auto_renew"`
IsFree *bool `json:"is_free"`
Platform *string `json:"platform" binding:"omitempty,max=20"`
Platform *string `json:"platform" validate:"omitempty,max=20"`
SubscribedAt *string `json:"subscribed_at"`
ExpiresAt *string `json:"expires_at"`
CancelledAt *string `json:"cancelled_at"`
@@ -246,15 +246,15 @@ type UpdateSubscriptionRequest struct {
// CreateResidenceRequest for creating a new residence
type CreateResidenceRequest struct {
OwnerID uint `json:"owner_id" binding:"required"`
Name string `json:"name" binding:"required,max=200"`
OwnerID uint `json:"owner_id" validate:"required"`
Name string `json:"name" validate:"required,max=200"`
PropertyTypeID *uint `json:"property_type_id"`
StreetAddress string `json:"street_address" binding:"max=255"`
ApartmentUnit string `json:"apartment_unit" binding:"max=50"`
City string `json:"city" binding:"max=100"`
StateProvince string `json:"state_province" binding:"max=100"`
PostalCode string `json:"postal_code" binding:"max=20"`
Country string `json:"country" binding:"max=100"`
StreetAddress string `json:"street_address" validate:"max=255"`
ApartmentUnit string `json:"apartment_unit" validate:"max=50"`
City string `json:"city" validate:"max=100"`
StateProvince string `json:"state_province" validate:"max=100"`
PostalCode string `json:"postal_code" validate:"max=20"`
Country string `json:"country" validate:"max=100"`
Bedrooms *int `json:"bedrooms"`
Bathrooms *float64 `json:"bathrooms"`
SquareFootage *int `json:"square_footage"`
@@ -265,9 +265,9 @@ type CreateResidenceRequest struct {
// CreateTaskRequest for creating a new task
type CreateTaskRequest struct {
ResidenceID uint `json:"residence_id" binding:"required"`
CreatedByID uint `json:"created_by_id" binding:"required"`
Title string `json:"title" binding:"required,max=200"`
ResidenceID uint `json:"residence_id" validate:"required"`
CreatedByID uint `json:"created_by_id" validate:"required"`
Title string `json:"title" validate:"required,max=200"`
Description string `json:"description"`
CategoryID *uint `json:"category_id"`
PriorityID *uint `json:"priority_id"`
@@ -282,51 +282,51 @@ type CreateTaskRequest struct {
// CreateContractorRequest for creating a new contractor
type CreateContractorRequest struct {
ResidenceID *uint `json:"residence_id"`
CreatedByID uint `json:"created_by_id" binding:"required"`
Name string `json:"name" binding:"required,max=200"`
Company string `json:"company" binding:"max=200"`
Phone string `json:"phone" binding:"max=20"`
Email string `json:"email" binding:"omitempty,email"`
Website string `json:"website" binding:"max=200"`
CreatedByID uint `json:"created_by_id" validate:"required"`
Name string `json:"name" validate:"required,max=200"`
Company string `json:"company" validate:"max=200"`
Phone string `json:"phone" validate:"max=20"`
Email string `json:"email" validate:"omitempty,email"`
Website string `json:"website" validate:"max=200"`
Notes string `json:"notes"`
StreetAddress string `json:"street_address" binding:"max=255"`
City string `json:"city" binding:"max=100"`
StateProvince string `json:"state_province" binding:"max=100"`
PostalCode string `json:"postal_code" binding:"max=20"`
StreetAddress string `json:"street_address" validate:"max=255"`
City string `json:"city" validate:"max=100"`
StateProvince string `json:"state_province" validate:"max=100"`
PostalCode string `json:"postal_code" validate:"max=20"`
IsFavorite bool `json:"is_favorite"`
SpecialtyIDs []uint `json:"specialty_ids"`
}
// CreateDocumentRequest for creating a new document
type CreateDocumentRequest struct {
ResidenceID uint `json:"residence_id" binding:"required"`
CreatedByID uint `json:"created_by_id" binding:"required"`
Title string `json:"title" binding:"required,max=200"`
ResidenceID uint `json:"residence_id" validate:"required"`
CreatedByID uint `json:"created_by_id" validate:"required"`
Title string `json:"title" validate:"required,max=200"`
Description string `json:"description"`
DocumentType string `json:"document_type" binding:"omitempty,oneof=general warranty receipt contract insurance manual"`
FileURL string `json:"file_url" binding:"max=500"`
FileName string `json:"file_name" binding:"max=255"`
DocumentType string `json:"document_type" validate:"omitempty,oneof=general warranty receipt contract insurance manual"`
FileURL string `json:"file_url" validate:"max=500"`
FileName string `json:"file_name" validate:"max=255"`
FileSize *int64 `json:"file_size"`
MimeType string `json:"mime_type" binding:"max=100"`
MimeType string `json:"mime_type" validate:"max=100"`
PurchaseDate *string `json:"purchase_date"`
ExpiryDate *string `json:"expiry_date"`
PurchasePrice *float64 `json:"purchase_price"`
Vendor string `json:"vendor" binding:"max=200"`
SerialNumber string `json:"serial_number" binding:"max=100"`
ModelNumber string `json:"model_number" binding:"max=100"`
Vendor string `json:"vendor" validate:"max=200"`
SerialNumber string `json:"serial_number" validate:"max=100"`
ModelNumber string `json:"model_number" validate:"max=100"`
TaskID *uint `json:"task_id"`
}
// SendTestNotificationRequest for sending a test push notification
type SendTestNotificationRequest struct {
UserID uint `json:"user_id" binding:"required"`
Title string `json:"title" binding:"required,max=200"`
Body string `json:"body" binding:"required,max=500"`
UserID uint `json:"user_id" validate:"required"`
Title string `json:"title" validate:"required,max=200"`
Body string `json:"body" validate:"required,max=500"`
}
// SendTestEmailRequest for sending a test email
type SendTestEmailRequest struct {
UserID uint `json:"user_id" binding:"required"`
Subject string `json:"subject" binding:"required,max=200"`
Body string `json:"body" binding:"required"`
UserID uint `json:"user_id" validate:"required"`
Subject string `json:"subject" validate:"required,max=200"`
Body string `json:"body" validate:"required"`
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -52,11 +52,10 @@ type UpdateAdminUserRequest struct {
}
// List handles GET /api/admin/admin-users
func (h *AdminUserManagementHandler) List(c *gin.Context) {
func (h *AdminUserManagementHandler) List(c echo.Context) error {
var filters AdminUserFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var adminUsers []models.AdminUser
@@ -92,8 +91,7 @@ func (h *AdminUserManagementHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&adminUsers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin users"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch admin users"})
}
responses := make([]AdminUserResponse, len(adminUsers))
@@ -101,56 +99,49 @@ func (h *AdminUserManagementHandler) List(c *gin.Context) {
responses[i] = h.toAdminUserResponse(&u)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/admin-users/:id
func (h *AdminUserManagementHandler) Get(c *gin.Context) {
func (h *AdminUserManagementHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid admin user ID"})
}
var adminUser models.AdminUser
if err := h.db.First(&adminUser, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Admin user not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Admin user not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch admin user"})
}
c.JSON(http.StatusOK, h.toAdminUserResponse(&adminUser))
return c.JSON(http.StatusOK, h.toAdminUserResponse(&adminUser))
}
// Create handles POST /api/admin/admin-users
func (h *AdminUserManagementHandler) Create(c *gin.Context) {
func (h *AdminUserManagementHandler) Create(c echo.Context) error {
// Only super admins can create admin users
currentAdmin, exists := c.Get(middleware.AdminUserKey)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
currentAdmin := c.Get(middleware.AdminUserKey)
if currentAdmin == nil {
return c.JSON(http.StatusUnauthorized, map[string]interface{}{"error": "Unauthorized"})
}
admin := currentAdmin.(*models.AdminUser)
if !admin.IsSuperAdmin() {
c.JSON(http.StatusForbidden, gin.H{"error": "Only super admins can create admin users"})
return
return c.JSON(http.StatusForbidden, map[string]interface{}{"error": "Only super admins can create admin users"})
}
var req CreateAdminUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Check if email already exists
var existingCount int64
h.db.Model(&models.AdminUser{}).Where("email = ?", req.Email).Count(&existingCount)
if existingCount > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Email already exists"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Email already exists"})
}
adminUser := models.AdminUser{
@@ -169,54 +160,46 @@ func (h *AdminUserManagementHandler) Create(c *gin.Context) {
}
if err := adminUser.SetPassword(req.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to hash password"})
}
if err := h.db.Create(&adminUser).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create admin user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create admin user"})
}
c.JSON(http.StatusCreated, h.toAdminUserResponse(&adminUser))
return c.JSON(http.StatusCreated, h.toAdminUserResponse(&adminUser))
}
// Update handles PUT /api/admin/admin-users/:id
func (h *AdminUserManagementHandler) Update(c *gin.Context) {
func (h *AdminUserManagementHandler) Update(c echo.Context) error {
// Only super admins can update admin users
currentAdmin, exists := c.Get(middleware.AdminUserKey)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
currentAdmin := c.Get(middleware.AdminUserKey)
if currentAdmin == nil {
return c.JSON(http.StatusUnauthorized, map[string]interface{}{"error": "Unauthorized"})
}
admin := currentAdmin.(*models.AdminUser)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid admin user ID"})
}
// Allow users to update themselves, but only super admins can update others
if uint(id) != admin.ID && !admin.IsSuperAdmin() {
c.JSON(http.StatusForbidden, gin.H{"error": "Only super admins can update other admin users"})
return
return c.JSON(http.StatusForbidden, map[string]interface{}{"error": "Only super admins can update other admin users"})
}
var adminUser models.AdminUser
if err := h.db.First(&adminUser, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Admin user not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Admin user not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch admin user"})
}
var req UpdateAdminUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.Email != nil {
@@ -224,8 +207,7 @@ func (h *AdminUserManagementHandler) Update(c *gin.Context) {
var existingCount int64
h.db.Model(&models.AdminUser{}).Where("email = ? AND id != ?", *req.Email, id).Count(&existingCount)
if existingCount > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Email already exists"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Email already exists"})
}
adminUser.Email = *req.Email
}
@@ -242,68 +224,58 @@ func (h *AdminUserManagementHandler) Update(c *gin.Context) {
if req.IsActive != nil && admin.IsSuperAdmin() {
// Prevent disabling yourself
if uint(id) == admin.ID && !*req.IsActive {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot deactivate your own account"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Cannot deactivate your own account"})
}
adminUser.IsActive = *req.IsActive
}
if req.Password != nil {
if err := adminUser.SetPassword(*req.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to hash password"})
}
}
if err := h.db.Save(&adminUser).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update admin user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update admin user"})
}
c.JSON(http.StatusOK, h.toAdminUserResponse(&adminUser))
return c.JSON(http.StatusOK, h.toAdminUserResponse(&adminUser))
}
// Delete handles DELETE /api/admin/admin-users/:id
func (h *AdminUserManagementHandler) Delete(c *gin.Context) {
func (h *AdminUserManagementHandler) Delete(c echo.Context) error {
// Only super admins can delete admin users
currentAdmin, exists := c.Get(middleware.AdminUserKey)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
currentAdmin := c.Get(middleware.AdminUserKey)
if currentAdmin == nil {
return c.JSON(http.StatusUnauthorized, map[string]interface{}{"error": "Unauthorized"})
}
admin := currentAdmin.(*models.AdminUser)
if !admin.IsSuperAdmin() {
c.JSON(http.StatusForbidden, gin.H{"error": "Only super admins can delete admin users"})
return
return c.JSON(http.StatusForbidden, map[string]interface{}{"error": "Only super admins can delete admin users"})
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid admin user ID"})
}
// Prevent self-deletion
if uint(id) == admin.ID {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Cannot delete your own account"})
}
var adminUser models.AdminUser
if err := h.db.First(&adminUser, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Admin user not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Admin user not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch admin user"})
}
if err := h.db.Delete(&adminUser).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete admin user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete admin user"})
}
c.JSON(http.StatusOK, gin.H{"message": "Admin user deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Admin user deleted successfully"})
}
func (h *AdminUserManagementHandler) toAdminUserResponse(u *models.AdminUser) AdminUserResponse {

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -41,11 +41,10 @@ type UpdateAppleSocialAuthRequest struct {
}
// List handles GET /api/admin/apple-social-auth
func (h *AdminAppleSocialAuthHandler) List(c *gin.Context) {
func (h *AdminAppleSocialAuthHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var entries []models.AppleSocialAuth
@@ -75,8 +74,7 @@ func (h *AdminAppleSocialAuthHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&entries).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Apple social auth entries"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch Apple social auth entries"})
}
// Build response
@@ -85,73 +83,63 @@ func (h *AdminAppleSocialAuthHandler) List(c *gin.Context) {
responses[i] = h.toResponse(&entry)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/apple-social-auth/:id
func (h *AdminAppleSocialAuthHandler) Get(c *gin.Context) {
func (h *AdminAppleSocialAuthHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var entry models.AppleSocialAuth
if err := h.db.Preload("User").First(&entry, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Apple social auth entry not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Apple social auth entry not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Apple social auth entry"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch Apple social auth entry"})
}
c.JSON(http.StatusOK, h.toResponse(&entry))
return c.JSON(http.StatusOK, h.toResponse(&entry))
}
// GetByUser handles GET /api/admin/apple-social-auth/user/:user_id
func (h *AdminAppleSocialAuthHandler) GetByUser(c *gin.Context) {
func (h *AdminAppleSocialAuthHandler) GetByUser(c echo.Context) error {
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var entry models.AppleSocialAuth
if err := h.db.Preload("User").Where("user_id = ?", userID).First(&entry).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Apple social auth entry not found for user"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Apple social auth entry not found for user"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Apple social auth entry"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch Apple social auth entry"})
}
c.JSON(http.StatusOK, h.toResponse(&entry))
return c.JSON(http.StatusOK, h.toResponse(&entry))
}
// Update handles PUT /api/admin/apple-social-auth/:id
func (h *AdminAppleSocialAuthHandler) Update(c *gin.Context) {
func (h *AdminAppleSocialAuthHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var entry models.AppleSocialAuth
if err := h.db.First(&entry, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Apple social auth entry not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Apple social auth entry not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Apple social auth entry"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch Apple social auth entry"})
}
var req UpdateAppleSocialAuthRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.Email != nil {
@@ -162,54 +150,47 @@ func (h *AdminAppleSocialAuthHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&entry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update Apple social auth entry"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update Apple social auth entry"})
}
h.db.Preload("User").First(&entry, id)
c.JSON(http.StatusOK, h.toResponse(&entry))
return c.JSON(http.StatusOK, h.toResponse(&entry))
}
// Delete handles DELETE /api/admin/apple-social-auth/:id
func (h *AdminAppleSocialAuthHandler) Delete(c *gin.Context) {
func (h *AdminAppleSocialAuthHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var entry models.AppleSocialAuth
if err := h.db.First(&entry, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Apple social auth entry not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Apple social auth entry not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Apple social auth entry"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch Apple social auth entry"})
}
if err := h.db.Delete(&entry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete Apple social auth entry"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete Apple social auth entry"})
}
c.JSON(http.StatusOK, gin.H{"message": "Apple social auth entry deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Apple social auth entry deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/apple-social-auth/bulk
func (h *AdminAppleSocialAuthHandler) BulkDelete(c *gin.Context) {
func (h *AdminAppleSocialAuthHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if err := h.db.Where("id IN ?", req.IDs).Delete(&models.AppleSocialAuth{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete Apple social auth entries"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete Apple social auth entries"})
}
c.JSON(http.StatusOK, gin.H{"message": "Apple social auth entries deleted successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Apple social auth entries deleted successfully", "count": len(req.IDs)})
}
// toResponse converts an AppleSocialAuth model to AppleSocialAuthResponse

View File

@@ -3,7 +3,7 @@ package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/treytartt/casera-api/internal/config"
"github.com/treytartt/casera-api/internal/middleware"
@@ -68,37 +68,32 @@ func NewAdminUserResponse(admin *models.AdminUser) AdminUserResponse {
}
// Login handles POST /api/admin/auth/login
func (h *AdminAuthHandler) Login(c *gin.Context) {
func (h *AdminAuthHandler) Login(c echo.Context) error {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid request: " + err.Error()})
}
// Find admin by email
admin, err := h.adminRepo.FindByEmail(req.Email)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
return c.JSON(http.StatusUnauthorized, map[string]interface{}{"error": "Invalid email or password"})
}
// Check password
if !admin.CheckPassword(req.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
return c.JSON(http.StatusUnauthorized, map[string]interface{}{"error": "Invalid email or password"})
}
// Check if admin is active
if !admin.IsActive {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Account is disabled"})
return
return c.JSON(http.StatusUnauthorized, map[string]interface{}{"error": "Account is disabled"})
}
// Generate JWT token
token, err := middleware.GenerateAdminToken(admin, h.cfg)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to generate token"})
}
// Update last login
@@ -107,7 +102,7 @@ func (h *AdminAuthHandler) Login(c *gin.Context) {
// Refresh admin data after updating last login
admin, _ = h.adminRepo.FindByID(admin.ID)
c.JSON(http.StatusOK, LoginResponse{
return c.JSON(http.StatusOK, LoginResponse{
Token: token,
Admin: NewAdminUserResponse(admin),
})
@@ -115,26 +110,25 @@ func (h *AdminAuthHandler) Login(c *gin.Context) {
// Logout handles POST /api/admin/auth/logout
// Note: JWT tokens are stateless, so logout is handled client-side by removing the token
func (h *AdminAuthHandler) Logout(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
func (h *AdminAuthHandler) Logout(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Logged out successfully"})
}
// Me handles GET /api/admin/auth/me
func (h *AdminAuthHandler) Me(c *gin.Context) {
admin := c.MustGet(middleware.AdminUserKey).(*models.AdminUser)
c.JSON(http.StatusOK, NewAdminUserResponse(admin))
func (h *AdminAuthHandler) Me(c echo.Context) error {
admin := c.Get(middleware.AdminUserKey).(*models.AdminUser)
return c.JSON(http.StatusOK, NewAdminUserResponse(admin))
}
// RefreshToken handles POST /api/admin/auth/refresh
func (h *AdminAuthHandler) RefreshToken(c *gin.Context) {
admin := c.MustGet(middleware.AdminUserKey).(*models.AdminUser)
func (h *AdminAuthHandler) RefreshToken(c echo.Context) error {
admin := c.Get(middleware.AdminUserKey).(*models.AdminUser)
// Generate new token
token, err := middleware.GenerateAdminToken(admin, h.cfg)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to generate token"})
}
c.JSON(http.StatusOK, gin.H{"token": token})
return c.JSON(http.StatusOK, map[string]interface{}{"token": token})
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -31,11 +31,10 @@ type AuthTokenResponse struct {
}
// List handles GET /api/admin/auth-tokens
func (h *AdminAuthTokenHandler) List(c *gin.Context) {
func (h *AdminAuthTokenHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var tokens []models.AuthToken
@@ -67,8 +66,7 @@ func (h *AdminAuthTokenHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&tokens).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch auth tokens"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch auth tokens"})
}
// Build response
@@ -83,25 +81,22 @@ func (h *AdminAuthTokenHandler) List(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/auth-tokens/:id (id is actually user_id)
func (h *AdminAuthTokenHandler) Get(c *gin.Context) {
func (h *AdminAuthTokenHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var token models.AuthToken
if err := h.db.Preload("User").Where("user_id = ?", id).First(&token).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Auth token not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Auth token not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch auth token"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch auth token"})
}
response := AuthTokenResponse{
@@ -112,44 +107,39 @@ func (h *AdminAuthTokenHandler) Get(c *gin.Context) {
Created: token.Created.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Delete handles DELETE /api/admin/auth-tokens/:id (revoke token)
func (h *AdminAuthTokenHandler) Delete(c *gin.Context) {
func (h *AdminAuthTokenHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
result := h.db.Where("user_id = ?", id).Delete(&models.AuthToken{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke token"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to revoke token"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Auth token not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Auth token not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Auth token revoked successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Auth token revoked successfully"})
}
// BulkDelete handles DELETE /api/admin/auth-tokens/bulk
func (h *AdminAuthTokenHandler) BulkDelete(c *gin.Context) {
func (h *AdminAuthTokenHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
result := h.db.Where("user_id IN ?", req.IDs).Delete(&models.AuthToken{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke tokens"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to revoke tokens"})
}
c.JSON(http.StatusOK, gin.H{"message": "Auth tokens revoked successfully", "count": result.RowsAffected})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Auth tokens revoked successfully", "count": result.RowsAffected})
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/shopspring/decimal"
"gorm.io/gorm"
@@ -55,11 +55,10 @@ type CompletionFilters struct {
}
// List handles GET /api/admin/completions
func (h *AdminCompletionHandler) List(c *gin.Context) {
func (h *AdminCompletionHandler) List(c echo.Context) error {
var filters CompletionFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var completions []models.TaskCompletion
@@ -112,8 +111,7 @@ func (h *AdminCompletionHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&completions).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completions"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completions"})
}
// Build response
@@ -122,71 +120,62 @@ func (h *AdminCompletionHandler) List(c *gin.Context) {
responses[i] = h.toCompletionResponse(&completion)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/completions/:id
func (h *AdminCompletionHandler) Get(c *gin.Context) {
func (h *AdminCompletionHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid completion ID"})
}
var completion models.TaskCompletion
if err := h.db.Preload("Task").Preload("Task.Residence").Preload("CompletedBy").Preload("Images").First(&completion, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion"})
}
c.JSON(http.StatusOK, h.toCompletionResponse(&completion))
return c.JSON(http.StatusOK, h.toCompletionResponse(&completion))
}
// Delete handles DELETE /api/admin/completions/:id
func (h *AdminCompletionHandler) Delete(c *gin.Context) {
func (h *AdminCompletionHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid completion ID"})
}
var completion models.TaskCompletion
if err := h.db.First(&completion, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion"})
}
if err := h.db.Delete(&completion).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete completion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete completion"})
}
c.JSON(http.StatusOK, gin.H{"message": "Completion deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Completion deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/completions/bulk
func (h *AdminCompletionHandler) BulkDelete(c *gin.Context) {
func (h *AdminCompletionHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
result := h.db.Where("id IN ?", req.IDs).Delete(&models.TaskCompletion{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete completions"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete completions"})
}
c.JSON(http.StatusOK, gin.H{"message": "Completions deleted successfully", "count": result.RowsAffected})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Completions deleted successfully", "count": result.RowsAffected})
}
// UpdateCompletionRequest represents the request to update a completion
@@ -196,27 +185,23 @@ type UpdateCompletionRequest struct {
}
// Update handles PUT /api/admin/completions/:id
func (h *AdminCompletionHandler) Update(c *gin.Context) {
func (h *AdminCompletionHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid completion ID"})
}
var completion models.TaskCompletion
if err := h.db.First(&completion, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion"})
}
var req UpdateCompletionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.Notes != nil {
@@ -234,12 +219,11 @@ func (h *AdminCompletionHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&completion).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update completion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update completion"})
}
h.db.Preload("Task").Preload("Task.Residence").Preload("CompletedBy").Preload("Images").First(&completion, id)
c.JSON(http.StatusOK, h.toCompletionResponse(&completion))
return c.JSON(http.StatusOK, h.toCompletionResponse(&completion))
}
// toCompletionResponse converts a TaskCompletion model to CompletionResponse

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -47,15 +47,14 @@ type UpdateCompletionImageRequest struct {
}
// List handles GET /api/admin/completion-images
func (h *AdminCompletionImageHandler) List(c *gin.Context) {
func (h *AdminCompletionImageHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Optional completion_id filter
completionIDStr := c.Query("completion_id")
completionIDStr := c.QueryParam("completion_id")
var images []models.TaskCompletionImage
var total int64
@@ -90,8 +89,7 @@ func (h *AdminCompletionImageHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&images).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion images"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion images"})
}
// Build response with task info
@@ -100,47 +98,41 @@ func (h *AdminCompletionImageHandler) List(c *gin.Context) {
responses[i] = h.toResponse(&image)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/completion-images/:id
func (h *AdminCompletionImageHandler) Get(c *gin.Context) {
func (h *AdminCompletionImageHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.TaskCompletionImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion image"})
}
c.JSON(http.StatusOK, h.toResponse(&image))
return c.JSON(http.StatusOK, h.toResponse(&image))
}
// Create handles POST /api/admin/completion-images
func (h *AdminCompletionImageHandler) Create(c *gin.Context) {
func (h *AdminCompletionImageHandler) Create(c echo.Context) error {
var req CreateCompletionImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify completion exists
var completion models.TaskCompletion
if err := h.db.First(&completion, req.CompletionID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusBadRequest, gin.H{"error": "Task completion not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Task completion not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify completion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to verify completion"})
}
image := models.TaskCompletionImage{
@@ -150,35 +142,30 @@ func (h *AdminCompletionImageHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create completion image"})
}
c.JSON(http.StatusCreated, h.toResponse(&image))
return c.JSON(http.StatusCreated, h.toResponse(&image))
}
// Update handles PUT /api/admin/completion-images/:id
func (h *AdminCompletionImageHandler) Update(c *gin.Context) {
func (h *AdminCompletionImageHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.TaskCompletionImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion image"})
}
var req UpdateCompletionImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.ImageURL != nil {
@@ -189,53 +176,46 @@ func (h *AdminCompletionImageHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update completion image"})
}
c.JSON(http.StatusOK, h.toResponse(&image))
return c.JSON(http.StatusOK, h.toResponse(&image))
}
// Delete handles DELETE /api/admin/completion-images/:id
func (h *AdminCompletionImageHandler) Delete(c *gin.Context) {
func (h *AdminCompletionImageHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.TaskCompletionImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion image"})
}
if err := h.db.Delete(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete completion image"})
}
c.JSON(http.StatusOK, gin.H{"message": "Completion image deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Completion image deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/completion-images/bulk
func (h *AdminCompletionImageHandler) BulkDelete(c *gin.Context) {
func (h *AdminCompletionImageHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if err := h.db.Where("id IN ?", req.IDs).Delete(&models.TaskCompletionImage{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete completion images"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete completion images"})
}
c.JSON(http.StatusOK, gin.H{"message": "Completion images deleted successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Completion images deleted successfully", "count": len(req.IDs)})
}
// toResponse converts a TaskCompletionImage model to AdminCompletionImageResponse

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -34,11 +34,10 @@ type ConfirmationCodeResponse struct {
}
// List handles GET /api/admin/confirmation-codes
func (h *AdminConfirmationCodeHandler) List(c *gin.Context) {
func (h *AdminConfirmationCodeHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var codes []models.ConfirmationCode
@@ -70,8 +69,7 @@ func (h *AdminConfirmationCodeHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&codes).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch confirmation codes"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch confirmation codes"})
}
// Build response
@@ -89,25 +87,22 @@ func (h *AdminConfirmationCodeHandler) List(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/confirmation-codes/:id
func (h *AdminConfirmationCodeHandler) Get(c *gin.Context) {
func (h *AdminConfirmationCodeHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var code models.ConfirmationCode
if err := h.db.Preload("User").First(&code, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Confirmation code not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Confirmation code not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch confirmation code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch confirmation code"})
}
response := ConfirmationCodeResponse{
@@ -121,44 +116,39 @@ func (h *AdminConfirmationCodeHandler) Get(c *gin.Context) {
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Delete handles DELETE /api/admin/confirmation-codes/:id
func (h *AdminConfirmationCodeHandler) Delete(c *gin.Context) {
func (h *AdminConfirmationCodeHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.ConfirmationCode{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete confirmation code"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Confirmation code not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Confirmation code not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Confirmation code deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Confirmation code deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/confirmation-codes/bulk
func (h *AdminConfirmationCodeHandler) BulkDelete(c *gin.Context) {
func (h *AdminConfirmationCodeHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
result := h.db.Where("id IN ?", req.IDs).Delete(&models.ConfirmationCode{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete confirmation codes"})
}
c.JSON(http.StatusOK, gin.H{"message": "Confirmation codes deleted successfully", "count": result.RowsAffected})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Confirmation codes deleted successfully", "count": result.RowsAffected})
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -22,11 +22,10 @@ func NewAdminContractorHandler(db *gorm.DB) *AdminContractorHandler {
}
// List handles GET /api/admin/contractors
func (h *AdminContractorHandler) List(c *gin.Context) {
func (h *AdminContractorHandler) List(c echo.Context) error {
var filters dto.ContractorFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var contractors []models.Contractor
@@ -71,8 +70,7 @@ func (h *AdminContractorHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&contractors).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractors"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch contractors"})
}
// Build response
@@ -81,15 +79,14 @@ func (h *AdminContractorHandler) List(c *gin.Context) {
responses[i] = h.toContractorResponse(&contractor)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/contractors/:id
func (h *AdminContractorHandler) Get(c *gin.Context) {
func (h *AdminContractorHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid contractor ID"})
}
var contractor models.Contractor
@@ -99,11 +96,9 @@ func (h *AdminContractorHandler) Get(c *gin.Context) {
Preload("Specialties").
First(&contractor, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Contractor not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Contractor not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractor"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch contractor"})
}
response := dto.ContractorDetailResponse{
@@ -115,39 +110,34 @@ func (h *AdminContractorHandler) Get(c *gin.Context) {
h.db.Model(&models.Task{}).Where("contractor_id = ?", contractor.ID).Count(&taskCount)
response.TaskCount = int(taskCount)
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Update handles PUT /api/admin/contractors/:id
func (h *AdminContractorHandler) Update(c *gin.Context) {
func (h *AdminContractorHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid contractor ID"})
}
var contractor models.Contractor
if err := h.db.First(&contractor, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Contractor not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Contractor not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractor"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch contractor"})
}
var req dto.UpdateContractorRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify residence if changing
if req.ResidenceID != nil {
var residence models.Residence
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Residence not found"})
}
contractor.ResidenceID = req.ResidenceID
}
@@ -155,8 +145,7 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
if req.CreatedByID != nil {
var user models.User
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Created by user not found"})
}
contractor.CreatedByID = *req.CreatedByID
}
@@ -213,36 +202,32 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&contractor).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update contractor"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update contractor"})
}
h.db.Preload("Residence").Preload("CreatedBy").Preload("Specialties").First(&contractor, id)
c.JSON(http.StatusOK, h.toContractorResponse(&contractor))
return c.JSON(http.StatusOK, h.toContractorResponse(&contractor))
}
// Create handles POST /api/admin/contractors
func (h *AdminContractorHandler) Create(c *gin.Context) {
func (h *AdminContractorHandler) Create(c echo.Context) error {
var req dto.CreateContractorRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify residence exists if provided
if req.ResidenceID != nil {
var residence models.Residence
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Residence not found"})
}
}
// Verify created_by user exists
var creator models.User
if err := h.db.First(&creator, req.CreatedByID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Creator user not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Creator user not found"})
}
contractor := models.Contractor{
@@ -263,8 +248,7 @@ func (h *AdminContractorHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&contractor).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create contractor"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create contractor"})
}
// Add specialties if provided
@@ -275,52 +259,46 @@ func (h *AdminContractorHandler) Create(c *gin.Context) {
}
h.db.Preload("Residence").Preload("CreatedBy").Preload("Specialties").First(&contractor, contractor.ID)
c.JSON(http.StatusCreated, h.toContractorResponse(&contractor))
return c.JSON(http.StatusCreated, h.toContractorResponse(&contractor))
}
// Delete handles DELETE /api/admin/contractors/:id
func (h *AdminContractorHandler) Delete(c *gin.Context) {
func (h *AdminContractorHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid contractor ID"})
}
var contractor models.Contractor
if err := h.db.First(&contractor, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Contractor not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Contractor not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractor"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch contractor"})
}
// Soft delete
contractor.IsActive = false
if err := h.db.Save(&contractor).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractor"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete contractor"})
}
c.JSON(http.StatusOK, gin.H{"message": "Contractor deactivated successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Contractor deactivated successfully"})
}
// BulkDelete handles DELETE /api/admin/contractors/bulk
func (h *AdminContractorHandler) BulkDelete(c *gin.Context) {
func (h *AdminContractorHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Soft delete - deactivate all
if err := h.db.Model(&models.Contractor{}).Where("id IN ?", req.IDs).Update("is_active", false).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractors"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete contractors"})
}
c.JSON(http.StatusOK, gin.H{"message": "Contractors deactivated successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Contractors deactivated successfully", "count": len(req.IDs)})
}
func (h *AdminContractorHandler) toContractorResponse(contractor *models.Contractor) dto.ContractorResponse {

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/models"
@@ -93,7 +93,7 @@ type SubscriptionStats struct {
}
// GetStats handles GET /api/admin/dashboard/stats
func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
func (h *AdminDashboardHandler) GetStats(c echo.Context) error {
stats := DashboardStats{}
now := time.Now()
thirtyDaysAgo := now.AddDate(0, 0, -30)
@@ -164,5 +164,5 @@ func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
h.db.Model(&models.UserSubscription{}).Where("tier = ?", "premium").Count(&stats.Subscriptions.Premium)
h.db.Model(&models.UserSubscription{}).Where("tier = ?", "pro").Count(&stats.Subscriptions.Pro)
c.JSON(http.StatusOK, stats)
return c.JSON(http.StatusOK, stats)
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -47,11 +47,10 @@ type GCMDeviceResponse struct {
}
// ListAPNS handles GET /api/admin/devices/apns
func (h *AdminDeviceHandler) ListAPNS(c *gin.Context) {
func (h *AdminDeviceHandler) ListAPNS(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var devices []models.APNSDevice
@@ -79,8 +78,7 @@ func (h *AdminDeviceHandler) ListAPNS(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&devices).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch devices"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch devices"})
}
responses := make([]APNSDeviceResponse, len(devices))
@@ -101,15 +99,14 @@ func (h *AdminDeviceHandler) ListAPNS(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// ListGCM handles GET /api/admin/devices/gcm
func (h *AdminDeviceHandler) ListGCM(c *gin.Context) {
func (h *AdminDeviceHandler) ListGCM(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var devices []models.GCMDevice
@@ -136,8 +133,7 @@ func (h *AdminDeviceHandler) ListGCM(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&devices).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch devices"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch devices"})
}
responses := make([]GCMDeviceResponse, len(devices))
@@ -159,159 +155,139 @@ func (h *AdminDeviceHandler) ListGCM(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// UpdateAPNS handles PUT /api/admin/devices/apns/:id
func (h *AdminDeviceHandler) UpdateAPNS(c *gin.Context) {
func (h *AdminDeviceHandler) UpdateAPNS(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var device models.APNSDevice
if err := h.db.First(&device, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Device not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch device"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch device"})
}
var req struct {
Active bool `json:"active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
device.Active = req.Active
if err := h.db.Save(&device).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update device"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update device"})
}
c.JSON(http.StatusOK, gin.H{"message": "Device updated successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Device updated successfully"})
}
// UpdateGCM handles PUT /api/admin/devices/gcm/:id
func (h *AdminDeviceHandler) UpdateGCM(c *gin.Context) {
func (h *AdminDeviceHandler) UpdateGCM(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var device models.GCMDevice
if err := h.db.First(&device, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Device not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch device"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch device"})
}
var req struct {
Active bool `json:"active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
device.Active = req.Active
if err := h.db.Save(&device).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update device"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update device"})
}
c.JSON(http.StatusOK, gin.H{"message": "Device updated successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Device updated successfully"})
}
// DeleteAPNS handles DELETE /api/admin/devices/apns/:id
func (h *AdminDeviceHandler) DeleteAPNS(c *gin.Context) {
func (h *AdminDeviceHandler) DeleteAPNS(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.APNSDevice{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete device"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete device"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Device not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Device deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Device deleted successfully"})
}
// DeleteGCM handles DELETE /api/admin/devices/gcm/:id
func (h *AdminDeviceHandler) DeleteGCM(c *gin.Context) {
func (h *AdminDeviceHandler) DeleteGCM(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.GCMDevice{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete device"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete device"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Device not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Device deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Device deleted successfully"})
}
// BulkDeleteAPNS handles DELETE /api/admin/devices/apns/bulk
func (h *AdminDeviceHandler) BulkDeleteAPNS(c *gin.Context) {
func (h *AdminDeviceHandler) BulkDeleteAPNS(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
result := h.db.Where("id IN ?", req.IDs).Delete(&models.APNSDevice{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete devices"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete devices"})
}
c.JSON(http.StatusOK, gin.H{"message": "Devices deleted successfully", "count": result.RowsAffected})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Devices deleted successfully", "count": result.RowsAffected})
}
// BulkDeleteGCM handles DELETE /api/admin/devices/gcm/bulk
func (h *AdminDeviceHandler) BulkDeleteGCM(c *gin.Context) {
func (h *AdminDeviceHandler) BulkDeleteGCM(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
result := h.db.Where("id IN ?", req.IDs).Delete(&models.GCMDevice{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete devices"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete devices"})
}
c.JSON(http.StatusOK, gin.H{"message": "Devices deleted successfully", "count": result.RowsAffected})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Devices deleted successfully", "count": result.RowsAffected})
}
// GetStats handles GET /api/admin/devices/stats
func (h *AdminDeviceHandler) GetStats(c *gin.Context) {
func (h *AdminDeviceHandler) GetStats(c echo.Context) error {
var apnsTotal, apnsActive, gcmTotal, gcmActive int64
h.db.Model(&models.APNSDevice{}).Count(&apnsTotal)
@@ -319,12 +295,12 @@ func (h *AdminDeviceHandler) GetStats(c *gin.Context) {
h.db.Model(&models.GCMDevice{}).Count(&gcmTotal)
h.db.Model(&models.GCMDevice{}).Where("active = ?", true).Count(&gcmActive)
c.JSON(http.StatusOK, gin.H{
"apns": gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"apns": map[string]interface{}{
"total": apnsTotal,
"active": apnsActive,
},
"gcm": gin.H{
"gcm": map[string]interface{}{
"total": gcmTotal,
"active": gcmActive,
},

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/shopspring/decimal"
"gorm.io/gorm"
@@ -24,11 +24,10 @@ func NewAdminDocumentHandler(db *gorm.DB) *AdminDocumentHandler {
}
// List handles GET /api/admin/documents
func (h *AdminDocumentHandler) List(c *gin.Context) {
func (h *AdminDocumentHandler) List(c echo.Context) error {
var filters dto.DocumentFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var documents []models.Document
@@ -73,8 +72,7 @@ func (h *AdminDocumentHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&documents).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch documents"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch documents"})
}
// Build response
@@ -83,15 +81,14 @@ func (h *AdminDocumentHandler) List(c *gin.Context) {
responses[i] = h.toDocumentResponse(&doc)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/documents/:id
func (h *AdminDocumentHandler) Get(c *gin.Context) {
func (h *AdminDocumentHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid document ID"})
}
var document models.Document
@@ -102,11 +99,9 @@ func (h *AdminDocumentHandler) Get(c *gin.Context) {
Preload("Images").
First(&document, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Document not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch document"})
}
response := dto.DocumentDetailResponse{
@@ -117,39 +112,34 @@ func (h *AdminDocumentHandler) Get(c *gin.Context) {
response.TaskTitle = &document.Task.Title
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Update handles PUT /api/admin/documents/:id
func (h *AdminDocumentHandler) Update(c *gin.Context) {
func (h *AdminDocumentHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid document ID"})
}
var document models.Document
if err := h.db.First(&document, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Document not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch document"})
}
var req dto.UpdateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify residence if changing
if req.ResidenceID != nil {
var residence models.Residence
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Residence not found"})
}
document.ResidenceID = *req.ResidenceID
}
@@ -157,8 +147,7 @@ func (h *AdminDocumentHandler) Update(c *gin.Context) {
if req.CreatedByID != nil {
var user models.User
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Created by user not found"})
}
document.CreatedByID = *req.CreatedByID
}
@@ -232,34 +221,30 @@ func (h *AdminDocumentHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&document).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update document"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update document"})
}
h.db.Preload("Residence").Preload("CreatedBy").Preload("Images").First(&document, id)
c.JSON(http.StatusOK, h.toDocumentResponse(&document))
return c.JSON(http.StatusOK, h.toDocumentResponse(&document))
}
// Create handles POST /api/admin/documents
func (h *AdminDocumentHandler) Create(c *gin.Context) {
func (h *AdminDocumentHandler) Create(c echo.Context) error {
var req dto.CreateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify residence exists
var residence models.Residence
if err := h.db.First(&residence, req.ResidenceID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Residence not found"})
}
// Verify created_by user exists
var creator models.User
if err := h.db.First(&creator, req.CreatedByID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Creator user not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Creator user not found"})
}
documentType := models.DocumentTypeGeneral
@@ -302,57 +287,50 @@ func (h *AdminDocumentHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&document).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create document"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create document"})
}
h.db.Preload("Residence").Preload("CreatedBy").Preload("Images").First(&document, document.ID)
c.JSON(http.StatusCreated, h.toDocumentResponse(&document))
return c.JSON(http.StatusCreated, h.toDocumentResponse(&document))
}
// Delete handles DELETE /api/admin/documents/:id
func (h *AdminDocumentHandler) Delete(c *gin.Context) {
func (h *AdminDocumentHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid document ID"})
}
var document models.Document
if err := h.db.First(&document, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Document not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch document"})
}
// Soft delete
document.IsActive = false
if err := h.db.Save(&document).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete document"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete document"})
}
c.JSON(http.StatusOK, gin.H{"message": "Document deactivated successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Document deactivated successfully"})
}
// BulkDelete handles DELETE /api/admin/documents/bulk
func (h *AdminDocumentHandler) BulkDelete(c *gin.Context) {
func (h *AdminDocumentHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Soft delete - deactivate all
if err := h.db.Model(&models.Document{}).Where("id IN ?", req.IDs).Update("is_active", false).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete documents"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete documents"})
}
c.JSON(http.StatusOK, gin.H{"message": "Documents deactivated successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Documents deactivated successfully", "count": len(req.IDs)})
}
func (h *AdminDocumentHandler) toDocumentResponse(doc *models.Document) dto.DocumentResponse {

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -48,15 +48,14 @@ type UpdateDocumentImageRequest struct {
}
// List handles GET /api/admin/document-images
func (h *AdminDocumentImageHandler) List(c *gin.Context) {
func (h *AdminDocumentImageHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Optional document_id filter
documentIDStr := c.Query("document_id")
documentIDStr := c.QueryParam("document_id")
var images []models.DocumentImage
var total int64
@@ -91,8 +90,7 @@ func (h *AdminDocumentImageHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&images).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document images"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch document images"})
}
// Build response with document info
@@ -101,47 +99,41 @@ func (h *AdminDocumentImageHandler) List(c *gin.Context) {
responses[i] = h.toResponse(&image)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/document-images/:id
func (h *AdminDocumentImageHandler) Get(c *gin.Context) {
func (h *AdminDocumentImageHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.DocumentImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Document image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Document image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch document image"})
}
c.JSON(http.StatusOK, h.toResponse(&image))
return c.JSON(http.StatusOK, h.toResponse(&image))
}
// Create handles POST /api/admin/document-images
func (h *AdminDocumentImageHandler) Create(c *gin.Context) {
func (h *AdminDocumentImageHandler) Create(c echo.Context) error {
var req CreateDocumentImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify document exists
var document models.Document
if err := h.db.First(&document, req.DocumentID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusBadRequest, gin.H{"error": "Document not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Document not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify document"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to verify document"})
}
image := models.DocumentImage{
@@ -151,35 +143,30 @@ func (h *AdminDocumentImageHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create document image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create document image"})
}
c.JSON(http.StatusCreated, h.toResponse(&image))
return c.JSON(http.StatusCreated, h.toResponse(&image))
}
// Update handles PUT /api/admin/document-images/:id
func (h *AdminDocumentImageHandler) Update(c *gin.Context) {
func (h *AdminDocumentImageHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.DocumentImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Document image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Document image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch document image"})
}
var req UpdateDocumentImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.ImageURL != nil {
@@ -190,53 +177,46 @@ func (h *AdminDocumentImageHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update document image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update document image"})
}
c.JSON(http.StatusOK, h.toResponse(&image))
return c.JSON(http.StatusOK, h.toResponse(&image))
}
// Delete handles DELETE /api/admin/document-images/:id
func (h *AdminDocumentImageHandler) Delete(c *gin.Context) {
func (h *AdminDocumentImageHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.DocumentImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Document image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Document image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch document image"})
}
if err := h.db.Delete(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete document image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete document image"})
}
c.JSON(http.StatusOK, gin.H{"message": "Document image deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Document image deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/document-images/bulk
func (h *AdminDocumentImageHandler) BulkDelete(c *gin.Context) {
func (h *AdminDocumentImageHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if err := h.db.Where("id IN ?", req.IDs).Delete(&models.DocumentImage{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete document images"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete document images"})
}
c.JSON(http.StatusOK, gin.H{"message": "Document images deleted successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Document images deleted successfully", "count": len(req.IDs)})
}
// toResponse converts a DocumentImage model to DocumentImageResponse

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -34,11 +34,10 @@ type FeatureBenefitResponse struct {
}
// List handles GET /api/admin/feature-benefits
func (h *AdminFeatureBenefitHandler) List(c *gin.Context) {
func (h *AdminFeatureBenefitHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var benefits []models.FeatureBenefit
@@ -61,8 +60,7 @@ func (h *AdminFeatureBenefitHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&benefits).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feature benefits"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch feature benefits"})
}
responses := make([]FeatureBenefitResponse, len(benefits))
@@ -79,25 +77,22 @@ func (h *AdminFeatureBenefitHandler) List(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/feature-benefits/:id
func (h *AdminFeatureBenefitHandler) Get(c *gin.Context) {
func (h *AdminFeatureBenefitHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var benefit models.FeatureBenefit
if err := h.db.First(&benefit, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Feature benefit not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Feature benefit not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feature benefit"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch feature benefit"})
}
response := FeatureBenefitResponse{
@@ -111,11 +106,11 @@ func (h *AdminFeatureBenefitHandler) Get(c *gin.Context) {
UpdatedAt: benefit.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Create handles POST /api/admin/feature-benefits
func (h *AdminFeatureBenefitHandler) Create(c *gin.Context) {
func (h *AdminFeatureBenefitHandler) Create(c echo.Context) error {
var req struct {
FeatureName string `json:"feature_name" binding:"required"`
FreeTierText string `json:"free_tier_text" binding:"required"`
@@ -124,9 +119,8 @@ func (h *AdminFeatureBenefitHandler) Create(c *gin.Context) {
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
benefit := models.FeatureBenefit{
@@ -142,11 +136,10 @@ func (h *AdminFeatureBenefitHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&benefit).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create feature benefit"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create feature benefit"})
}
c.JSON(http.StatusCreated, FeatureBenefitResponse{
return c.JSON(http.StatusCreated, FeatureBenefitResponse{
ID: benefit.ID,
FeatureName: benefit.FeatureName,
FreeTierText: benefit.FreeTierText,
@@ -159,21 +152,18 @@ func (h *AdminFeatureBenefitHandler) Create(c *gin.Context) {
}
// Update handles PUT /api/admin/feature-benefits/:id
func (h *AdminFeatureBenefitHandler) Update(c *gin.Context) {
func (h *AdminFeatureBenefitHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var benefit models.FeatureBenefit
if err := h.db.First(&benefit, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Feature benefit not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Feature benefit not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feature benefit"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch feature benefit"})
}
var req struct {
@@ -184,9 +174,8 @@ func (h *AdminFeatureBenefitHandler) Update(c *gin.Context) {
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.FeatureName != nil {
@@ -206,11 +195,10 @@ func (h *AdminFeatureBenefitHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&benefit).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update feature benefit"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update feature benefit"})
}
c.JSON(http.StatusOK, FeatureBenefitResponse{
return c.JSON(http.StatusOK, FeatureBenefitResponse{
ID: benefit.ID,
FeatureName: benefit.FeatureName,
FreeTierText: benefit.FreeTierText,
@@ -223,23 +211,20 @@ func (h *AdminFeatureBenefitHandler) Update(c *gin.Context) {
}
// Delete handles DELETE /api/admin/feature-benefits/:id
func (h *AdminFeatureBenefitHandler) Delete(c *gin.Context) {
func (h *AdminFeatureBenefitHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.FeatureBenefit{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete feature benefit"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete feature benefit"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Feature benefit not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Feature benefit not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Feature benefit deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Feature benefit deleted successfully"})
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/models"
@@ -28,7 +28,7 @@ type LimitationsSettingsResponse struct {
}
// GetSettings handles GET /api/admin/limitations/settings
func (h *AdminLimitationsHandler) GetSettings(c *gin.Context) {
func (h *AdminLimitationsHandler) GetSettings(c echo.Context) error {
var settings models.SubscriptionSettings
if err := h.db.First(&settings, 1).Error; err != nil {
if err == gorm.ErrRecordNotFound {
@@ -36,12 +36,11 @@ func (h *AdminLimitationsHandler) GetSettings(c *gin.Context) {
settings = models.SubscriptionSettings{ID: 1, EnableLimitations: false}
h.db.Create(&settings)
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch settings"})
}
}
c.JSON(http.StatusOK, LimitationsSettingsResponse{
return c.JSON(http.StatusOK, LimitationsSettingsResponse{
EnableLimitations: settings.EnableLimitations,
})
}
@@ -52,11 +51,10 @@ type UpdateLimitationsSettingsRequest struct {
}
// UpdateSettings handles PUT /api/admin/limitations/settings
func (h *AdminLimitationsHandler) UpdateSettings(c *gin.Context) {
func (h *AdminLimitationsHandler) UpdateSettings(c echo.Context) error {
var req UpdateLimitationsSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var settings models.SubscriptionSettings
@@ -64,8 +62,7 @@ func (h *AdminLimitationsHandler) UpdateSettings(c *gin.Context) {
if err == gorm.ErrRecordNotFound {
settings = models.SubscriptionSettings{ID: 1}
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch settings"})
}
}
@@ -74,11 +71,10 @@ func (h *AdminLimitationsHandler) UpdateSettings(c *gin.Context) {
}
if err := h.db.Save(&settings).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update settings"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update settings"})
}
c.JSON(http.StatusOK, LimitationsSettingsResponse{
return c.JSON(http.StatusOK, LimitationsSettingsResponse{
EnableLimitations: settings.EnableLimitations,
})
}
@@ -111,11 +107,10 @@ func toTierLimitsResponse(t *models.TierLimits) TierLimitsResponse {
}
// ListTierLimits handles GET /api/admin/limitations/tier-limits
func (h *AdminLimitationsHandler) ListTierLimits(c *gin.Context) {
func (h *AdminLimitationsHandler) ListTierLimits(c echo.Context) error {
var limits []models.TierLimits
if err := h.db.Order("tier").Find(&limits).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tier limits"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch tier limits"})
}
// If no limits exist, create defaults
@@ -132,18 +127,17 @@ func (h *AdminLimitationsHandler) ListTierLimits(c *gin.Context) {
responses[i] = toTierLimitsResponse(&l)
}
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"data": responses,
"total": len(responses),
})
}
// GetTierLimits handles GET /api/admin/limitations/tier-limits/:tier
func (h *AdminLimitationsHandler) GetTierLimits(c *gin.Context) {
func (h *AdminLimitationsHandler) GetTierLimits(c echo.Context) error {
tier := c.Param("tier")
if tier != "free" && tier != "pro" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tier. Must be 'free' or 'pro'"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid tier. Must be 'free' or 'pro'"})
}
var limits models.TierLimits
@@ -157,12 +151,11 @@ func (h *AdminLimitationsHandler) GetTierLimits(c *gin.Context) {
}
h.db.Create(&limits)
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tier limits"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch tier limits"})
}
}
c.JSON(http.StatusOK, toTierLimitsResponse(&limits))
return c.JSON(http.StatusOK, toTierLimitsResponse(&limits))
}
// UpdateTierLimitsRequest represents the update request for tier limits
@@ -174,17 +167,15 @@ type UpdateTierLimitsRequest struct {
}
// UpdateTierLimits handles PUT /api/admin/limitations/tier-limits/:tier
func (h *AdminLimitationsHandler) UpdateTierLimits(c *gin.Context) {
func (h *AdminLimitationsHandler) UpdateTierLimits(c echo.Context) error {
tier := c.Param("tier")
if tier != "free" && tier != "pro" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tier. Must be 'free' or 'pro'"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid tier. Must be 'free' or 'pro'"})
}
var req UpdateTierLimitsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var limits models.TierLimits
@@ -193,8 +184,7 @@ func (h *AdminLimitationsHandler) UpdateTierLimits(c *gin.Context) {
// Create new entry
limits = models.TierLimits{Tier: models.SubscriptionTier(tier)}
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tier limits"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch tier limits"})
}
}
@@ -207,11 +197,10 @@ func (h *AdminLimitationsHandler) UpdateTierLimits(c *gin.Context) {
limits.DocumentsLimit = req.DocumentsLimit
if err := h.db.Save(&limits).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update tier limits"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update tier limits"})
}
c.JSON(http.StatusOK, toTierLimitsResponse(&limits))
return c.JSON(http.StatusOK, toTierLimitsResponse(&limits))
}
// === Upgrade Triggers ===
@@ -253,7 +242,7 @@ var availableTriggerKeys = []string{
}
// GetAvailableTriggerKeys handles GET /api/admin/limitations/upgrade-triggers/keys
func (h *AdminLimitationsHandler) GetAvailableTriggerKeys(c *gin.Context) {
func (h *AdminLimitationsHandler) GetAvailableTriggerKeys(c echo.Context) error {
type KeyOption struct {
Key string `json:"key"`
Label string `json:"label"`
@@ -267,15 +256,14 @@ func (h *AdminLimitationsHandler) GetAvailableTriggerKeys(c *gin.Context) {
{Key: "view_documents", Label: "View Documents & Warranties"},
}
c.JSON(http.StatusOK, keys)
return c.JSON(http.StatusOK, keys)
}
// ListUpgradeTriggers handles GET /api/admin/limitations/upgrade-triggers
func (h *AdminLimitationsHandler) ListUpgradeTriggers(c *gin.Context) {
func (h *AdminLimitationsHandler) ListUpgradeTriggers(c echo.Context) error {
var triggers []models.UpgradeTrigger
if err := h.db.Order("trigger_key").Find(&triggers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch upgrade triggers"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch upgrade triggers"})
}
responses := make([]UpgradeTriggerResponse, len(triggers))
@@ -283,31 +271,28 @@ func (h *AdminLimitationsHandler) ListUpgradeTriggers(c *gin.Context) {
responses[i] = toUpgradeTriggerResponse(&t)
}
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"data": responses,
"total": len(responses),
})
}
// GetUpgradeTrigger handles GET /api/admin/limitations/upgrade-triggers/:id
func (h *AdminLimitationsHandler) GetUpgradeTrigger(c *gin.Context) {
func (h *AdminLimitationsHandler) GetUpgradeTrigger(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var trigger models.UpgradeTrigger
if err := h.db.First(&trigger, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Upgrade trigger not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Upgrade trigger not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch upgrade trigger"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch upgrade trigger"})
}
c.JSON(http.StatusOK, toUpgradeTriggerResponse(&trigger))
return c.JSON(http.StatusOK, toUpgradeTriggerResponse(&trigger))
}
// CreateUpgradeTriggerRequest represents the create request
@@ -321,11 +306,10 @@ type CreateUpgradeTriggerRequest struct {
}
// CreateUpgradeTrigger handles POST /api/admin/limitations/upgrade-triggers
func (h *AdminLimitationsHandler) CreateUpgradeTrigger(c *gin.Context) {
func (h *AdminLimitationsHandler) CreateUpgradeTrigger(c echo.Context) error {
var req CreateUpgradeTriggerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Validate trigger key
@@ -337,15 +321,13 @@ func (h *AdminLimitationsHandler) CreateUpgradeTrigger(c *gin.Context) {
}
}
if !validKey {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid trigger_key"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid trigger_key"})
}
// Check if trigger key already exists
var existing models.UpgradeTrigger
if err := h.db.Where("trigger_key = ?", req.TriggerKey).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Trigger key already exists"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Trigger key already exists"})
}
trigger := models.UpgradeTrigger{
@@ -365,11 +347,10 @@ func (h *AdminLimitationsHandler) CreateUpgradeTrigger(c *gin.Context) {
}
if err := h.db.Create(&trigger).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upgrade trigger"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create upgrade trigger"})
}
c.JSON(http.StatusCreated, toUpgradeTriggerResponse(&trigger))
return c.JSON(http.StatusCreated, toUpgradeTriggerResponse(&trigger))
}
// UpdateUpgradeTriggerRequest represents the update request
@@ -383,27 +364,23 @@ type UpdateUpgradeTriggerRequest struct {
}
// UpdateUpgradeTrigger handles PUT /api/admin/limitations/upgrade-triggers/:id
func (h *AdminLimitationsHandler) UpdateUpgradeTrigger(c *gin.Context) {
func (h *AdminLimitationsHandler) UpdateUpgradeTrigger(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var trigger models.UpgradeTrigger
if err := h.db.First(&trigger, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Upgrade trigger not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Upgrade trigger not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch upgrade trigger"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch upgrade trigger"})
}
var req UpdateUpgradeTriggerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.TriggerKey != nil {
@@ -416,15 +393,13 @@ func (h *AdminLimitationsHandler) UpdateUpgradeTrigger(c *gin.Context) {
}
}
if !validKey {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid trigger_key"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid trigger_key"})
}
// Check if key is already used by another trigger
if *req.TriggerKey != trigger.TriggerKey {
var existing models.UpgradeTrigger
if err := h.db.Where("trigger_key = ?", *req.TriggerKey).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Trigger key already exists"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Trigger key already exists"})
}
}
trigger.TriggerKey = *req.TriggerKey
@@ -446,35 +421,30 @@ func (h *AdminLimitationsHandler) UpdateUpgradeTrigger(c *gin.Context) {
}
if err := h.db.Save(&trigger).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update upgrade trigger"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update upgrade trigger"})
}
c.JSON(http.StatusOK, toUpgradeTriggerResponse(&trigger))
return c.JSON(http.StatusOK, toUpgradeTriggerResponse(&trigger))
}
// DeleteUpgradeTrigger handles DELETE /api/admin/limitations/upgrade-triggers/:id
func (h *AdminLimitationsHandler) DeleteUpgradeTrigger(c *gin.Context) {
func (h *AdminLimitationsHandler) DeleteUpgradeTrigger(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var trigger models.UpgradeTrigger
if err := h.db.First(&trigger, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Upgrade trigger not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Upgrade trigger not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch upgrade trigger"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch upgrade trigger"})
}
if err := h.db.Delete(&trigger).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete upgrade trigger"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete upgrade trigger"})
}
c.JSON(http.StatusOK, gin.H{"message": "Upgrade trigger deleted"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Upgrade trigger deleted"})
}

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
@@ -28,18 +28,15 @@ func NewAdminLookupHandler(db *gorm.DB) *AdminLookupHandler {
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")
@@ -51,18 +48,15 @@ func (h *AdminLookupHandler) refreshCategoriesCache(ctx context.Context) {
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")
@@ -74,18 +68,15 @@ func (h *AdminLookupHandler) refreshPrioritiesCache(ctx context.Context) {
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")
@@ -97,18 +88,15 @@ func (h *AdminLookupHandler) refreshFrequenciesCache(ctx context.Context) {
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")
@@ -120,18 +108,15 @@ func (h *AdminLookupHandler) refreshResidenceTypesCache(ctx context.Context) {
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")
@@ -144,12 +129,10 @@ func (h *AdminLookupHandler) refreshSpecialtiesCache(ctx context.Context) {
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")
}
@@ -173,11 +156,10 @@ type CreateUpdateCategoryRequest struct {
DisplayOrder *int `json:"display_order"`
}
func (h *AdminLookupHandler) ListCategories(c *gin.Context) {
func (h *AdminLookupHandler) ListCategories(c echo.Context) error {
var categories []models.TaskCategory
if err := h.db.Order("display_order ASC, name ASC").Find(&categories).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch categories"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch categories"})
}
responses := make([]TaskCategoryResponse, len(categories))
@@ -192,14 +174,13 @@ func (h *AdminLookupHandler) ListCategories(c *gin.Context) {
}
}
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
return c.JSON(http.StatusOK, map[string]interface{}{"data": responses, "total": len(responses)})
}
func (h *AdminLookupHandler) CreateCategory(c *gin.Context) {
func (h *AdminLookupHandler) CreateCategory(c echo.Context) error {
var req CreateUpdateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
category := models.TaskCategory{
@@ -213,14 +194,13 @@ func (h *AdminLookupHandler) CreateCategory(c *gin.Context) {
}
if err := h.db.Create(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create category"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create category"})
}
// Refresh cache after creating
h.refreshCategoriesCache(c.Request.Context())
h.refreshCategoriesCache(c.Request().Context())
c.JSON(http.StatusCreated, TaskCategoryResponse{
return c.JSON(http.StatusCreated, TaskCategoryResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
@@ -230,27 +210,23 @@ func (h *AdminLookupHandler) CreateCategory(c *gin.Context) {
})
}
func (h *AdminLookupHandler) UpdateCategory(c *gin.Context) {
func (h *AdminLookupHandler) UpdateCategory(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid category ID"})
}
var category models.TaskCategory
if err := h.db.First(&category, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Category not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch category"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch category"})
}
var req CreateUpdateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
category.Name = req.Name
@@ -262,14 +238,13 @@ func (h *AdminLookupHandler) UpdateCategory(c *gin.Context) {
}
if err := h.db.Save(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update category"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update category"})
}
// Refresh cache after updating
h.refreshCategoriesCache(c.Request.Context())
h.refreshCategoriesCache(c.Request().Context())
c.JSON(http.StatusOK, TaskCategoryResponse{
return c.JSON(http.StatusOK, TaskCategoryResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
@@ -279,30 +254,27 @@ func (h *AdminLookupHandler) UpdateCategory(c *gin.Context) {
})
}
func (h *AdminLookupHandler) DeleteCategory(c *gin.Context) {
func (h *AdminLookupHandler) DeleteCategory(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid category ID"})
}
// Check if category is in use
var count int64
h.db.Model(&models.Task{}).Where("category_id = ?", id).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete category that is in use by tasks"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Cannot delete category that is in use by tasks"})
}
if err := h.db.Delete(&models.TaskCategory{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete category"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete category"})
}
// Refresh cache after deleting
h.refreshCategoriesCache(c.Request.Context())
h.refreshCategoriesCache(c.Request().Context())
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Category deleted successfully"})
}
// ========== Task Priorities ==========
@@ -322,11 +294,10 @@ type CreateUpdatePriorityRequest struct {
DisplayOrder *int `json:"display_order"`
}
func (h *AdminLookupHandler) ListPriorities(c *gin.Context) {
func (h *AdminLookupHandler) ListPriorities(c echo.Context) error {
var priorities []models.TaskPriority
if err := h.db.Order("display_order ASC, level ASC").Find(&priorities).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch priorities"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch priorities"})
}
responses := make([]TaskPriorityResponse, len(priorities))
@@ -340,14 +311,13 @@ func (h *AdminLookupHandler) ListPriorities(c *gin.Context) {
}
}
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
return c.JSON(http.StatusOK, map[string]interface{}{"data": responses, "total": len(responses)})
}
func (h *AdminLookupHandler) CreatePriority(c *gin.Context) {
func (h *AdminLookupHandler) CreatePriority(c echo.Context) error {
var req CreateUpdatePriorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
priority := models.TaskPriority{
@@ -360,14 +330,13 @@ func (h *AdminLookupHandler) CreatePriority(c *gin.Context) {
}
if err := h.db.Create(&priority).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create priority"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create priority"})
}
// Refresh cache after creating
h.refreshPrioritiesCache(c.Request.Context())
h.refreshPrioritiesCache(c.Request().Context())
c.JSON(http.StatusCreated, TaskPriorityResponse{
return c.JSON(http.StatusCreated, TaskPriorityResponse{
ID: priority.ID,
Name: priority.Name,
Level: priority.Level,
@@ -376,27 +345,23 @@ func (h *AdminLookupHandler) CreatePriority(c *gin.Context) {
})
}
func (h *AdminLookupHandler) UpdatePriority(c *gin.Context) {
func (h *AdminLookupHandler) UpdatePriority(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid priority ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid priority ID"})
}
var priority models.TaskPriority
if err := h.db.First(&priority, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Priority not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Priority not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch priority"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch priority"})
}
var req CreateUpdatePriorityRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
priority.Name = req.Name
@@ -407,14 +372,13 @@ func (h *AdminLookupHandler) UpdatePriority(c *gin.Context) {
}
if err := h.db.Save(&priority).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update priority"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update priority"})
}
// Refresh cache after updating
h.refreshPrioritiesCache(c.Request.Context())
h.refreshPrioritiesCache(c.Request().Context())
c.JSON(http.StatusOK, TaskPriorityResponse{
return c.JSON(http.StatusOK, TaskPriorityResponse{
ID: priority.ID,
Name: priority.Name,
Level: priority.Level,
@@ -423,29 +387,26 @@ func (h *AdminLookupHandler) UpdatePriority(c *gin.Context) {
})
}
func (h *AdminLookupHandler) DeletePriority(c *gin.Context) {
func (h *AdminLookupHandler) DeletePriority(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid priority ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid priority ID"})
}
var count int64
h.db.Model(&models.Task{}).Where("priority_id = ?", id).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete priority that is in use by tasks"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Cannot delete priority that is in use by tasks"})
}
if err := h.db.Delete(&models.TaskPriority{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete priority"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete priority"})
}
// Refresh cache after deleting
h.refreshPrioritiesCache(c.Request.Context())
h.refreshPrioritiesCache(c.Request().Context())
c.JSON(http.StatusOK, gin.H{"message": "Priority deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Priority deleted successfully"})
}
// ========== Task Frequencies ==========
@@ -463,11 +424,10 @@ type CreateUpdateFrequencyRequest struct {
DisplayOrder *int `json:"display_order"`
}
func (h *AdminLookupHandler) ListFrequencies(c *gin.Context) {
func (h *AdminLookupHandler) ListFrequencies(c echo.Context) error {
var frequencies []models.TaskFrequency
if err := h.db.Order("display_order ASC, name ASC").Find(&frequencies).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch frequencies"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch frequencies"})
}
responses := make([]TaskFrequencyResponse, len(frequencies))
@@ -480,14 +440,13 @@ func (h *AdminLookupHandler) ListFrequencies(c *gin.Context) {
}
}
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
return c.JSON(http.StatusOK, map[string]interface{}{"data": responses, "total": len(responses)})
}
func (h *AdminLookupHandler) CreateFrequency(c *gin.Context) {
func (h *AdminLookupHandler) CreateFrequency(c echo.Context) error {
var req CreateUpdateFrequencyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
frequency := models.TaskFrequency{
@@ -499,14 +458,13 @@ func (h *AdminLookupHandler) CreateFrequency(c *gin.Context) {
}
if err := h.db.Create(&frequency).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create frequency"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create frequency"})
}
// Refresh cache after creating
h.refreshFrequenciesCache(c.Request.Context())
h.refreshFrequenciesCache(c.Request().Context())
c.JSON(http.StatusCreated, TaskFrequencyResponse{
return c.JSON(http.StatusCreated, TaskFrequencyResponse{
ID: frequency.ID,
Name: frequency.Name,
Days: frequency.Days,
@@ -514,27 +472,23 @@ func (h *AdminLookupHandler) CreateFrequency(c *gin.Context) {
})
}
func (h *AdminLookupHandler) UpdateFrequency(c *gin.Context) {
func (h *AdminLookupHandler) UpdateFrequency(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid frequency ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid frequency ID"})
}
var frequency models.TaskFrequency
if err := h.db.First(&frequency, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Frequency not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Frequency not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch frequency"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch frequency"})
}
var req CreateUpdateFrequencyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
frequency.Name = req.Name
@@ -544,14 +498,13 @@ func (h *AdminLookupHandler) UpdateFrequency(c *gin.Context) {
}
if err := h.db.Save(&frequency).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update frequency"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update frequency"})
}
// Refresh cache after updating
h.refreshFrequenciesCache(c.Request.Context())
h.refreshFrequenciesCache(c.Request().Context())
c.JSON(http.StatusOK, TaskFrequencyResponse{
return c.JSON(http.StatusOK, TaskFrequencyResponse{
ID: frequency.ID,
Name: frequency.Name,
Days: frequency.Days,
@@ -559,29 +512,26 @@ func (h *AdminLookupHandler) UpdateFrequency(c *gin.Context) {
})
}
func (h *AdminLookupHandler) DeleteFrequency(c *gin.Context) {
func (h *AdminLookupHandler) DeleteFrequency(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid frequency ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid frequency ID"})
}
var count int64
h.db.Model(&models.Task{}).Where("frequency_id = ?", id).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete frequency that is in use by tasks"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Cannot delete frequency that is in use by tasks"})
}
if err := h.db.Delete(&models.TaskFrequency{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete frequency"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete frequency"})
}
// Refresh cache after deleting
h.refreshFrequenciesCache(c.Request.Context())
h.refreshFrequenciesCache(c.Request().Context())
c.JSON(http.StatusOK, gin.H{"message": "Frequency deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Frequency deleted successfully"})
}
// ========== Residence Types ==========
@@ -595,11 +545,10 @@ type CreateUpdateResidenceTypeRequest struct {
Name string `json:"name" binding:"required,max=20"`
}
func (h *AdminLookupHandler) ListResidenceTypes(c *gin.Context) {
func (h *AdminLookupHandler) ListResidenceTypes(c echo.Context) error {
var types []models.ResidenceType
if err := h.db.Order("name ASC").Find(&types).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence types"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch residence types"})
}
responses := make([]ResidenceTypeResponse, len(types))
@@ -610,92 +559,82 @@ func (h *AdminLookupHandler) ListResidenceTypes(c *gin.Context) {
}
}
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
return c.JSON(http.StatusOK, map[string]interface{}{"data": responses, "total": len(responses)})
}
func (h *AdminLookupHandler) CreateResidenceType(c *gin.Context) {
func (h *AdminLookupHandler) CreateResidenceType(c echo.Context) error {
var req CreateUpdateResidenceTypeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
residenceType := models.ResidenceType{Name: req.Name}
if err := h.db.Create(&residenceType).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create residence type"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create residence type"})
}
// Refresh cache after creating
h.refreshResidenceTypesCache(c.Request.Context())
h.refreshResidenceTypesCache(c.Request().Context())
c.JSON(http.StatusCreated, ResidenceTypeResponse{
return c.JSON(http.StatusCreated, ResidenceTypeResponse{
ID: residenceType.ID,
Name: residenceType.Name,
})
}
func (h *AdminLookupHandler) UpdateResidenceType(c *gin.Context) {
func (h *AdminLookupHandler) UpdateResidenceType(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence type ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid residence type ID"})
}
var residenceType models.ResidenceType
if err := h.db.First(&residenceType, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Residence type not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Residence type not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence type"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch residence type"})
}
var req CreateUpdateResidenceTypeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
residenceType.Name = req.Name
if err := h.db.Save(&residenceType).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update residence type"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update residence type"})
}
// Refresh cache after updating
h.refreshResidenceTypesCache(c.Request.Context())
h.refreshResidenceTypesCache(c.Request().Context())
c.JSON(http.StatusOK, ResidenceTypeResponse{
return c.JSON(http.StatusOK, ResidenceTypeResponse{
ID: residenceType.ID,
Name: residenceType.Name,
})
}
func (h *AdminLookupHandler) DeleteResidenceType(c *gin.Context) {
func (h *AdminLookupHandler) DeleteResidenceType(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence type ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid residence type ID"})
}
var count int64
h.db.Model(&models.Residence{}).Where("property_type_id = ?", id).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete residence type that is in use"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Cannot delete residence type that is in use"})
}
if err := h.db.Delete(&models.ResidenceType{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residence type"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete residence type"})
}
// Refresh cache after deleting
h.refreshResidenceTypesCache(c.Request.Context())
h.refreshResidenceTypesCache(c.Request().Context())
c.JSON(http.StatusOK, gin.H{"message": "Residence type deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Residence type deleted successfully"})
}
// ========== Contractor Specialties ==========
@@ -715,11 +654,10 @@ type CreateUpdateSpecialtyRequest struct {
DisplayOrder *int `json:"display_order"`
}
func (h *AdminLookupHandler) ListSpecialties(c *gin.Context) {
func (h *AdminLookupHandler) ListSpecialties(c echo.Context) error {
var specialties []models.ContractorSpecialty
if err := h.db.Order("display_order ASC, name ASC").Find(&specialties).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch specialties"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch specialties"})
}
responses := make([]ContractorSpecialtyResponse, len(specialties))
@@ -733,14 +671,13 @@ func (h *AdminLookupHandler) ListSpecialties(c *gin.Context) {
}
}
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
return c.JSON(http.StatusOK, map[string]interface{}{"data": responses, "total": len(responses)})
}
func (h *AdminLookupHandler) CreateSpecialty(c *gin.Context) {
func (h *AdminLookupHandler) CreateSpecialty(c echo.Context) error {
var req CreateUpdateSpecialtyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
specialty := models.ContractorSpecialty{
@@ -753,14 +690,13 @@ func (h *AdminLookupHandler) CreateSpecialty(c *gin.Context) {
}
if err := h.db.Create(&specialty).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create specialty"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create specialty"})
}
// Refresh cache after creating
h.refreshSpecialtiesCache(c.Request.Context())
h.refreshSpecialtiesCache(c.Request().Context())
c.JSON(http.StatusCreated, ContractorSpecialtyResponse{
return c.JSON(http.StatusCreated, ContractorSpecialtyResponse{
ID: specialty.ID,
Name: specialty.Name,
Description: specialty.Description,
@@ -769,27 +705,23 @@ func (h *AdminLookupHandler) CreateSpecialty(c *gin.Context) {
})
}
func (h *AdminLookupHandler) UpdateSpecialty(c *gin.Context) {
func (h *AdminLookupHandler) UpdateSpecialty(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid specialty ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid specialty ID"})
}
var specialty models.ContractorSpecialty
if err := h.db.First(&specialty, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Specialty not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Specialty not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch specialty"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch specialty"})
}
var req CreateUpdateSpecialtyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
specialty.Name = req.Name
@@ -800,14 +732,13 @@ func (h *AdminLookupHandler) UpdateSpecialty(c *gin.Context) {
}
if err := h.db.Save(&specialty).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update specialty"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update specialty"})
}
// Refresh cache after updating
h.refreshSpecialtiesCache(c.Request.Context())
h.refreshSpecialtiesCache(c.Request().Context())
c.JSON(http.StatusOK, ContractorSpecialtyResponse{
return c.JSON(http.StatusOK, ContractorSpecialtyResponse{
ID: specialty.ID,
Name: specialty.Name,
Description: specialty.Description,
@@ -816,30 +747,27 @@ func (h *AdminLookupHandler) UpdateSpecialty(c *gin.Context) {
})
}
func (h *AdminLookupHandler) DeleteSpecialty(c *gin.Context) {
func (h *AdminLookupHandler) DeleteSpecialty(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid specialty ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid specialty ID"})
}
// Check if in use via many-to-many relationship
var count int64
h.db.Table("task_contractor_specialties").Where("contractorspecialty_id = ?", id).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete specialty that is in use by contractors"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Cannot delete specialty that is in use by contractors"})
}
if err := h.db.Delete(&models.ContractorSpecialty{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete specialty"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete specialty"})
}
// Refresh cache after deleting
h.refreshSpecialtiesCache(c.Request.Context())
h.refreshSpecialtiesCache(c.Request().Context())
c.JSON(http.StatusOK, gin.H{"message": "Specialty deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Specialty deleted successfully"})
}
// Ensure dto import is used

View File

@@ -6,7 +6,7 @@ import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -32,11 +32,10 @@ func NewAdminNotificationHandler(db *gorm.DB, emailService *services.EmailServic
}
// List handles GET /api/admin/notifications
func (h *AdminNotificationHandler) List(c *gin.Context) {
func (h *AdminNotificationHandler) List(c echo.Context) error {
var filters dto.NotificationFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var notifications []models.Notification
@@ -79,8 +78,7 @@ func (h *AdminNotificationHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&notifications).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notifications"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notifications"})
}
// Build response
@@ -89,15 +87,14 @@ func (h *AdminNotificationHandler) List(c *gin.Context) {
responses[i] = h.toNotificationResponse(&notif)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/notifications/:id
func (h *AdminNotificationHandler) Get(c *gin.Context) {
func (h *AdminNotificationHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid notification ID"})
}
var notification models.Notification
@@ -105,65 +102,55 @@ func (h *AdminNotificationHandler) Get(c *gin.Context) {
Preload("User").
First(&notification, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Notification not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification"})
}
c.JSON(http.StatusOK, h.toNotificationDetailResponse(&notification))
return c.JSON(http.StatusOK, h.toNotificationDetailResponse(&notification))
}
// Delete handles DELETE /api/admin/notifications/:id
func (h *AdminNotificationHandler) Delete(c *gin.Context) {
func (h *AdminNotificationHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid notification ID"})
}
var notification models.Notification
if err := h.db.First(&notification, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Notification not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification"})
}
// Hard delete notifications
if err := h.db.Delete(&notification).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete notification"})
}
c.JSON(http.StatusOK, gin.H{"message": "Notification deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Notification deleted successfully"})
}
// Update handles PUT /api/admin/notifications/:id
func (h *AdminNotificationHandler) Update(c *gin.Context) {
func (h *AdminNotificationHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid notification ID"})
}
var notification models.Notification
if err := h.db.First(&notification, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Notification not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification"})
}
var req dto.UpdateNotificationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
now := time.Now().UTC()
@@ -183,16 +170,15 @@ func (h *AdminNotificationHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&notification).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update notification"})
}
h.db.Preload("User").First(&notification, id)
c.JSON(http.StatusOK, h.toNotificationResponse(&notification))
return c.JSON(http.StatusOK, h.toNotificationResponse(&notification))
}
// GetStats handles GET /api/admin/notifications/stats
func (h *AdminNotificationHandler) GetStats(c *gin.Context) {
func (h *AdminNotificationHandler) GetStats(c echo.Context) error {
var total, sent, read, pending int64
h.db.Model(&models.Notification{}).Count(&total)
@@ -200,7 +186,7 @@ func (h *AdminNotificationHandler) GetStats(c *gin.Context) {
h.db.Model(&models.Notification{}).Where("read = ?", true).Count(&read)
h.db.Model(&models.Notification{}).Where("sent = ?", false).Count(&pending)
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"total": total,
"sent": sent,
"read": read,
@@ -245,22 +231,19 @@ func (h *AdminNotificationHandler) toNotificationDetailResponse(notif *models.No
}
// SendTestNotification handles POST /api/admin/notifications/send-test
func (h *AdminNotificationHandler) SendTestNotification(c *gin.Context) {
func (h *AdminNotificationHandler) SendTestNotification(c echo.Context) error {
var req dto.SendTestNotificationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify user exists
var user models.User
if err := h.db.First(&user, req.UserID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user"})
}
// Get user's device tokens
@@ -271,8 +254,7 @@ func (h *AdminNotificationHandler) SendTestNotification(c *gin.Context) {
h.db.Where("user_id = ? AND active = ?", req.UserID, true).Find(&androidDevices)
if len(iosDevices) == 0 && len(androidDevices) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "User has no registered devices"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "User has no registered devices"})
}
// Create notification record
@@ -287,8 +269,7 @@ func (h *AdminNotificationHandler) SendTestNotification(c *gin.Context) {
}
if err := h.db.Create(&notification).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create notification record"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create notification record"})
}
// Collect tokens
@@ -316,15 +297,13 @@ func (h *AdminNotificationHandler) SendTestNotification(c *gin.Context) {
h.db.Model(&notification).Updates(map[string]interface{}{
"error": err.Error(),
})
c.JSON(http.StatusInternalServerError, gin.H{
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": "Failed to send push notification",
"details": err.Error(),
})
return
}
} else {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Push notification service not configured"})
return
return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{"error": "Push notification service not configured"})
}
// Mark as sent
@@ -333,10 +312,10 @@ func (h *AdminNotificationHandler) SendTestNotification(c *gin.Context) {
"sent_at": now,
})
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Test notification sent successfully",
"notification_id": notification.ID,
"devices": gin.H{
"devices": map[string]interface{}{
"ios": len(iosTokens),
"android": len(androidTokens),
},
@@ -344,33 +323,28 @@ func (h *AdminNotificationHandler) SendTestNotification(c *gin.Context) {
}
// SendTestEmail handles POST /api/admin/emails/send-test
func (h *AdminNotificationHandler) SendTestEmail(c *gin.Context) {
func (h *AdminNotificationHandler) SendTestEmail(c echo.Context) error {
var req dto.SendTestEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify user exists
var user models.User
if err := h.db.First(&user, req.UserID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user"})
}
if user.Email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "User has no email address"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "User has no email address"})
}
// Send email
if h.emailService == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Email service not configured"})
return
return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{"error": "Email service not configured"})
}
// Create HTML body with basic styling
@@ -390,61 +364,54 @@ func (h *AdminNotificationHandler) SendTestEmail(c *gin.Context) {
err := h.emailService.SendEmail(user.Email, req.Subject, htmlBody, req.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": "Failed to send email",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Test email sent successfully",
"to": user.Email,
})
}
// SendPostVerificationEmail handles POST /api/admin/emails/send-post-verification
func (h *AdminNotificationHandler) SendPostVerificationEmail(c *gin.Context) {
func (h *AdminNotificationHandler) SendPostVerificationEmail(c echo.Context) error {
var req struct {
UserID uint `json:"user_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id is required"})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "user_id is required"})
}
// Verify user exists
var user models.User
if err := h.db.First(&user, req.UserID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user"})
}
if user.Email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "User has no email address"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "User has no email address"})
}
// Send email
if h.emailService == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Email service not configured"})
return
return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{"error": "Email service not configured"})
}
err := h.emailService.SendPostVerificationEmail(user.Email, user.FirstName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": "Failed to send email",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Post-verification email sent successfully",
"to": user.Email,
})

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -49,11 +49,10 @@ type NotificationPrefResponse struct {
}
// List handles GET /api/admin/notification-prefs
func (h *AdminNotificationPrefsHandler) List(c *gin.Context) {
func (h *AdminNotificationPrefsHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var prefs []models.NotificationPreference
@@ -85,8 +84,7 @@ func (h *AdminNotificationPrefsHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&prefs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preferences"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preferences"})
}
// Get user info for each preference
@@ -130,31 +128,28 @@ func (h *AdminNotificationPrefsHandler) List(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
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 *gin.Context) {
func (h *AdminNotificationPrefsHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Notification preference not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preference"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preference"})
}
var user models.User
h.db.First(&user, pref.UserID)
c.JSON(http.StatusOK, NotificationPrefResponse{
return c.JSON(http.StatusOK, NotificationPrefResponse{
ID: pref.ID,
UserID: pref.UserID,
Username: user.Username,
@@ -197,27 +192,23 @@ type UpdateNotificationPrefRequest struct {
}
// Update handles PUT /api/admin/notification-prefs/:id
func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
func (h *AdminNotificationPrefsHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Notification preference not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preference"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preference"})
}
var req UpdateNotificationPrefRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Apply updates
@@ -261,14 +252,13 @@ func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&pref).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification preference"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update notification preference"})
}
var user models.User
h.db.First(&user, pref.UserID)
c.JSON(http.StatusOK, NotificationPrefResponse{
return c.JSON(http.StatusOK, NotificationPrefResponse{
ID: pref.ID,
UserID: pref.UserID,
Username: user.Username,
@@ -291,48 +281,42 @@ func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
}
// Delete handles DELETE /api/admin/notification-prefs/:id
func (h *AdminNotificationPrefsHandler) Delete(c *gin.Context) {
func (h *AdminNotificationPrefsHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.NotificationPreference{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preference"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete notification preference"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Notification preference not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Notification preference deleted"})
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 *gin.Context) {
func (h *AdminNotificationPrefsHandler) GetByUser(c echo.Context) error {
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Notification preference not found for this user"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Notification preference not found for this user"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preference"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch notification preference"})
}
var user models.User
h.db.First(&user, pref.UserID)
c.JSON(http.StatusOK, NotificationPrefResponse{
return c.JSON(http.StatusOK, NotificationPrefResponse{
ID: pref.ID,
UserID: pref.UserID,
Username: user.Username,

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/models"
@@ -62,12 +62,21 @@ type OnboardingStatsResponse struct {
// List returns paginated list of onboarding emails
// GET /api/admin/onboarding-emails
func (h *AdminOnboardingHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
emailType := c.Query("email_type")
userID, _ := strconv.Atoi(c.Query("user_id"))
opened := c.Query("opened")
func (h *AdminOnboardingHandler) List(c echo.Context) error {
pageStr := c.QueryParam("page")
if pageStr == "" {
pageStr = "1"
}
page, _ := strconv.Atoi(pageStr)
pageSizeStr := c.QueryParam("page_size")
if pageSizeStr == "" {
pageSizeStr = "20"
}
pageSize, _ := strconv.Atoi(pageSizeStr)
emailType := c.QueryParam("email_type")
userID, _ := strconv.Atoi(c.QueryParam("user_id"))
opened := c.QueryParam("opened")
if page < 1 {
page = 1
@@ -96,15 +105,13 @@ func (h *AdminOnboardingHandler) List(c *gin.Context) {
// Count total
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count emails"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to count emails"})
}
// Get paginated results
var emails []models.OnboardingEmail
if err := query.Order("sent_at DESC").Offset(offset).Limit(pageSize).Find(&emails).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch emails"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch emails"})
}
// Transform to response
@@ -118,7 +125,7 @@ func (h *AdminOnboardingHandler) List(c *gin.Context) {
totalPages++
}
c.JSON(http.StatusOK, OnboardingEmailListResponse{
return c.JSON(http.StatusOK, OnboardingEmailListResponse{
Data: data,
Total: total,
Page: page,
@@ -129,7 +136,7 @@ func (h *AdminOnboardingHandler) List(c *gin.Context) {
// GetStats returns onboarding email statistics
// GET /api/admin/onboarding-emails/stats
func (h *AdminOnboardingHandler) GetStats(c *gin.Context) {
func (h *AdminOnboardingHandler) GetStats(c echo.Context) error {
var stats OnboardingStatsResponse
// No residence email stats
@@ -162,22 +169,20 @@ func (h *AdminOnboardingHandler) GetStats(c *gin.Context) {
stats.OverallRate = float64(stats.TotalOpened) / float64(stats.TotalSent) * 100
}
c.JSON(http.StatusOK, stats)
return c.JSON(http.StatusOK, stats)
}
// GetByUser returns onboarding emails for a specific user
// GET /api/admin/onboarding-emails/user/:user_id
func (h *AdminOnboardingHandler) GetByUser(c *gin.Context) {
func (h *AdminOnboardingHandler) GetByUser(c echo.Context) error {
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var emails []models.OnboardingEmail
if err := h.db.Where("user_id = ?", userID).Order("sent_at DESC").Find(&emails).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch emails"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch emails"})
}
// Transform to response
@@ -186,7 +191,7 @@ func (h *AdminOnboardingHandler) GetByUser(c *gin.Context) {
data[i] = transformOnboardingEmail(email)
}
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"data": data,
"user_id": userID,
"count": len(data),
@@ -195,71 +200,62 @@ func (h *AdminOnboardingHandler) GetByUser(c *gin.Context) {
// Get returns a single onboarding email by ID
// GET /api/admin/onboarding-emails/:id
func (h *AdminOnboardingHandler) Get(c *gin.Context) {
func (h *AdminOnboardingHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var email models.OnboardingEmail
if err := h.db.Preload("User").First(&email, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Email not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Email not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch email"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch email"})
}
c.JSON(http.StatusOK, transformOnboardingEmail(email))
return c.JSON(http.StatusOK, transformOnboardingEmail(email))
}
// Delete removes an onboarding email record
// DELETE /api/admin/onboarding-emails/:id
func (h *AdminOnboardingHandler) Delete(c *gin.Context) {
func (h *AdminOnboardingHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.OnboardingEmail{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete email"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete email"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Email not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Email not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Email record deleted"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Email record deleted"})
}
// BulkDelete removes multiple onboarding email records
// DELETE /api/admin/onboarding-emails/bulk
func (h *AdminOnboardingHandler) BulkDelete(c *gin.Context) {
func (h *AdminOnboardingHandler) BulkDelete(c echo.Context) error {
var req struct {
IDs []uint `json:"ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid request"})
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No IDs provided"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "No IDs provided"})
}
result := h.db.Delete(&models.OnboardingEmail{}, req.IDs)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete emails"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete emails"})
}
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Emails deleted",
"count": result.RowsAffected,
})
@@ -273,16 +269,14 @@ type SendOnboardingEmailRequest struct {
// Send sends an onboarding email to a specific user
// POST /api/admin/onboarding-emails/send
func (h *AdminOnboardingHandler) Send(c *gin.Context) {
func (h *AdminOnboardingHandler) Send(c echo.Context) error {
if h.onboardingService == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Onboarding email service not configured"})
return
return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{"error": "Onboarding email service not configured"})
}
var req SendOnboardingEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: user_id and email_type are required"})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid request: user_id and email_type are required"})
}
// Validate email type
@@ -293,28 +287,24 @@ func (h *AdminOnboardingHandler) Send(c *gin.Context) {
case "no_tasks":
emailType = models.OnboardingEmailNoTasks
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email_type. Must be 'no_residence' or 'no_tasks'"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid email_type. Must be 'no_residence' or 'no_tasks'"})
}
// Get user email for response
var user models.User
if err := h.db.First(&user, req.UserID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user"})
}
// Send the email
if err := h.onboardingService.SendOnboardingEmailToUser(req.UserID, emailType); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
}
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Onboarding email sent successfully",
"user_id": req.UserID,
"email": user.Email,

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -36,11 +36,10 @@ type PasswordResetCodeResponse struct {
}
// List handles GET /api/admin/password-reset-codes
func (h *AdminPasswordResetCodeHandler) List(c *gin.Context) {
func (h *AdminPasswordResetCodeHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var codes []models.PasswordResetCode
@@ -72,8 +71,7 @@ func (h *AdminPasswordResetCodeHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&codes).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch password reset codes"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch password reset codes"})
}
// Build response
@@ -93,25 +91,22 @@ func (h *AdminPasswordResetCodeHandler) List(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/password-reset-codes/:id
func (h *AdminPasswordResetCodeHandler) Get(c *gin.Context) {
func (h *AdminPasswordResetCodeHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var code models.PasswordResetCode
if err := h.db.Preload("User").First(&code, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Password reset code not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Password reset code not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch password reset code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch password reset code"})
}
response := PasswordResetCodeResponse{
@@ -127,44 +122,39 @@ func (h *AdminPasswordResetCodeHandler) Get(c *gin.Context) {
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Delete handles DELETE /api/admin/password-reset-codes/:id
func (h *AdminPasswordResetCodeHandler) Delete(c *gin.Context) {
func (h *AdminPasswordResetCodeHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.PasswordResetCode{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete password reset code"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Password reset code not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Password reset code not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Password reset code deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Password reset code deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/password-reset-codes/bulk
func (h *AdminPasswordResetCodeHandler) BulkDelete(c *gin.Context) {
func (h *AdminPasswordResetCodeHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
result := h.db.Where("id IN ?", req.IDs).Delete(&models.PasswordResetCode{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete password reset codes"})
}
c.JSON(http.StatusOK, gin.H{"message": "Password reset codes deleted successfully", "count": result.RowsAffected})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Password reset codes deleted successfully", "count": result.RowsAffected})
}

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -38,11 +38,10 @@ type PromotionResponse struct {
}
// List handles GET /api/admin/promotions
func (h *AdminPromotionHandler) List(c *gin.Context) {
func (h *AdminPromotionHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var promotions []models.Promotion
@@ -65,8 +64,7 @@ func (h *AdminPromotionHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&promotions).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch promotions"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch promotions"})
}
responses := make([]PromotionResponse, len(promotions))
@@ -86,25 +84,22 @@ func (h *AdminPromotionHandler) List(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/promotions/:id
func (h *AdminPromotionHandler) Get(c *gin.Context) {
func (h *AdminPromotionHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var promotion models.Promotion
if err := h.db.First(&promotion, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Promotion not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Promotion not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch promotion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch promotion"})
}
response := PromotionResponse{
@@ -121,11 +116,11 @@ func (h *AdminPromotionHandler) Get(c *gin.Context) {
UpdatedAt: promotion.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Create handles POST /api/admin/promotions
func (h *AdminPromotionHandler) Create(c *gin.Context) {
func (h *AdminPromotionHandler) Create(c echo.Context) error {
var req struct {
PromotionID string `json:"promotion_id" binding:"required"`
Title string `json:"title" binding:"required"`
@@ -137,17 +132,15 @@ func (h *AdminPromotionHandler) Create(c *gin.Context) {
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
startDate, err := time.Parse("2006-01-02T15:04:05Z", req.StartDate)
if err != nil {
startDate, err = time.Parse("2006-01-02", req.StartDate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start_date format"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid start_date format"})
}
}
@@ -155,8 +148,7 @@ func (h *AdminPromotionHandler) Create(c *gin.Context) {
if err != nil {
endDate, err = time.Parse("2006-01-02", req.EndDate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end_date format"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid end_date format"})
}
}
@@ -181,11 +173,10 @@ func (h *AdminPromotionHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&promotion).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create promotion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create promotion"})
}
c.JSON(http.StatusCreated, PromotionResponse{
return c.JSON(http.StatusCreated, PromotionResponse{
ID: promotion.ID,
PromotionID: promotion.PromotionID,
Title: promotion.Title,
@@ -201,21 +192,18 @@ func (h *AdminPromotionHandler) Create(c *gin.Context) {
}
// Update handles PUT /api/admin/promotions/:id
func (h *AdminPromotionHandler) Update(c *gin.Context) {
func (h *AdminPromotionHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var promotion models.Promotion
if err := h.db.First(&promotion, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Promotion not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Promotion not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch promotion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch promotion"})
}
var req struct {
@@ -229,9 +217,8 @@ func (h *AdminPromotionHandler) Update(c *gin.Context) {
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.PromotionID != nil {
@@ -251,8 +238,7 @@ func (h *AdminPromotionHandler) Update(c *gin.Context) {
if err != nil {
startDate, err = time.Parse("2006-01-02", *req.StartDate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start_date format"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid start_date format"})
}
}
promotion.StartDate = startDate
@@ -262,8 +248,7 @@ func (h *AdminPromotionHandler) Update(c *gin.Context) {
if err != nil {
endDate, err = time.Parse("2006-01-02", *req.EndDate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end_date format"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid end_date format"})
}
}
promotion.EndDate = endDate
@@ -280,11 +265,10 @@ func (h *AdminPromotionHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&promotion).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update promotion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update promotion"})
}
c.JSON(http.StatusOK, PromotionResponse{
return c.JSON(http.StatusOK, PromotionResponse{
ID: promotion.ID,
PromotionID: promotion.PromotionID,
Title: promotion.Title,
@@ -300,23 +284,20 @@ func (h *AdminPromotionHandler) Update(c *gin.Context) {
}
// Delete handles DELETE /api/admin/promotions/:id
func (h *AdminPromotionHandler) Delete(c *gin.Context) {
func (h *AdminPromotionHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.Promotion{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete promotion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete promotion"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Promotion not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Promotion not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Promotion deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Promotion deleted successfully"})
}

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/shopspring/decimal"
"gorm.io/gorm"
@@ -24,11 +24,10 @@ func NewAdminResidenceHandler(db *gorm.DB) *AdminResidenceHandler {
}
// List handles GET /api/admin/residences
func (h *AdminResidenceHandler) List(c *gin.Context) {
func (h *AdminResidenceHandler) List(c echo.Context) error {
var filters dto.ResidenceFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var residences []models.Residence
@@ -70,8 +69,7 @@ func (h *AdminResidenceHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&residences).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residences"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch residences"})
}
// Build response
@@ -80,25 +78,22 @@ func (h *AdminResidenceHandler) List(c *gin.Context) {
responses[i] = h.toResidenceResponse(&res)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/residences/:id
func (h *AdminResidenceHandler) Get(c *gin.Context) {
func (h *AdminResidenceHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid residence ID"})
}
var residence models.Residence
if err := h.db.Preload("Owner").Preload("PropertyType").Preload("Users").First(&residence, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Residence not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch residence"})
}
response := dto.ResidenceDetailResponse{
@@ -128,39 +123,34 @@ func (h *AdminResidenceHandler) Get(c *gin.Context) {
response.TaskCount = int(taskCount)
response.DocumentCount = int(documentCount)
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Update handles PUT /api/admin/residences/:id
func (h *AdminResidenceHandler) Update(c *gin.Context) {
func (h *AdminResidenceHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid residence ID"})
}
var residence models.Residence
if err := h.db.First(&residence, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Residence not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch residence"})
}
var req dto.UpdateResidenceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.OwnerID != nil {
// Verify owner exists
var owner models.User
if err := h.db.First(&owner, *req.OwnerID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Owner not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Owner not found"})
}
residence.OwnerID = *req.OwnerID
}
@@ -225,27 +215,24 @@ func (h *AdminResidenceHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&residence).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update residence"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update residence"})
}
h.db.Preload("Owner").Preload("PropertyType").First(&residence, id)
c.JSON(http.StatusOK, h.toResidenceResponse(&residence))
return c.JSON(http.StatusOK, h.toResidenceResponse(&residence))
}
// Create handles POST /api/admin/residences
func (h *AdminResidenceHandler) Create(c *gin.Context) {
func (h *AdminResidenceHandler) Create(c echo.Context) error {
var req dto.CreateResidenceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify owner exists
var owner models.User
if err := h.db.First(&owner, req.OwnerID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Owner not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Owner not found"})
}
residence := models.Residence{
@@ -278,57 +265,50 @@ func (h *AdminResidenceHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&residence).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create residence"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create residence"})
}
h.db.Preload("Owner").Preload("PropertyType").First(&residence, residence.ID)
c.JSON(http.StatusCreated, h.toResidenceResponse(&residence))
return c.JSON(http.StatusCreated, h.toResidenceResponse(&residence))
}
// Delete handles DELETE /api/admin/residences/:id
func (h *AdminResidenceHandler) Delete(c *gin.Context) {
func (h *AdminResidenceHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid residence ID"})
}
var residence models.Residence
if err := h.db.First(&residence, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Residence not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch residence"})
}
// Soft delete
residence.IsActive = false
if err := h.db.Save(&residence).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residence"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete residence"})
}
c.JSON(http.StatusOK, gin.H{"message": "Residence deactivated successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Residence deactivated successfully"})
}
// BulkDelete handles DELETE /api/admin/residences/bulk
func (h *AdminResidenceHandler) BulkDelete(c *gin.Context) {
func (h *AdminResidenceHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Soft delete - deactivate all
if err := h.db.Model(&models.Residence{}).Where("id IN ?", req.IDs).Update("is_active", false).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residences"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete residences"})
}
c.JSON(http.StatusOK, gin.H{"message": "Residences deactivated successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Residences deactivated successfully", "count": len(req.IDs)})
}
func (h *AdminResidenceHandler) toResidenceResponse(res *models.Residence) dto.ResidenceResponse {

View File

@@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
@@ -33,7 +33,7 @@ type SettingsResponse struct {
}
// GetSettings handles GET /api/admin/settings
func (h *AdminSettingsHandler) GetSettings(c *gin.Context) {
func (h *AdminSettingsHandler) GetSettings(c echo.Context) error {
var settings models.SubscriptionSettings
if err := h.db.First(&settings, 1).Error; err != nil {
if err == gorm.ErrRecordNotFound {
@@ -41,12 +41,11 @@ func (h *AdminSettingsHandler) GetSettings(c *gin.Context) {
settings = models.SubscriptionSettings{ID: 1, EnableLimitations: false, EnableMonitoring: true}
h.db.Create(&settings)
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch settings"})
}
}
c.JSON(http.StatusOK, SettingsResponse{
return c.JSON(http.StatusOK, SettingsResponse{
EnableLimitations: settings.EnableLimitations,
EnableMonitoring: settings.EnableMonitoring,
})
@@ -59,11 +58,10 @@ type UpdateSettingsRequest struct {
}
// UpdateSettings handles PUT /api/admin/settings
func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
func (h *AdminSettingsHandler) UpdateSettings(c echo.Context) error {
var req UpdateSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var settings models.SubscriptionSettings
@@ -71,8 +69,7 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
if err == gorm.ErrRecordNotFound {
settings = models.SubscriptionSettings{ID: 1, EnableMonitoring: true}
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch settings"})
}
}
@@ -85,11 +82,10 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
}
if err := h.db.Save(&settings).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update settings"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update settings"})
}
c.JSON(http.StatusOK, SettingsResponse{
return c.JSON(http.StatusOK, SettingsResponse{
EnableLimitations: settings.EnableLimitations,
EnableMonitoring: settings.EnableMonitoring,
})
@@ -97,31 +93,29 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
// SeedLookups handles POST /api/admin/settings/seed-lookups
// Seeds both lookup tables AND task templates, then caches all lookups in Redis
func (h *AdminSettingsHandler) SeedLookups(c *gin.Context) {
func (h *AdminSettingsHandler) SeedLookups(c echo.Context) error {
// First seed lookup tables
if err := h.runSeedFile("001_lookups.sql"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed lookups: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to seed lookups: " + err.Error()})
}
// Then seed task templates
if err := h.runSeedFile("003_task_templates.sql"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed task templates: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to seed task templates: " + err.Error()})
}
// Cache all lookups in Redis
cached, cacheErr := h.cacheAllLookups(c.Request.Context())
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{
response := map[string]interface{}{
"message": "Lookup data and task templates seeded successfully",
"redis_cached": cached,
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// cacheAllLookups fetches all lookup data from the database and caches it in Redis
@@ -326,23 +320,21 @@ func parseTags(tags string) []string {
}
// SeedTestData handles POST /api/admin/settings/seed-test-data
func (h *AdminSettingsHandler) SeedTestData(c *gin.Context) {
func (h *AdminSettingsHandler) SeedTestData(c echo.Context) error {
if err := h.runSeedFile("002_test_data.sql"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed test data: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to seed test data: " + err.Error()})
}
c.JSON(http.StatusOK, gin.H{"message": "Test data seeded successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Test data seeded successfully"})
}
// SeedTaskTemplates handles POST /api/admin/settings/seed-task-templates
func (h *AdminSettingsHandler) SeedTaskTemplates(c *gin.Context) {
func (h *AdminSettingsHandler) SeedTaskTemplates(c echo.Context) error {
if err := h.runSeedFile("003_task_templates.sql"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed task templates: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to seed task templates: " + err.Error()})
}
c.JSON(http.StatusOK, gin.H{"message": "Task templates seeded successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Task templates seeded successfully"})
}
// runSeedFile executes a seed SQL file
@@ -473,14 +465,13 @@ type ClearStuckJobsResponse struct {
// ClearStuckJobs handles POST /api/admin/settings/clear-stuck-jobs
// This clears stuck/failed asynq worker jobs from Redis
func (h *AdminSettingsHandler) ClearStuckJobs(c *gin.Context) {
func (h *AdminSettingsHandler) ClearStuckJobs(c echo.Context) error {
cache := services.GetCache()
if cache == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Redis cache not available"})
return
return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{"error": "Redis cache not available"})
}
ctx := c.Request.Context()
ctx := c.Request().Context()
client := cache.Client()
var deletedKeys []string
@@ -526,7 +517,7 @@ func (h *AdminSettingsHandler) ClearStuckJobs(c *gin.Context) {
log.Info().Int("count", len(deletedKeys)).Strs("keys", deletedKeys).Msg("Cleared stuck Redis jobs")
c.JSON(http.StatusOK, ClearStuckJobsResponse{
return c.JSON(http.StatusOK, ClearStuckJobsResponse{
Message: "Stuck jobs cleared successfully",
KeysDeleted: len(deletedKeys),
DeletedKeys: deletedKeys,
@@ -535,12 +526,11 @@ func (h *AdminSettingsHandler) ClearStuckJobs(c *gin.Context) {
// ClearAllData handles POST /api/admin/settings/clear-all-data
// This clears all data except super admin accounts and lookup tables
func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
func (h *AdminSettingsHandler) ClearAllData(c echo.Context) error {
// Start a transaction
tx := h.db.Begin()
if tx.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start transaction"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to start transaction"})
}
defer func() {
@@ -555,8 +545,7 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
Where("is_superuser = ?", true).
Pluck("id", &preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get superuser IDs"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to get superuser IDs"})
}
// Count users that will be deleted
@@ -565,8 +554,7 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
Where("is_superuser = ?", false).
Count(&usersToDelete).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count users"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to count users"})
}
// Delete in order to respect foreign key constraints
@@ -575,110 +563,94 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
// 1. Delete task completion images
if err := tx.Exec("DELETE FROM task_taskcompletionimage").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task completion images: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete task completion images: " + err.Error()})
}
// 2. Delete task completions
if err := tx.Exec("DELETE FROM task_taskcompletion").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task completions: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete task completions: " + err.Error()})
}
// 3. Delete notifications (must be before tasks since notifications have task_id FK)
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM notifications_notification WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notifications: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete notifications: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM notifications_notification").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notifications: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete notifications: " + err.Error()})
}
}
// 4. Delete document images
if err := tx.Exec("DELETE FROM task_documentimage").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete document images: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete document images: " + err.Error()})
}
// 5. Delete documents
if err := tx.Exec("DELETE FROM task_document").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete documents: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete documents: " + err.Error()})
}
// 6. Delete tasks (must be before contractors since tasks reference contractors)
if err := tx.Exec("DELETE FROM task_task").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete tasks: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete tasks: " + err.Error()})
}
// 7. Delete contractor specialties (many-to-many)
if err := tx.Exec("DELETE FROM task_contractor_specialties").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractor specialties: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete contractor specialties: " + err.Error()})
}
// 8. Delete contractors
if err := tx.Exec("DELETE FROM task_contractor").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractors: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete contractors: " + err.Error()})
}
// 9. Delete residence_users (many-to-many for shared residences)
if err := tx.Exec("DELETE FROM residence_residence_users").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residence users: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete residence users: " + err.Error()})
}
// 10. Delete residence share codes (must be before residences since share codes have residence_id FK)
if err := tx.Exec("DELETE FROM residence_residencesharecode").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residence share codes: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete residence share codes: " + err.Error()})
}
// 11. Delete residences
if err := tx.Exec("DELETE FROM residence_residence").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residences: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete residences: " + err.Error()})
}
// 12. Delete push devices for non-superusers (both APNS and GCM)
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM push_notifications_apnsdevice WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete APNS devices: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete APNS devices: " + err.Error()})
}
if err := tx.Exec("DELETE FROM push_notifications_gcmdevice WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete GCM devices: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete GCM devices: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM push_notifications_apnsdevice").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete APNS devices: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete APNS devices: " + err.Error()})
}
if err := tx.Exec("DELETE FROM push_notifications_gcmdevice").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete GCM devices: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete GCM devices: " + err.Error()})
}
}
@@ -686,14 +658,12 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM notifications_notificationpreference WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preferences: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete notification preferences: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM notifications_notificationpreference").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preferences: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete notification preferences: " + err.Error()})
}
}
@@ -701,14 +671,12 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM subscription_usersubscription WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete subscriptions: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete subscriptions: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM subscription_usersubscription").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete subscriptions: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete subscriptions: " + err.Error()})
}
}
@@ -716,14 +684,12 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM user_passwordresetcode WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete password reset codes: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM user_passwordresetcode").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete password reset codes: " + err.Error()})
}
}
@@ -731,14 +697,12 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM user_confirmationcode WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete confirmation codes: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM user_confirmationcode").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete confirmation codes: " + err.Error()})
}
}
@@ -746,14 +710,12 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM user_authtoken WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete auth tokens: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete auth tokens: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM user_authtoken").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete auth tokens: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete auth tokens: " + err.Error()})
}
}
@@ -761,14 +723,12 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM user_applesocialauth WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete Apple social auth: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete Apple social auth: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM user_applesocialauth").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete Apple social auth: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete Apple social auth: " + err.Error()})
}
}
@@ -776,14 +736,12 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
if len(preservedUserIDs) > 0 {
if err := tx.Exec("DELETE FROM user_userprofile WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profiles: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete user profiles: " + err.Error()})
}
} else {
if err := tx.Exec("DELETE FROM user_userprofile").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profiles: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete user profiles: " + err.Error()})
}
}
@@ -791,17 +749,15 @@ func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
// Always filter by is_superuser to be safe, regardless of preservedUserIDs
if err := tx.Exec("DELETE FROM auth_user WHERE is_superuser = false").Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete users: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete users: " + err.Error()})
}
// Commit the transaction
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction: " + err.Error()})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to commit transaction: " + err.Error()})
}
c.JSON(http.StatusOK, ClearAllDataResponse{
return c.JSON(http.StatusOK, ClearAllDataResponse{
Message: "All data cleared successfully (superadmin accounts preserved)",
UsersDeleted: usersToDelete,
PreservedUsers: int64(len(preservedUserIDs)),

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -35,11 +35,10 @@ type ShareCodeResponse struct {
}
// List handles GET /api/admin/share-codes
func (h *AdminShareCodeHandler) List(c *gin.Context) {
func (h *AdminShareCodeHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var codes []models.ResidenceShareCode
@@ -74,8 +73,7 @@ func (h *AdminShareCodeHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&codes).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch share codes"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch share codes"})
}
// Build response
@@ -100,25 +98,22 @@ func (h *AdminShareCodeHandler) List(c *gin.Context) {
}
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/share-codes/:id
func (h *AdminShareCodeHandler) Get(c *gin.Context) {
func (h *AdminShareCodeHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var code models.ResidenceShareCode
if err := h.db.Preload("Residence").Preload("CreatedBy").First(&code, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Share code not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Share code not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch share code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch share code"})
}
var expiresAt *string
@@ -139,40 +134,35 @@ func (h *AdminShareCodeHandler) Get(c *gin.Context) {
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Update handles PUT /api/admin/share-codes/:id
func (h *AdminShareCodeHandler) Update(c *gin.Context) {
func (h *AdminShareCodeHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
var code models.ResidenceShareCode
if err := h.db.First(&code, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Share code not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Share code not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch share code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch share code"})
}
var req struct {
IsActive bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
code.IsActive = req.IsActive
if err := h.db.Save(&code).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update share code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update share code"})
}
// Reload with relations
@@ -196,44 +186,39 @@ func (h *AdminShareCodeHandler) Update(c *gin.Context) {
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Delete handles DELETE /api/admin/share-codes/:id
func (h *AdminShareCodeHandler) Delete(c *gin.Context) {
func (h *AdminShareCodeHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
}
result := h.db.Delete(&models.ResidenceShareCode{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete share code"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete share code"})
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Share code not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Share code not found"})
}
c.JSON(http.StatusOK, gin.H{"message": "Share code deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Share code deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/share-codes/bulk
func (h *AdminShareCodeHandler) BulkDelete(c *gin.Context) {
func (h *AdminShareCodeHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
result := h.db.Where("id IN ?", req.IDs).Delete(&models.ResidenceShareCode{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete share codes"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete share codes"})
}
c.JSON(http.StatusOK, gin.H{"message": "Share codes deleted successfully", "count": result.RowsAffected})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Share codes deleted successfully", "count": result.RowsAffected})
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -22,11 +22,10 @@ func NewAdminSubscriptionHandler(db *gorm.DB) *AdminSubscriptionHandler {
}
// List handles GET /api/admin/subscriptions
func (h *AdminSubscriptionHandler) List(c *gin.Context) {
func (h *AdminSubscriptionHandler) List(c echo.Context) error {
var filters dto.SubscriptionFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var subscriptions []models.UserSubscription
@@ -77,8 +76,7 @@ func (h *AdminSubscriptionHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&subscriptions).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscriptions"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch subscriptions"})
}
// Build response
@@ -87,15 +85,14 @@ func (h *AdminSubscriptionHandler) List(c *gin.Context) {
responses[i] = h.toSubscriptionResponse(&sub)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/subscriptions/:id
func (h *AdminSubscriptionHandler) Get(c *gin.Context) {
func (h *AdminSubscriptionHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subscription ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid subscription ID"})
}
var subscription models.UserSubscription
@@ -103,38 +100,32 @@ func (h *AdminSubscriptionHandler) Get(c *gin.Context) {
Preload("User").
First(&subscription, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Subscription not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Subscription not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscription"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch subscription"})
}
c.JSON(http.StatusOK, h.toSubscriptionDetailResponse(&subscription))
return c.JSON(http.StatusOK, h.toSubscriptionDetailResponse(&subscription))
}
// Update handles PUT /api/admin/subscriptions/:id
func (h *AdminSubscriptionHandler) Update(c *gin.Context) {
func (h *AdminSubscriptionHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subscription ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid subscription ID"})
}
var subscription models.UserSubscription
if err := h.db.First(&subscription, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Subscription not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Subscription not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscription"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch subscription"})
}
var req dto.UpdateSubscriptionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.Tier != nil {
@@ -148,20 +139,18 @@ func (h *AdminSubscriptionHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&subscription).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update subscription"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update subscription"})
}
h.db.Preload("User").First(&subscription, id)
c.JSON(http.StatusOK, h.toSubscriptionResponse(&subscription))
return c.JSON(http.StatusOK, h.toSubscriptionResponse(&subscription))
}
// GetByUser handles GET /api/admin/subscriptions/user/:user_id
func (h *AdminSubscriptionHandler) GetByUser(c *gin.Context) {
func (h *AdminSubscriptionHandler) GetByUser(c echo.Context) error {
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var subscription models.UserSubscription
@@ -180,22 +169,20 @@ func (h *AdminSubscriptionHandler) GetByUser(c *gin.Context) {
IsFree: false,
}
if err := h.db.Create(&subscription).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create subscription"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create subscription"})
}
// Reload with user
h.db.Preload("User").First(&subscription, subscription.ID)
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscription"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch subscription"})
}
}
c.JSON(http.StatusOK, h.toSubscriptionResponse(&subscription))
return c.JSON(http.StatusOK, h.toSubscriptionResponse(&subscription))
}
// GetStats handles GET /api/admin/subscriptions/stats
func (h *AdminSubscriptionHandler) GetStats(c *gin.Context) {
func (h *AdminSubscriptionHandler) GetStats(c echo.Context) error {
var total, free, premium, pro int64
h.db.Model(&models.UserSubscription{}).Count(&total)
@@ -203,7 +190,7 @@ func (h *AdminSubscriptionHandler) GetStats(c *gin.Context) {
h.db.Model(&models.UserSubscription{}).Where("tier = ?", "premium").Count(&premium)
h.db.Model(&models.UserSubscription{}).Where("tier = ?", "pro").Count(&pro)
c.JSON(http.StatusOK, gin.H{
return c.JSON(http.StatusOK, map[string]interface{}{
"total": total,
"free": free,
"premium": premium,

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/shopspring/decimal"
"gorm.io/gorm"
@@ -24,11 +24,10 @@ func NewAdminTaskHandler(db *gorm.DB) *AdminTaskHandler {
}
// List handles GET /api/admin/tasks
func (h *AdminTaskHandler) List(c *gin.Context) {
func (h *AdminTaskHandler) List(c echo.Context) error {
var filters dto.TaskFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var tasks []models.Task
@@ -80,8 +79,7 @@ func (h *AdminTaskHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&tasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tasks"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch tasks"})
}
// Build response
@@ -90,15 +88,14 @@ func (h *AdminTaskHandler) List(c *gin.Context) {
responses[i] = h.toTaskResponse(&task)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/tasks/:id
func (h *AdminTaskHandler) Get(c *gin.Context) {
func (h *AdminTaskHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid task ID"})
}
var task models.Task
@@ -112,11 +109,9 @@ func (h *AdminTaskHandler) Get(c *gin.Context) {
Preload("Completions").
First(&task, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Task not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch task"})
}
response := dto.TaskDetailResponse{
@@ -133,55 +128,48 @@ func (h *AdminTaskHandler) Get(c *gin.Context) {
response.CompletionCount = len(task.Completions)
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Update handles PUT /api/admin/tasks/:id
func (h *AdminTaskHandler) Update(c *gin.Context) {
func (h *AdminTaskHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid task ID"})
}
var task models.Task
if err := h.db.First(&task, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Task not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch task"})
}
var req dto.UpdateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify residence if changing
if req.ResidenceID != nil {
var residence models.Residence
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Residence not found"})
}
}
// Verify created_by if changing
if req.CreatedByID != nil {
var user models.User
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Created by user not found"})
}
}
// Verify assigned_to if changing
if req.AssignedToID != nil {
var user models.User
if err := h.db.First(&user, *req.AssignedToID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Assigned to user not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Assigned to user not found"})
}
}
@@ -247,35 +235,31 @@ func (h *AdminTaskHandler) Update(c *gin.Context) {
// Use Updates with map to only update specified fields
if err := h.db.Model(&task).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update task"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update task"})
}
// Reload with preloads for response
h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").First(&task, id)
c.JSON(http.StatusOK, h.toTaskResponse(&task))
return c.JSON(http.StatusOK, h.toTaskResponse(&task))
}
// Create handles POST /api/admin/tasks
func (h *AdminTaskHandler) Create(c *gin.Context) {
func (h *AdminTaskHandler) Create(c echo.Context) error {
var req dto.CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify residence exists
var residence models.Residence
if err := h.db.First(&residence, req.ResidenceID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Residence not found"})
}
// Verify created_by user exists
var creator models.User
if err := h.db.First(&creator, req.CreatedByID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Creator user not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Creator user not found"})
}
task := models.Task{
@@ -305,49 +289,43 @@ func (h *AdminTaskHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&task).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create task"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create task"})
}
h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").First(&task, task.ID)
c.JSON(http.StatusCreated, h.toTaskResponse(&task))
return c.JSON(http.StatusCreated, h.toTaskResponse(&task))
}
// Delete handles DELETE /api/admin/tasks/:id
func (h *AdminTaskHandler) Delete(c *gin.Context) {
func (h *AdminTaskHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid task ID"})
}
var task models.Task
if err := h.db.First(&task, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Task not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch task"})
}
// Soft delete - archive and cancel
task.IsArchived = true
task.IsCancelled = true
if err := h.db.Omit("Residence", "CreatedBy", "AssignedTo", "Category", "Priority", "Frequency", "ParentTask", "Completions").Save(&task).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete task"})
}
c.JSON(http.StatusOK, gin.H{"message": "Task archived successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Task archived successfully"})
}
// BulkDelete handles DELETE /api/admin/tasks/bulk
func (h *AdminTaskHandler) BulkDelete(c *gin.Context) {
func (h *AdminTaskHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Soft delete - archive and cancel all
@@ -355,11 +333,10 @@ func (h *AdminTaskHandler) BulkDelete(c *gin.Context) {
"is_archived": true,
"is_cancelled": true,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete tasks"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete tasks"})
}
c.JSON(http.StatusOK, gin.H{"message": "Tasks archived successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Tasks archived successfully", "count": len(req.IDs)})
}
func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {

View File

@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
@@ -28,7 +28,6 @@ func NewAdminTaskTemplateHandler(db *gorm.DB) *AdminTaskTemplateHandler {
func (h *AdminTaskTemplateHandler) refreshTaskTemplatesCache(ctx context.Context) {
cache := services.GetCache()
if cache == nil {
return
}
var templates []models.TaskTemplate
@@ -37,19 +36,16 @@ func (h *AdminTaskTemplateHandler) refreshTaskTemplatesCache(ctx context.Context
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")
}
@@ -84,35 +80,34 @@ type CreateUpdateTaskTemplateRequest struct {
}
// ListTemplates handles GET /admin/api/task-templates/
func (h *AdminTaskTemplateHandler) ListTemplates(c *gin.Context) {
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.Query("is_active"); activeParam != "" {
if activeParam := c.QueryParam("is_active"); activeParam != "" {
isActive := activeParam == "true"
query = query.Where("is_active = ?", isActive)
}
// Optional filter by category
if categoryID := c.Query("category_id"); categoryID != "" {
if categoryID := c.QueryParam("category_id"); categoryID != "" {
query = query.Where("category_id = ?", categoryID)
}
// Optional filter by frequency
if frequencyID := c.Query("frequency_id"); frequencyID != "" {
if frequencyID := c.QueryParam("frequency_id"); frequencyID != "" {
query = query.Where("frequency_id = ?", frequencyID)
}
// Optional search
if search := c.Query("search"); 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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch templates"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch templates"})
}
responses := make([]TaskTemplateResponse, len(templates))
@@ -120,36 +115,32 @@ func (h *AdminTaskTemplateHandler) ListTemplates(c *gin.Context) {
responses[i] = h.toResponse(&t)
}
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
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 *gin.Context) {
func (h *AdminTaskTemplateHandler) GetTemplate(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
return
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Template not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch template"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch template"})
}
c.JSON(http.StatusOK, h.toResponse(&template))
return c.JSON(http.StatusOK, h.toResponse(&template))
}
// CreateTemplate handles POST /admin/api/task-templates/
func (h *AdminTaskTemplateHandler) CreateTemplate(c *gin.Context) {
func (h *AdminTaskTemplateHandler) CreateTemplate(c echo.Context) error {
var req CreateUpdateTaskTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
template := models.TaskTemplate{
@@ -171,41 +162,36 @@ func (h *AdminTaskTemplateHandler) CreateTemplate(c *gin.Context) {
}
if err := h.db.Create(&template).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create template"})
return
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())
h.refreshTaskTemplatesCache(c.Request().Context())
c.JSON(http.StatusCreated, h.toResponse(&template))
return c.JSON(http.StatusCreated, h.toResponse(&template))
}
// UpdateTemplate handles PUT /admin/api/task-templates/:id/
func (h *AdminTaskTemplateHandler) UpdateTemplate(c *gin.Context) {
func (h *AdminTaskTemplateHandler) UpdateTemplate(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
return
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Template not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch template"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch template"})
}
var req CreateUpdateTaskTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
template.Title = req.Title
@@ -224,79 +210,71 @@ func (h *AdminTaskTemplateHandler) UpdateTemplate(c *gin.Context) {
}
if err := h.db.Save(&template).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update template"})
return
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())
h.refreshTaskTemplatesCache(c.Request().Context())
c.JSON(http.StatusOK, h.toResponse(&template))
return c.JSON(http.StatusOK, h.toResponse(&template))
}
// DeleteTemplate handles DELETE /admin/api/task-templates/:id/
func (h *AdminTaskTemplateHandler) DeleteTemplate(c *gin.Context) {
func (h *AdminTaskTemplateHandler) DeleteTemplate(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid template ID"})
}
if err := h.db.Delete(&models.TaskTemplate{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete template"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete template"})
}
// Refresh cache after deleting
h.refreshTaskTemplatesCache(c.Request.Context())
h.refreshTaskTemplatesCache(c.Request().Context())
c.JSON(http.StatusOK, gin.H{"message": "Template deleted successfully"})
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 *gin.Context) {
func (h *AdminTaskTemplateHandler) ToggleActive(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
return
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Template not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch template"})
return
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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update template"})
return
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())
h.refreshTaskTemplatesCache(c.Request().Context())
c.JSON(http.StatusOK, h.toResponse(&template))
return c.JSON(http.StatusOK, h.toResponse(&template))
}
// BulkCreate handles POST /admin/api/task-templates/bulk/
func (h *AdminTaskTemplateHandler) BulkCreate(c *gin.Context) {
func (h *AdminTaskTemplateHandler) BulkCreate(c echo.Context) error {
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
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))
@@ -320,14 +298,13 @@ func (h *AdminTaskTemplateHandler) BulkCreate(c *gin.Context) {
}
if err := h.db.Create(&templates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create templates"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create templates"})
}
// Refresh cache after bulk creating
h.refreshTaskTemplatesCache(c.Request.Context())
h.refreshTaskTemplatesCache(c.Request().Context())
c.JSON(http.StatusCreated, gin.H{"message": "Templates created successfully", "count": len(templates)})
return c.JSON(http.StatusCreated, map[string]interface{}{"message": "Templates created successfully", "count": len(templates)})
}
// Helper to convert model to response

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -22,11 +22,10 @@ func NewAdminUserHandler(db *gorm.DB) *AdminUserHandler {
}
// List handles GET /api/admin/users
func (h *AdminUserHandler) List(c *gin.Context) {
func (h *AdminUserHandler) List(c echo.Context) error {
var filters dto.UserFilters
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var users []models.User
@@ -68,8 +67,7 @@ func (h *AdminUserHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch users"})
}
// Build response
@@ -78,25 +76,22 @@ func (h *AdminUserHandler) List(c *gin.Context) {
responses[i] = h.toUserResponse(&user)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/users/:id
func (h *AdminUserHandler) Get(c *gin.Context) {
func (h *AdminUserHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var user models.User
if err := h.db.Preload("Profile").Preload("OwnedResidences").First(&user, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user"})
}
// Build detailed response
@@ -114,30 +109,27 @@ func (h *AdminUserHandler) Get(c *gin.Context) {
})
}
c.JSON(http.StatusOK, response)
return c.JSON(http.StatusOK, response)
}
// Create handles POST /api/admin/users
func (h *AdminUserHandler) Create(c *gin.Context) {
func (h *AdminUserHandler) Create(c echo.Context) error {
var req dto.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Check if username exists
var count int64
h.db.Model(&models.User{}).Where("username = ?", req.Username).Count(&count)
if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})
return
return c.JSON(http.StatusConflict, map[string]interface{}{"error": "Username already exists"})
}
// Check if email exists
h.db.Model(&models.User{}).Where("email = ?", req.Email).Count(&count)
if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "Email already exists"})
return
return c.JSON(http.StatusConflict, map[string]interface{}{"error": "Email already exists"})
}
user := models.User{
@@ -160,13 +152,11 @@ func (h *AdminUserHandler) Create(c *gin.Context) {
}
if err := user.SetPassword(req.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to hash password"})
}
if err := h.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create user"})
}
// Create profile with phone number
@@ -178,31 +168,27 @@ func (h *AdminUserHandler) Create(c *gin.Context) {
// Reload with profile
h.db.Preload("Profile").First(&user, user.ID)
c.JSON(http.StatusCreated, h.toUserResponse(&user))
return c.JSON(http.StatusCreated, h.toUserResponse(&user))
}
// Update handles PUT /api/admin/users/:id
func (h *AdminUserHandler) Update(c *gin.Context) {
func (h *AdminUserHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var user models.User
if err := h.db.First(&user, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user"})
}
var req dto.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Check username uniqueness if changing
@@ -210,8 +196,7 @@ func (h *AdminUserHandler) Update(c *gin.Context) {
var count int64
h.db.Model(&models.User{}).Where("username = ? AND id != ?", *req.Username, id).Count(&count)
if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})
return
return c.JSON(http.StatusConflict, map[string]interface{}{"error": "Username already exists"})
}
user.Username = *req.Username
}
@@ -221,8 +206,7 @@ func (h *AdminUserHandler) Update(c *gin.Context) {
var count int64
h.db.Model(&models.User{}).Where("email = ? AND id != ?", *req.Email, id).Count(&count)
if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "Email already exists"})
return
return c.JSON(http.StatusConflict, map[string]interface{}{"error": "Email already exists"})
}
user.Email = *req.Email
}
@@ -244,14 +228,12 @@ func (h *AdminUserHandler) Update(c *gin.Context) {
}
if req.Password != nil {
if err := user.SetPassword(*req.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to hash password"})
}
}
if err := h.db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update user"})
}
// Update profile fields if provided
@@ -279,52 +261,46 @@ func (h *AdminUserHandler) Update(c *gin.Context) {
}
h.db.Preload("Profile").First(&user, id)
c.JSON(http.StatusOK, h.toUserResponse(&user))
return c.JSON(http.StatusOK, h.toUserResponse(&user))
}
// Delete handles DELETE /api/admin/users/:id
func (h *AdminUserHandler) Delete(c *gin.Context) {
func (h *AdminUserHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var user models.User
if err := h.db.First(&user, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user"})
}
// Soft delete - just deactivate
user.IsActive = false
if err := h.db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete user"})
}
c.JSON(http.StatusOK, gin.H{"message": "User deactivated successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "User deactivated successfully"})
}
// BulkDelete handles DELETE /api/admin/users/bulk
func (h *AdminUserHandler) BulkDelete(c *gin.Context) {
func (h *AdminUserHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Soft delete - deactivate all
if err := h.db.Model(&models.User{}).Where("id IN ?", req.IDs).Update("is_active", false).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete users"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete users"})
}
c.JSON(http.StatusOK, gin.H{"message": "Users deactivated successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Users deactivated successfully", "count": len(req.IDs)})
}
// toUserResponse converts a User model to UserResponse DTO

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -47,11 +47,10 @@ type UpdateUserProfileRequest struct {
}
// List handles GET /api/admin/user-profiles
func (h *AdminUserProfileHandler) List(c *gin.Context) {
func (h *AdminUserProfileHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
var profiles []models.UserProfile
@@ -81,8 +80,7 @@ func (h *AdminUserProfileHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&profiles).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user profiles"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user profiles"})
}
// Build response
@@ -91,73 +89,63 @@ func (h *AdminUserProfileHandler) List(c *gin.Context) {
responses[i] = h.toProfileResponse(&profile)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/user-profiles/:id
func (h *AdminUserProfileHandler) Get(c *gin.Context) {
func (h *AdminUserProfileHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid profile ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid profile ID"})
}
var profile models.UserProfile
if err := h.db.Preload("User").First(&profile, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User profile not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User profile not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user profile"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user profile"})
}
c.JSON(http.StatusOK, h.toProfileResponse(&profile))
return c.JSON(http.StatusOK, h.toProfileResponse(&profile))
}
// GetByUser handles GET /api/admin/user-profiles/user/:user_id
func (h *AdminUserProfileHandler) GetByUser(c *gin.Context) {
func (h *AdminUserProfileHandler) GetByUser(c echo.Context) error {
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid user ID"})
}
var profile models.UserProfile
if err := h.db.Preload("User").Where("user_id = ?", userID).First(&profile).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User profile not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User profile not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user profile"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user profile"})
}
c.JSON(http.StatusOK, h.toProfileResponse(&profile))
return c.JSON(http.StatusOK, h.toProfileResponse(&profile))
}
// Update handles PUT /api/admin/user-profiles/:id
func (h *AdminUserProfileHandler) Update(c *gin.Context) {
func (h *AdminUserProfileHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid profile ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid profile ID"})
}
var profile models.UserProfile
if err := h.db.First(&profile, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User profile not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User profile not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user profile"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user profile"})
}
var req UpdateUserProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.Verified != nil {
@@ -178,62 +166,54 @@ func (h *AdminUserProfileHandler) Update(c *gin.Context) {
} else {
dob, err := time.Parse("2006-01-02", *req.DateOfBirth)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format for date_of_birth, use YYYY-MM-DD"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid date format for date_of_birth, use YYYY-MM-DD"})
}
profile.DateOfBirth = &dob
}
}
if err := h.db.Save(&profile).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user profile"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update user profile"})
}
h.db.Preload("User").First(&profile, id)
c.JSON(http.StatusOK, h.toProfileResponse(&profile))
return c.JSON(http.StatusOK, h.toProfileResponse(&profile))
}
// Delete handles DELETE /api/admin/user-profiles/:id
func (h *AdminUserProfileHandler) Delete(c *gin.Context) {
func (h *AdminUserProfileHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid profile ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid profile ID"})
}
var profile models.UserProfile
if err := h.db.First(&profile, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "User profile not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "User profile not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user profile"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch user profile"})
}
if err := h.db.Delete(&profile).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profile"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete user profile"})
}
c.JSON(http.StatusOK, gin.H{"message": "User profile deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "User profile deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/user-profiles/bulk
func (h *AdminUserProfileHandler) BulkDelete(c *gin.Context) {
func (h *AdminUserProfileHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if err := h.db.Where("id IN ?", req.IDs).Delete(&models.UserProfile{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profiles"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete user profiles"})
}
c.JSON(http.StatusOK, gin.H{"message": "User profiles deleted successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "User profiles deleted successfully", "count": len(req.IDs)})
}
// toProfileResponse converts a UserProfile model to UserProfileResponse

View File

@@ -6,7 +6,7 @@ import (
"net/url"
"os"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/handlers"
@@ -27,7 +27,7 @@ type Dependencies struct {
}
// SetupRoutes configures all admin routes
func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Dependencies) {
func SetupRoutes(router *echo.Echo, db *gorm.DB, cfg *config.Config, deps *Dependencies) {
// Create repositories
adminRepo := repositories.NewAdminRepository(db)
@@ -445,7 +445,7 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe
}
// setupAdminProxy configures reverse proxy to the Next.js admin panel
func setupAdminProxy(router *gin.Engine) {
func setupAdminProxy(router *echo.Echo) {
// Get admin panel URL from env, default to localhost:3001
// Note: In production (Dokku), Next.js runs on internal port 3001
adminURL := os.Getenv("ADMIN_PANEL_URL")
@@ -461,17 +461,19 @@ func setupAdminProxy(router *gin.Engine) {
proxy := httputil.NewSingleHostReverseProxy(target)
// Handle all /admin/* requests
router.Any("/admin/*filepath", func(c *gin.Context) {
proxy.ServeHTTP(c.Writer, c.Request)
router.Any("/admin/*", func(c echo.Context) error {
proxy.ServeHTTP(c.Response(), c.Request())
return nil
})
// Also handle /admin without trailing path
router.Any("/admin", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/admin/")
router.Any("/admin", func(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, "/admin/")
})
// Proxy Next.js static assets
router.Any("/_next/*filepath", func(c *gin.Context) {
proxy.ServeHTTP(c.Writer, c.Request)
router.Any("/_next/*", func(c echo.Context) error {
proxy.ServeHTTP(c.Response(), c.Request())
return nil
})
}