Add Next.js admin panel and implement background worker jobs
- Add full Next.js admin panel with: - User, residence, task, contractor, document management - Notifications and notification preferences management - Subscriptions and auth token management - Dashboard with stats - Lookup tables management (categories, priorities, statuses, etc.) - Admin user management - Implement background worker job handlers: - HandleTaskReminder: sends push notifications for tasks due within 24h - HandleOverdueReminder: sends push notifications for overdue tasks - HandleDailyDigest: sends daily summary of pending tasks - HandleSendEmail: processes email sending jobs - HandleSendPush: processes push notification jobs - Make worker job schedules configurable via environment variables: - TASK_REMINDER_HOUR, TASK_REMINDER_MINUTE (default: 20:00 UTC) - OVERDUE_REMINDER_HOUR (default: 09:00 UTC) - DAILY_DIGEST_HOUR (default: 11:00 UTC) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,88 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
_ "github.com/GoAdminGroup/go-admin/adapter/gin" // Gin adapter for GoAdmin
|
||||
"github.com/GoAdminGroup/go-admin/engine"
|
||||
"github.com/GoAdminGroup/go-admin/modules/config"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/modules/language"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template"
|
||||
"github.com/GoAdminGroup/go-admin/template/chartjs"
|
||||
"github.com/GoAdminGroup/themes/adminlte"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
appconfig "github.com/treytartt/mycrib-api/internal/config"
|
||||
"github.com/treytartt/mycrib-api/internal/admin/tables"
|
||||
)
|
||||
|
||||
// Setup initializes the GoAdmin panel
|
||||
func Setup(r *gin.Engine, cfg *appconfig.Config) (*engine.Engine, error) {
|
||||
eng := engine.Default()
|
||||
|
||||
// Register the AdminLTE theme
|
||||
template.AddComp(chartjs.NewChart())
|
||||
|
||||
// Configure GoAdmin
|
||||
adminConfig := config.Config{
|
||||
Databases: config.DatabaseList{
|
||||
"default": {
|
||||
Host: cfg.Database.Host,
|
||||
Port: fmt.Sprintf("%d", cfg.Database.Port),
|
||||
User: cfg.Database.User,
|
||||
Pwd: cfg.Database.Password,
|
||||
Name: cfg.Database.Database,
|
||||
MaxIdleConns: cfg.Database.MaxIdleConns,
|
||||
MaxOpenConns: cfg.Database.MaxOpenConns,
|
||||
Driver: db.DriverPostgresql,
|
||||
},
|
||||
},
|
||||
UrlPrefix: "admin",
|
||||
IndexUrl: "/",
|
||||
Debug: cfg.Server.Debug,
|
||||
Language: language.EN,
|
||||
Theme: "adminlte",
|
||||
Store: config.Store{
|
||||
Path: "./uploads",
|
||||
Prefix: "uploads",
|
||||
},
|
||||
Title: "MyCrib Admin",
|
||||
Logo: "MyCrib",
|
||||
MiniLogo: "MC",
|
||||
BootstrapFilePath: "",
|
||||
GoModFilePath: "",
|
||||
ColorScheme: adminlte.ColorschemeSkinBlack,
|
||||
Animation: config.PageAnimation{
|
||||
Type: "fadeInUp",
|
||||
},
|
||||
}
|
||||
|
||||
// Add the admin plugin with generators
|
||||
adminPlugin := admin.NewAdmin(GetTables())
|
||||
|
||||
// Initialize engine and add generators
|
||||
if err := eng.AddConfig(&adminConfig).
|
||||
AddGenerators(GetTables()).
|
||||
AddPlugins(adminPlugin).
|
||||
Use(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add redirect for /admin to dashboard
|
||||
r.GET("/admin", func(c *gin.Context) {
|
||||
c.Redirect(302, "/admin/menu")
|
||||
})
|
||||
r.GET("/admin/", func(c *gin.Context) {
|
||||
c.Redirect(302, "/admin/menu")
|
||||
})
|
||||
|
||||
return eng, nil
|
||||
}
|
||||
|
||||
// GetTables returns all table generators for the admin panel
|
||||
func GetTables() table.GeneratorList {
|
||||
return tables.Generators
|
||||
}
|
||||
280
internal/admin/dto/requests.go
Normal file
280
internal/admin/dto/requests.go
Normal file
@@ -0,0 +1,280 @@
|
||||
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=100"`
|
||||
Search string `form:"search"`
|
||||
SortBy string `form:"sort_by"`
|
||||
SortDir string `form:"sort_dir" binding:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
// GetPage returns the page number with default
|
||||
func (p *PaginationParams) GetPage() int {
|
||||
if p.Page < 1 {
|
||||
return 1
|
||||
}
|
||||
return p.Page
|
||||
}
|
||||
|
||||
// GetPerPage returns items per page with default
|
||||
func (p *PaginationParams) GetPerPage() int {
|
||||
if p.PerPage < 1 {
|
||||
return 20
|
||||
}
|
||||
if p.PerPage > 100 {
|
||||
return 100
|
||||
}
|
||||
return p.PerPage
|
||||
}
|
||||
|
||||
// GetOffset calculates the database offset
|
||||
func (p *PaginationParams) GetOffset() int {
|
||||
return (p.GetPage() - 1) * p.GetPerPage()
|
||||
}
|
||||
|
||||
// GetSortDir returns sort direction with default
|
||||
func (p *PaginationParams) GetSortDir() string {
|
||||
if p.SortDir == "asc" {
|
||||
return "ASC"
|
||||
}
|
||||
return "DESC"
|
||||
}
|
||||
|
||||
// UserFilters holds user-specific filter parameters
|
||||
type UserFilters struct {
|
||||
PaginationParams
|
||||
IsActive *bool `form:"is_active"`
|
||||
IsStaff *bool `form:"is_staff"`
|
||||
IsSuperuser *bool `form:"is_superuser"`
|
||||
Verified *bool `form:"verified"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
IsStaff *bool `json:"is_staff"`
|
||||
IsSuperuser *bool `json:"is_superuser"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
IsStaff *bool `json:"is_staff"`
|
||||
IsSuperuser *bool `json:"is_superuser"`
|
||||
}
|
||||
|
||||
// BulkDeleteRequest for bulk delete operations
|
||||
type BulkDeleteRequest struct {
|
||||
IDs []uint `json:"ids" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// ResidenceFilters holds residence-specific filter parameters
|
||||
type ResidenceFilters struct {
|
||||
PaginationParams
|
||||
IsActive *bool `form:"is_active"`
|
||||
OwnerID *uint `form:"owner_id"`
|
||||
}
|
||||
|
||||
// UpdateResidenceRequest for updating a residence
|
||||
type UpdateResidenceRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=200"`
|
||||
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"`
|
||||
Country *string `json:"country" binding:"omitempty,max=100"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
IsPrimary *bool `json:"is_primary"`
|
||||
}
|
||||
|
||||
// TaskFilters holds task-specific filter parameters
|
||||
type TaskFilters struct {
|
||||
PaginationParams
|
||||
ResidenceID *uint `form:"residence_id"`
|
||||
CategoryID *uint `form:"category_id"`
|
||||
PriorityID *uint `form:"priority_id"`
|
||||
StatusID *uint `form:"status_id"`
|
||||
IsCancelled *bool `form:"is_cancelled"`
|
||||
IsArchived *bool `form:"is_archived"`
|
||||
}
|
||||
|
||||
// UpdateTaskRequest for updating a task
|
||||
type UpdateTaskRequest struct {
|
||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||
Description *string `json:"description"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
PriorityID *uint `json:"priority_id"`
|
||||
StatusID *uint `json:"status_id"`
|
||||
IsCancelled *bool `json:"is_cancelled"`
|
||||
IsArchived *bool `json:"is_archived"`
|
||||
}
|
||||
|
||||
// ContractorFilters holds contractor-specific filter parameters
|
||||
type ContractorFilters struct {
|
||||
PaginationParams
|
||||
IsActive *bool `form:"is_active"`
|
||||
IsFavorite *bool `form:"is_favorite"`
|
||||
ResidenceID *uint `form:"residence_id"`
|
||||
}
|
||||
|
||||
// UpdateContractorRequest for updating a contractor
|
||||
type UpdateContractorRequest struct {
|
||||
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"`
|
||||
Notes *string `json:"notes"`
|
||||
IsFavorite *bool `json:"is_favorite"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// DocumentFilters holds document-specific filter parameters
|
||||
type DocumentFilters struct {
|
||||
PaginationParams
|
||||
IsActive *bool `form:"is_active"`
|
||||
ResidenceID *uint `form:"residence_id"`
|
||||
DocumentType *string `form:"document_type"`
|
||||
}
|
||||
|
||||
// UpdateDocumentRequest for updating a document
|
||||
type UpdateDocumentRequest struct {
|
||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||
Description *string `json:"description"`
|
||||
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"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// NotificationFilters holds notification-specific filter parameters
|
||||
type NotificationFilters struct {
|
||||
PaginationParams
|
||||
UserID *uint `form:"user_id"`
|
||||
NotificationType *string `form:"notification_type"`
|
||||
Sent *bool `form:"sent"`
|
||||
Read *bool `form:"read"`
|
||||
}
|
||||
|
||||
// UpdateNotificationRequest for updating a notification
|
||||
type UpdateNotificationRequest struct {
|
||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||
Body *string `json:"body" binding:"omitempty,max=1000"`
|
||||
Read *bool `json:"read"`
|
||||
}
|
||||
|
||||
// SubscriptionFilters holds subscription-specific filter parameters
|
||||
type SubscriptionFilters struct {
|
||||
PaginationParams
|
||||
UserID *uint `form:"user_id"`
|
||||
Tier *string `form:"tier"`
|
||||
Platform *string `form:"platform"`
|
||||
AutoRenew *bool `form:"auto_renew"`
|
||||
Active *bool `form:"active"`
|
||||
}
|
||||
|
||||
// UpdateSubscriptionRequest for updating a subscription
|
||||
type UpdateSubscriptionRequest struct {
|
||||
Tier *string `json:"tier" binding:"omitempty,oneof=free premium pro"`
|
||||
AutoRenew *bool `json:"auto_renew"`
|
||||
}
|
||||
|
||||
// CreateResidenceRequest for creating a new residence
|
||||
type CreateResidenceRequest struct {
|
||||
OwnerID uint `json:"owner_id" binding:"required"`
|
||||
Name string `json:"name" binding:"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"`
|
||||
Bedrooms *int `json:"bedrooms"`
|
||||
Bathrooms *float64 `json:"bathrooms"`
|
||||
SquareFootage *int `json:"square_footage"`
|
||||
YearBuilt *int `json:"year_built"`
|
||||
Description string `json:"description"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Description string `json:"description"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
PriorityID *uint `json:"priority_id"`
|
||||
StatusID *uint `json:"status_id"`
|
||||
FrequencyID *uint `json:"frequency_id"`
|
||||
AssignedToID *uint `json:"assigned_to_id"`
|
||||
DueDate *string `json:"due_date"`
|
||||
EstimatedCost *float64 `json:"estimated_cost"`
|
||||
ContractorID *uint `json:"contractor_id"`
|
||||
}
|
||||
|
||||
// CreateContractorRequest for creating a new contractor
|
||||
type CreateContractorRequest struct {
|
||||
ResidenceID uint `json:"residence_id" binding:"required"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
FileSize *int64 `json:"file_size"`
|
||||
MimeType string `json:"mime_type" binding:"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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
219
internal/admin/dto/responses.go
Normal file
219
internal/admin/dto/responses.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package dto
|
||||
|
||||
// PaginatedResponse represents a paginated API response
|
||||
type PaginatedResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// NewPaginatedResponse creates a new paginated response
|
||||
func NewPaginatedResponse(data interface{}, total int64, page, perPage int) PaginatedResponse {
|
||||
totalPages := int(total) / perPage
|
||||
if int(total)%perPage > 0 {
|
||||
totalPages++
|
||||
}
|
||||
return PaginatedResponse{
|
||||
Data: data,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
// UserResponse represents a user in admin responses
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsStaff bool `json:"is_staff"`
|
||||
IsSuperuser bool `json:"is_superuser"`
|
||||
DateJoined string `json:"date_joined"`
|
||||
LastLogin *string `json:"last_login,omitempty"`
|
||||
// Profile info
|
||||
Verified bool `json:"verified"`
|
||||
PhoneNumber *string `json:"phone_number,omitempty"`
|
||||
// Counts
|
||||
ResidenceCount int `json:"residence_count"`
|
||||
TaskCount int `json:"task_count"`
|
||||
}
|
||||
|
||||
// UserDetailResponse includes more details for single user view
|
||||
type UserDetailResponse struct {
|
||||
UserResponse
|
||||
Residences []ResidenceSummary `json:"residences,omitempty"`
|
||||
Devices []DeviceSummary `json:"devices,omitempty"`
|
||||
}
|
||||
|
||||
// ResidenceSummary is a brief residence representation
|
||||
type ResidenceSummary struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsOwner bool `json:"is_owner"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// DeviceSummary is a brief device representation
|
||||
type DeviceSummary struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"` // "apns" or "gcm"
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// UserSummary is a brief user representation
|
||||
type UserSummary struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// ResidenceResponse represents a residence in admin responses
|
||||
type ResidenceResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID uint `json:"owner_id"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
PropertyType *string `json:"property_type,omitempty"`
|
||||
StreetAddress string `json:"street_address"`
|
||||
City string `json:"city"`
|
||||
StateProvince string `json:"state_province"`
|
||||
PostalCode string `json:"postal_code"`
|
||||
Country string `json:"country"`
|
||||
Bedrooms *int `json:"bedrooms,omitempty"`
|
||||
Bathrooms *float64 `json:"bathrooms,omitempty"`
|
||||
SquareFootage *int `json:"square_footage,omitempty"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ResidenceDetailResponse includes more details for single residence view
|
||||
type ResidenceDetailResponse struct {
|
||||
ResidenceResponse
|
||||
Owner *UserSummary `json:"owner,omitempty"`
|
||||
SharedUsers []UserSummary `json:"shared_users,omitempty"`
|
||||
TaskCount int `json:"task_count"`
|
||||
DocumentCount int `json:"document_count"`
|
||||
}
|
||||
|
||||
// TaskResponse represents a task in admin responses
|
||||
type TaskResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CreatedByName string `json:"created_by_name"`
|
||||
CategoryName *string `json:"category_name,omitempty"`
|
||||
PriorityName *string `json:"priority_name,omitempty"`
|
||||
StatusName *string `json:"status_name,omitempty"`
|
||||
DueDate *string `json:"due_date,omitempty"`
|
||||
EstimatedCost *float64 `json:"estimated_cost,omitempty"`
|
||||
IsCancelled bool `json:"is_cancelled"`
|
||||
IsArchived bool `json:"is_archived"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// TaskDetailResponse includes more details for single task view
|
||||
type TaskDetailResponse struct {
|
||||
TaskResponse
|
||||
AssignedTo *UserSummary `json:"assigned_to,omitempty"`
|
||||
CompletionCount int `json:"completion_count"`
|
||||
}
|
||||
|
||||
// ContractorResponse represents a contractor in admin responses
|
||||
type ContractorResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Website string `json:"website"`
|
||||
City string `json:"city"`
|
||||
Rating *float64 `json:"rating,omitempty"`
|
||||
Specialties []string `json:"specialties,omitempty"`
|
||||
IsFavorite bool `json:"is_favorite"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ContractorDetailResponse includes more details for single contractor view
|
||||
type ContractorDetailResponse struct {
|
||||
ContractorResponse
|
||||
TaskCount int `json:"task_count"`
|
||||
}
|
||||
|
||||
// DocumentResponse represents a document in admin responses
|
||||
type DocumentResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DocumentType string `json:"document_type"`
|
||||
FileName string `json:"file_name"`
|
||||
FileURL string `json:"file_url"`
|
||||
Vendor string `json:"vendor"`
|
||||
ExpiryDate *string `json:"expiry_date,omitempty"`
|
||||
PurchaseDate *string `json:"purchase_date,omitempty"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// DocumentDetailResponse includes more details for single document view
|
||||
type DocumentDetailResponse struct {
|
||||
DocumentResponse
|
||||
TaskTitle *string `json:"task_title,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationResponse represents a notification in admin responses
|
||||
type NotificationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
UserEmail string `json:"user_email"`
|
||||
Username string `json:"username"`
|
||||
NotificationType string `json:"notification_type"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Sent bool `json:"sent"`
|
||||
SentAt *string `json:"sent_at,omitempty"`
|
||||
Read bool `json:"read"`
|
||||
ReadAt *string `json:"read_at,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// NotificationDetailResponse includes more details for single notification view
|
||||
type NotificationDetailResponse struct {
|
||||
NotificationResponse
|
||||
Data string `json:"data"`
|
||||
TaskID *uint `json:"task_id,omitempty"`
|
||||
}
|
||||
|
||||
// SubscriptionResponse represents a subscription in admin responses
|
||||
type SubscriptionResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
UserEmail string `json:"user_email"`
|
||||
Username string `json:"username"`
|
||||
Tier string `json:"tier"`
|
||||
Platform string `json:"platform"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
SubscribedAt *string `json:"subscribed_at,omitempty"`
|
||||
ExpiresAt *string `json:"expires_at,omitempty"`
|
||||
CancelledAt *string `json:"cancelled_at,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// SubscriptionDetailResponse includes more details for single subscription view
|
||||
type SubscriptionDetailResponse struct {
|
||||
SubscriptionResponse
|
||||
}
|
||||
311
internal/admin/handlers/admin_user_handler.go
Normal file
311
internal/admin/handlers/admin_user_handler.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/middleware"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminUserManagementHandler handles admin user management endpoints
|
||||
type AdminUserManagementHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminUserManagementHandler creates a new admin user management handler
|
||||
func NewAdminUserManagementHandler(db *gorm.DB) *AdminUserManagementHandler {
|
||||
return &AdminUserManagementHandler{db: db}
|
||||
}
|
||||
|
||||
// Note: AdminUserResponse is defined in auth_handler.go and reused here
|
||||
|
||||
// AdminUserFilters for listing admin users
|
||||
type AdminUserFilters struct {
|
||||
dto.PaginationParams
|
||||
Role *string `form:"role"`
|
||||
IsActive *bool `form:"is_active"`
|
||||
}
|
||||
|
||||
// CreateAdminUserRequest for creating a new admin user
|
||||
type CreateAdminUserRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
FirstName string `json:"first_name" binding:"max=100"`
|
||||
LastName string `json:"last_name" binding:"max=100"`
|
||||
Role string `json:"role" binding:"omitempty,oneof=admin super_admin"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// UpdateAdminUserRequest for updating an admin user
|
||||
type UpdateAdminUserRequest struct {
|
||||
Email *string `json:"email" binding:"omitempty,email"`
|
||||
Password *string `json:"password" binding:"omitempty,min=8"`
|
||||
FirstName *string `json:"first_name" binding:"omitempty,max=100"`
|
||||
LastName *string `json:"last_name" binding:"omitempty,max=100"`
|
||||
Role *string `json:"role" binding:"omitempty,oneof=admin super_admin"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/admin-users
|
||||
func (h *AdminUserManagementHandler) List(c *gin.Context) {
|
||||
var filters AdminUserFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var adminUsers []models.AdminUser
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.AdminUser{})
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where("email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", search, search, search)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.Role != nil {
|
||||
query = query.Where("role = ?", *filters.Role)
|
||||
}
|
||||
if filters.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *filters.IsActive)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&adminUsers).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin users"})
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]AdminUserResponse, len(adminUsers))
|
||||
for i, u := range adminUsers {
|
||||
responses[i] = h.toAdminUserResponse(&u)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin user"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, h.toAdminUserResponse(&adminUser))
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/admin-users
|
||||
func (h *AdminUserManagementHandler) Create(c *gin.Context) {
|
||||
// Only super admins can create admin users
|
||||
currentAdmin, exists := c.Get(middleware.AdminUserKey)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
admin := currentAdmin.(*models.AdminUser)
|
||||
if !admin.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Only super admins can create admin users"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateAdminUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
adminUser := models.AdminUser{
|
||||
Email: req.Email,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Role: models.AdminRole(req.Role),
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if req.Role == "" {
|
||||
adminUser.Role = models.AdminRoleAdmin
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
adminUser.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := adminUser.SetPassword(req.Password); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Create(&adminUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create admin user"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, h.toAdminUserResponse(&adminUser))
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/admin-users/:id
|
||||
func (h *AdminUserManagementHandler) Update(c *gin.Context) {
|
||||
// Only super admins can update admin users
|
||||
currentAdmin, exists := c.Get(middleware.AdminUserKey)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin user"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateAdminUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
// Check if email already exists for another user
|
||||
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
|
||||
}
|
||||
adminUser.Email = *req.Email
|
||||
}
|
||||
if req.FirstName != nil {
|
||||
adminUser.FirstName = *req.FirstName
|
||||
}
|
||||
if req.LastName != nil {
|
||||
adminUser.LastName = *req.LastName
|
||||
}
|
||||
// Only super admins can change roles
|
||||
if req.Role != nil && admin.IsSuperAdmin() {
|
||||
adminUser.Role = models.AdminRole(*req.Role)
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.db.Save(&adminUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update admin user"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, h.toAdminUserResponse(&adminUser))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/admin-users/:id
|
||||
func (h *AdminUserManagementHandler) Delete(c *gin.Context) {
|
||||
// Only super admins can delete admin users
|
||||
currentAdmin, exists := c.Get(middleware.AdminUserKey)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
admin := currentAdmin.(*models.AdminUser)
|
||||
if !admin.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Only super admins can delete admin users"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent self-deletion
|
||||
if uint(id) == admin.ID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch admin user"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&adminUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete admin user"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Admin user deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *AdminUserManagementHandler) toAdminUserResponse(u *models.AdminUser) AdminUserResponse {
|
||||
return NewAdminUserResponse(u)
|
||||
}
|
||||
140
internal/admin/handlers/auth_handler.go
Normal file
140
internal/admin/handlers/auth_handler.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/config"
|
||||
"github.com/treytartt/mycrib-api/internal/middleware"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
"github.com/treytartt/mycrib-api/internal/repositories"
|
||||
)
|
||||
|
||||
// AdminAuthHandler handles admin authentication endpoints
|
||||
type AdminAuthHandler struct {
|
||||
adminRepo *repositories.AdminRepository
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewAdminAuthHandler creates a new admin auth handler
|
||||
func NewAdminAuthHandler(adminRepo *repositories.AdminRepository, cfg *config.Config) *AdminAuthHandler {
|
||||
return &AdminAuthHandler{
|
||||
adminRepo: adminRepo,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// LoginRequest represents the admin login request
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// LoginResponse represents the admin login response
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
Admin AdminUserResponse `json:"admin"`
|
||||
}
|
||||
|
||||
// AdminUserResponse represents an admin user in API responses
|
||||
type AdminUserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Role models.AdminRole `json:"role"`
|
||||
IsActive bool `json:"is_active"`
|
||||
LastLogin *string `json:"last_login,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// NewAdminUserResponse creates an AdminUserResponse from an AdminUser model
|
||||
func NewAdminUserResponse(admin *models.AdminUser) AdminUserResponse {
|
||||
resp := AdminUserResponse{
|
||||
ID: admin.ID,
|
||||
Email: admin.Email,
|
||||
FirstName: admin.FirstName,
|
||||
LastName: admin.LastName,
|
||||
Role: admin.Role,
|
||||
IsActive: admin.IsActive,
|
||||
CreatedAt: admin.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
if admin.LastLogin != nil {
|
||||
lastLogin := admin.LastLogin.Format("2006-01-02T15:04:05Z")
|
||||
resp.LastLogin = &lastLogin
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// Login handles POST /api/admin/auth/login
|
||||
func (h *AdminAuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Check password
|
||||
if !admin.CheckPassword(req.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if admin is active
|
||||
if !admin.IsActive {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Account is disabled"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Update last login
|
||||
_ = h.adminRepo.UpdateLastLogin(admin.ID)
|
||||
|
||||
// Refresh admin data after updating last login
|
||||
admin, _ = h.adminRepo.FindByID(admin.ID)
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{
|
||||
Token: token,
|
||||
Admin: NewAdminUserResponse(admin),
|
||||
})
|
||||
}
|
||||
|
||||
// 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"})
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// RefreshToken handles POST /api/admin/auth/refresh
|
||||
func (h *AdminAuthHandler) RefreshToken(c *gin.Context) {
|
||||
admin := c.MustGet(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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||
}
|
||||
155
internal/admin/handlers/auth_token_handler.go
Normal file
155
internal/admin/handlers/auth_token_handler.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminAuthTokenHandler handles admin auth token management endpoints
|
||||
type AdminAuthTokenHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminAuthTokenHandler creates a new admin auth token handler
|
||||
func NewAdminAuthTokenHandler(db *gorm.DB) *AdminAuthTokenHandler {
|
||||
return &AdminAuthTokenHandler{db: db}
|
||||
}
|
||||
|
||||
// AuthTokenResponse represents an auth token in API responses
|
||||
type AuthTokenResponse struct {
|
||||
Key string `json:"key"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/auth-tokens
|
||||
func (h *AdminAuthTokenHandler) List(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var tokens []models.AuthToken
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.AuthToken{}).Preload("User")
|
||||
|
||||
// Apply search (search by user info)
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("JOIN auth_user ON auth_user.id = user_authtoken.user_id").
|
||||
Where(
|
||||
"auth_user.username ILIKE ? OR auth_user.email ILIKE ? OR user_authtoken.key ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&tokens).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch auth tokens"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]AuthTokenResponse, len(tokens))
|
||||
for i, token := range tokens {
|
||||
responses[i] = AuthTokenResponse{
|
||||
Key: token.Key,
|
||||
UserID: token.UserID,
|
||||
Username: token.User.Username,
|
||||
Email: token.User.Email,
|
||||
Created: token.Created.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch auth token"})
|
||||
return
|
||||
}
|
||||
|
||||
response := AuthTokenResponse{
|
||||
Key: token.Key,
|
||||
UserID: token.UserID,
|
||||
Username: token.User.Username,
|
||||
Email: token.User.Email,
|
||||
Created: token.Created.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/auth-tokens/:id (revoke token)
|
||||
func (h *AdminAuthTokenHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Auth token not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Auth token revoked successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/auth-tokens/bulk
|
||||
func (h *AdminAuthTokenHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Auth tokens revoked successfully", "count": result.RowsAffected})
|
||||
}
|
||||
266
internal/admin/handlers/completion_handler.go
Normal file
266
internal/admin/handlers/completion_handler.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminCompletionHandler handles admin task completion management endpoints
|
||||
type AdminCompletionHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminCompletionHandler creates a new admin completion handler
|
||||
func NewAdminCompletionHandler(db *gorm.DB) *AdminCompletionHandler {
|
||||
return &AdminCompletionHandler{db: db}
|
||||
}
|
||||
|
||||
// CompletionResponse represents a task completion in API responses
|
||||
type CompletionResponse struct {
|
||||
ID uint `json:"id"`
|
||||
TaskID uint `json:"task_id"`
|
||||
TaskTitle string `json:"task_title"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
CompletedByID uint `json:"completed_by_id"`
|
||||
CompletedBy string `json:"completed_by"`
|
||||
CompletedAt string `json:"completed_at"`
|
||||
Notes string `json:"notes"`
|
||||
ActualCost *string `json:"actual_cost"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// CompletionFilters extends PaginationParams with completion-specific filters
|
||||
type CompletionFilters struct {
|
||||
dto.PaginationParams
|
||||
TaskID *uint `form:"task_id"`
|
||||
ResidenceID *uint `form:"residence_id"`
|
||||
UserID *uint `form:"user_id"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/completions
|
||||
func (h *AdminCompletionHandler) List(c *gin.Context) {
|
||||
var filters CompletionFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var completions []models.TaskCompletion
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.TaskCompletion{}).
|
||||
Preload("Task").
|
||||
Preload("Task.Residence").
|
||||
Preload("CompletedBy")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
|
||||
Joins("JOIN auth_user ON auth_user.id = task_taskcompletion.completed_by_id").
|
||||
Where(
|
||||
"task_task.title ILIKE ? OR auth_user.username ILIKE ? OR task_taskcompletion.notes ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.TaskID != nil {
|
||||
query = query.Where("task_id = ?", *filters.TaskID)
|
||||
}
|
||||
if filters.ResidenceID != nil {
|
||||
query = query.Joins("JOIN task_task t ON t.id = task_taskcompletion.task_id").
|
||||
Where("t.residence_id = ?", *filters.ResidenceID)
|
||||
}
|
||||
if filters.UserID != nil {
|
||||
query = query.Where("completed_by_id = ?", *filters.UserID)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "completed_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
sortDir := "DESC"
|
||||
if filters.SortDir != "" {
|
||||
sortDir = filters.GetSortDir()
|
||||
}
|
||||
query = query.Order(sortBy + " " + sortDir)
|
||||
|
||||
// Apply pagination
|
||||
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
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]CompletionResponse, len(completions))
|
||||
for i, completion := range completions {
|
||||
responses[i] = h.toCompletionResponse(&completion)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var completion models.TaskCompletion
|
||||
if err := h.db.Preload("Task").Preload("Task.Residence").Preload("CompletedBy").First(&completion, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Completion not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, h.toCompletionResponse(&completion))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/completions/:id
|
||||
func (h *AdminCompletionHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&completion).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete completion"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Completion deleted successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/completions/bulk
|
||||
func (h *AdminCompletionHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Completions deleted successfully", "count": result.RowsAffected})
|
||||
}
|
||||
|
||||
// UpdateCompletionRequest represents the request to update a completion
|
||||
type UpdateCompletionRequest struct {
|
||||
Notes *string `json:"notes"`
|
||||
ActualCost *string `json:"actual_cost"`
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/completions/:id
|
||||
func (h *AdminCompletionHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateCompletionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Notes != nil {
|
||||
completion.Notes = *req.Notes
|
||||
}
|
||||
if req.ActualCost != nil {
|
||||
if *req.ActualCost == "" {
|
||||
completion.ActualCost = nil
|
||||
} else {
|
||||
cost, err := decimal.NewFromString(*req.ActualCost)
|
||||
if err == nil {
|
||||
completion.ActualCost = &cost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.db.Save(&completion).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update completion"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Task").Preload("Task.Residence").Preload("CompletedBy").First(&completion, id)
|
||||
c.JSON(http.StatusOK, h.toCompletionResponse(&completion))
|
||||
}
|
||||
|
||||
// toCompletionResponse converts a TaskCompletion model to CompletionResponse
|
||||
func (h *AdminCompletionHandler) toCompletionResponse(completion *models.TaskCompletion) CompletionResponse {
|
||||
response := CompletionResponse{
|
||||
ID: completion.ID,
|
||||
TaskID: completion.TaskID,
|
||||
CompletedByID: completion.CompletedByID,
|
||||
CompletedAt: completion.CompletedAt.Format("2006-01-02T15:04:05Z"),
|
||||
Notes: completion.Notes,
|
||||
PhotoURL: completion.PhotoURL,
|
||||
CreatedAt: completion.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if completion.Task.ID != 0 {
|
||||
response.TaskTitle = completion.Task.Title
|
||||
response.ResidenceID = completion.Task.ResidenceID
|
||||
if completion.Task.Residence.ID != 0 {
|
||||
response.ResidenceName = completion.Task.Residence.Name
|
||||
}
|
||||
}
|
||||
|
||||
if completion.CompletedBy.ID != 0 {
|
||||
response.CompletedBy = completion.CompletedBy.Username
|
||||
}
|
||||
|
||||
if completion.ActualCost != nil {
|
||||
cost := completion.ActualCost.String()
|
||||
response.ActualCost = &cost
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
307
internal/admin/handlers/contractor_handler.go
Normal file
307
internal/admin/handlers/contractor_handler.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminContractorHandler handles admin contractor management endpoints
|
||||
type AdminContractorHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminContractorHandler creates a new admin contractor handler
|
||||
func NewAdminContractorHandler(db *gorm.DB) *AdminContractorHandler {
|
||||
return &AdminContractorHandler{db: db}
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/contractors
|
||||
func (h *AdminContractorHandler) List(c *gin.Context) {
|
||||
var filters dto.ContractorFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var contractors []models.Contractor
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.Contractor{}).
|
||||
Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("Specialties")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where(
|
||||
"name ILIKE ? OR company ILIKE ? OR email ILIKE ? OR phone ILIKE ?",
|
||||
search, search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *filters.IsActive)
|
||||
}
|
||||
if filters.IsFavorite != nil {
|
||||
query = query.Where("is_favorite = ?", *filters.IsFavorite)
|
||||
}
|
||||
if filters.ResidenceID != nil {
|
||||
query = query.Where("residence_id = ?", *filters.ResidenceID)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&contractors).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractors"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]dto.ContractorResponse, len(contractors))
|
||||
for i, contractor := range contractors {
|
||||
responses[i] = h.toContractorResponse(&contractor)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var contractor models.Contractor
|
||||
if err := h.db.
|
||||
Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("Specialties").
|
||||
First(&contractor, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Contractor not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractor"})
|
||||
return
|
||||
}
|
||||
|
||||
response := dto.ContractorDetailResponse{
|
||||
ContractorResponse: h.toContractorResponse(&contractor),
|
||||
}
|
||||
|
||||
// Get task count
|
||||
var taskCount int64
|
||||
h.db.Model(&models.Task{}).Where("contractor_id = ?", contractor.ID).Count(&taskCount)
|
||||
response.TaskCount = int(taskCount)
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/contractors/:id
|
||||
func (h *AdminContractorHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractor"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateContractorRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
contractor.Name = *req.Name
|
||||
}
|
||||
if req.Company != nil {
|
||||
contractor.Company = *req.Company
|
||||
}
|
||||
if req.Phone != nil {
|
||||
contractor.Phone = *req.Phone
|
||||
}
|
||||
if req.Email != nil {
|
||||
contractor.Email = *req.Email
|
||||
}
|
||||
if req.Website != nil {
|
||||
contractor.Website = *req.Website
|
||||
}
|
||||
if req.Notes != nil {
|
||||
contractor.Notes = *req.Notes
|
||||
}
|
||||
if req.IsFavorite != nil {
|
||||
contractor.IsFavorite = *req.IsFavorite
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
contractor.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.db.Save(&contractor).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update contractor"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Residence").Preload("CreatedBy").Preload("Specialties").First(&contractor, id)
|
||||
c.JSON(http.StatusOK, h.toContractorResponse(&contractor))
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/contractors
|
||||
func (h *AdminContractorHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateContractorRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
contractor := models.Contractor{
|
||||
ResidenceID: req.ResidenceID,
|
||||
CreatedByID: req.CreatedByID,
|
||||
Name: req.Name,
|
||||
Company: req.Company,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
Website: req.Website,
|
||||
Notes: req.Notes,
|
||||
StreetAddress: req.StreetAddress,
|
||||
City: req.City,
|
||||
StateProvince: req.StateProvince,
|
||||
PostalCode: req.PostalCode,
|
||||
IsFavorite: req.IsFavorite,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := h.db.Create(&contractor).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create contractor"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add specialties if provided
|
||||
if len(req.SpecialtyIDs) > 0 {
|
||||
var specialties []models.ContractorSpecialty
|
||||
h.db.Where("id IN ?", req.SpecialtyIDs).Find(&specialties)
|
||||
h.db.Model(&contractor).Association("Specialties").Append(specialties)
|
||||
}
|
||||
|
||||
h.db.Preload("Residence").Preload("CreatedBy").Preload("Specialties").First(&contractor, contractor.ID)
|
||||
c.JSON(http.StatusCreated, h.toContractorResponse(&contractor))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/contractors/:id
|
||||
func (h *AdminContractorHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractor"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Contractor deactivated successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/contractors/bulk
|
||||
func (h *AdminContractorHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Contractors deactivated successfully", "count": len(req.IDs)})
|
||||
}
|
||||
|
||||
func (h *AdminContractorHandler) toContractorResponse(contractor *models.Contractor) dto.ContractorResponse {
|
||||
response := dto.ContractorResponse{
|
||||
ID: contractor.ID,
|
||||
ResidenceID: contractor.ResidenceID,
|
||||
Name: contractor.Name,
|
||||
Company: contractor.Company,
|
||||
Phone: contractor.Phone,
|
||||
Email: contractor.Email,
|
||||
Website: contractor.Website,
|
||||
City: contractor.City,
|
||||
IsFavorite: contractor.IsFavorite,
|
||||
IsActive: contractor.IsActive,
|
||||
CreatedAt: contractor.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if contractor.Residence.ID != 0 {
|
||||
response.ResidenceName = contractor.Residence.Name
|
||||
}
|
||||
if contractor.Rating != nil {
|
||||
response.Rating = contractor.Rating
|
||||
}
|
||||
|
||||
// Add specialties
|
||||
for _, s := range contractor.Specialties {
|
||||
response.Specialties = append(response.Specialties, s.Name)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
142
internal/admin/handlers/dashboard_handler.go
Normal file
142
internal/admin/handlers/dashboard_handler.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminDashboardHandler handles admin dashboard endpoints
|
||||
type AdminDashboardHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminDashboardHandler creates a new admin dashboard handler
|
||||
func NewAdminDashboardHandler(db *gorm.DB) *AdminDashboardHandler {
|
||||
return &AdminDashboardHandler{db: db}
|
||||
}
|
||||
|
||||
// DashboardStats holds all dashboard statistics
|
||||
type DashboardStats struct {
|
||||
Users UserStats `json:"users"`
|
||||
Residences ResidenceStats `json:"residences"`
|
||||
Tasks TaskStats `json:"tasks"`
|
||||
Contractors ContractorStats `json:"contractors"`
|
||||
Documents DocumentStats `json:"documents"`
|
||||
Notifications NotificationStats `json:"notifications"`
|
||||
Subscriptions SubscriptionStats `json:"subscriptions"`
|
||||
}
|
||||
|
||||
// UserStats holds user-related statistics
|
||||
type UserStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
Verified int64 `json:"verified"`
|
||||
New30d int64 `json:"new_30d"`
|
||||
}
|
||||
|
||||
// ResidenceStats holds residence-related statistics
|
||||
type ResidenceStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
New30d int64 `json:"new_30d"`
|
||||
}
|
||||
|
||||
// TaskStats holds task-related statistics
|
||||
type TaskStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Pending int64 `json:"pending"`
|
||||
Completed int64 `json:"completed"`
|
||||
Overdue int64 `json:"overdue"`
|
||||
}
|
||||
|
||||
// ContractorStats holds contractor-related statistics
|
||||
type ContractorStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
Favorite int64 `json:"favorite"`
|
||||
}
|
||||
|
||||
// DocumentStats holds document-related statistics
|
||||
type DocumentStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
Expired int64 `json:"expired"`
|
||||
}
|
||||
|
||||
// NotificationStats holds notification-related statistics
|
||||
type NotificationStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Sent int64 `json:"sent"`
|
||||
Pending int64 `json:"pending"`
|
||||
Read int64 `json:"read"`
|
||||
}
|
||||
|
||||
// SubscriptionStats holds subscription-related statistics
|
||||
type SubscriptionStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Free int64 `json:"free"`
|
||||
Premium int64 `json:"premium"`
|
||||
Pro int64 `json:"pro"`
|
||||
}
|
||||
|
||||
// GetStats handles GET /api/admin/dashboard/stats
|
||||
func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
|
||||
stats := DashboardStats{}
|
||||
now := time.Now()
|
||||
thirtyDaysAgo := now.AddDate(0, 0, -30)
|
||||
|
||||
// User stats
|
||||
h.db.Model(&models.User{}).Count(&stats.Users.Total)
|
||||
h.db.Model(&models.User{}).Where("is_active = ?", true).Count(&stats.Users.Active)
|
||||
h.db.Model(&models.User{}).Where("verified = ?", true).Count(&stats.Users.Verified)
|
||||
h.db.Model(&models.User{}).Where("date_joined >= ?", thirtyDaysAgo).Count(&stats.Users.New30d)
|
||||
|
||||
// Residence stats
|
||||
h.db.Model(&models.Residence{}).Count(&stats.Residences.Total)
|
||||
h.db.Model(&models.Residence{}).Where("is_active = ?", true).Count(&stats.Residences.Active)
|
||||
h.db.Model(&models.Residence{}).Where("created_at >= ?", thirtyDaysAgo).Count(&stats.Residences.New30d)
|
||||
|
||||
// Task stats
|
||||
h.db.Model(&models.Task{}).Count(&stats.Tasks.Total)
|
||||
h.db.Model(&models.Task{}).Where("is_cancelled = ? AND is_archived = ?", false, false).
|
||||
Joins("JOIN task_taskstatus ON task_taskstatus.id = tasks.status_id").
|
||||
Where("task_taskstatus.name IN ?", []string{"pending", "in_progress"}).
|
||||
Count(&stats.Tasks.Pending)
|
||||
h.db.Model(&models.Task{}).
|
||||
Joins("JOIN task_taskstatus ON task_taskstatus.id = tasks.status_id").
|
||||
Where("task_taskstatus.name = ?", "completed").
|
||||
Count(&stats.Tasks.Completed)
|
||||
h.db.Model(&models.Task{}).Where("due_date < ? AND is_cancelled = ? AND is_archived = ?", now, false, false).
|
||||
Joins("JOIN task_taskstatus ON task_taskstatus.id = tasks.status_id").
|
||||
Where("task_taskstatus.name NOT IN ?", []string{"completed", "cancelled"}).
|
||||
Count(&stats.Tasks.Overdue)
|
||||
|
||||
// Contractor stats
|
||||
h.db.Model(&models.Contractor{}).Count(&stats.Contractors.Total)
|
||||
h.db.Model(&models.Contractor{}).Where("is_active = ?", true).Count(&stats.Contractors.Active)
|
||||
h.db.Model(&models.Contractor{}).Where("is_favorite = ?", true).Count(&stats.Contractors.Favorite)
|
||||
|
||||
// Document stats
|
||||
h.db.Model(&models.Document{}).Count(&stats.Documents.Total)
|
||||
h.db.Model(&models.Document{}).Where("is_active = ?", true).Count(&stats.Documents.Active)
|
||||
h.db.Model(&models.Document{}).Where("expiry_date < ? AND is_active = ?", now, true).Count(&stats.Documents.Expired)
|
||||
|
||||
// Notification stats
|
||||
h.db.Model(&models.Notification{}).Count(&stats.Notifications.Total)
|
||||
h.db.Model(&models.Notification{}).Where("sent = ?", true).Count(&stats.Notifications.Sent)
|
||||
h.db.Model(&models.Notification{}).Where("sent = ?", false).Count(&stats.Notifications.Pending)
|
||||
h.db.Model(&models.Notification{}).Where("read = ?", true).Count(&stats.Notifications.Read)
|
||||
|
||||
// Subscription stats
|
||||
h.db.Model(&models.UserSubscription{}).Count(&stats.Subscriptions.Total)
|
||||
h.db.Model(&models.UserSubscription{}).Where("tier = ?", "free").Count(&stats.Subscriptions.Free)
|
||||
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)
|
||||
}
|
||||
315
internal/admin/handlers/document_handler.go
Normal file
315
internal/admin/handlers/document_handler.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminDocumentHandler handles admin document management endpoints
|
||||
type AdminDocumentHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminDocumentHandler creates a new admin document handler
|
||||
func NewAdminDocumentHandler(db *gorm.DB) *AdminDocumentHandler {
|
||||
return &AdminDocumentHandler{db: db}
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/documents
|
||||
func (h *AdminDocumentHandler) List(c *gin.Context) {
|
||||
var filters dto.DocumentFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var documents []models.Document
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.Document{}).
|
||||
Preload("Residence").
|
||||
Preload("CreatedBy")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where(
|
||||
"title ILIKE ? OR description ILIKE ? OR vendor ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *filters.IsActive)
|
||||
}
|
||||
if filters.ResidenceID != nil {
|
||||
query = query.Where("residence_id = ?", *filters.ResidenceID)
|
||||
}
|
||||
if filters.DocumentType != nil {
|
||||
query = query.Where("document_type = ?", *filters.DocumentType)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&documents).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch documents"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]dto.DocumentResponse, len(documents))
|
||||
for i, doc := range documents {
|
||||
responses[i] = h.toDocumentResponse(&doc)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var document models.Document
|
||||
if err := h.db.
|
||||
Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("Task").
|
||||
First(&document, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document"})
|
||||
return
|
||||
}
|
||||
|
||||
response := dto.DocumentDetailResponse{
|
||||
DocumentResponse: h.toDocumentResponse(&document),
|
||||
}
|
||||
|
||||
if document.Task != nil {
|
||||
response.TaskTitle = &document.Task.Title
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/documents/:id
|
||||
func (h *AdminDocumentHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateDocumentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
document.Title = *req.Title
|
||||
}
|
||||
if req.Description != nil {
|
||||
document.Description = *req.Description
|
||||
}
|
||||
if req.Vendor != nil {
|
||||
document.Vendor = *req.Vendor
|
||||
}
|
||||
if req.SerialNumber != nil {
|
||||
document.SerialNumber = *req.SerialNumber
|
||||
}
|
||||
if req.ModelNumber != nil {
|
||||
document.ModelNumber = *req.ModelNumber
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
document.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.db.Save(&document).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update document"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Residence").Preload("CreatedBy").First(&document, id)
|
||||
c.JSON(http.StatusOK, h.toDocumentResponse(&document))
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/documents
|
||||
func (h *AdminDocumentHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateDocumentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
documentType := models.DocumentTypeGeneral
|
||||
if req.DocumentType != "" {
|
||||
documentType = models.DocumentType(req.DocumentType)
|
||||
}
|
||||
|
||||
document := models.Document{
|
||||
ResidenceID: req.ResidenceID,
|
||||
CreatedByID: req.CreatedByID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
DocumentType: documentType,
|
||||
FileURL: req.FileURL,
|
||||
FileName: req.FileName,
|
||||
FileSize: req.FileSize,
|
||||
MimeType: req.MimeType,
|
||||
Vendor: req.Vendor,
|
||||
SerialNumber: req.SerialNumber,
|
||||
ModelNumber: req.ModelNumber,
|
||||
TaskID: req.TaskID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if req.PurchaseDate != nil {
|
||||
purchaseDate, err := time.Parse("2006-01-02", *req.PurchaseDate)
|
||||
if err == nil {
|
||||
document.PurchaseDate = &purchaseDate
|
||||
}
|
||||
}
|
||||
if req.ExpiryDate != nil {
|
||||
expiryDate, err := time.Parse("2006-01-02", *req.ExpiryDate)
|
||||
if err == nil {
|
||||
document.ExpiryDate = &expiryDate
|
||||
}
|
||||
}
|
||||
if req.PurchasePrice != nil {
|
||||
d := decimal.NewFromFloat(*req.PurchasePrice)
|
||||
document.PurchasePrice = &d
|
||||
}
|
||||
|
||||
if err := h.db.Create(&document).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create document"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Residence").Preload("CreatedBy").First(&document, document.ID)
|
||||
c.JSON(http.StatusCreated, h.toDocumentResponse(&document))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/documents/:id
|
||||
func (h *AdminDocumentHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch document"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Document deactivated successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/documents/bulk
|
||||
func (h *AdminDocumentHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Documents deactivated successfully", "count": len(req.IDs)})
|
||||
}
|
||||
|
||||
func (h *AdminDocumentHandler) toDocumentResponse(doc *models.Document) dto.DocumentResponse {
|
||||
response := dto.DocumentResponse{
|
||||
ID: doc.ID,
|
||||
ResidenceID: doc.ResidenceID,
|
||||
Title: doc.Title,
|
||||
Description: doc.Description,
|
||||
DocumentType: string(doc.DocumentType),
|
||||
FileName: doc.FileName,
|
||||
FileURL: doc.FileURL,
|
||||
Vendor: doc.Vendor,
|
||||
IsActive: doc.IsActive,
|
||||
CreatedAt: doc.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if doc.Residence.ID != 0 {
|
||||
response.ResidenceName = doc.Residence.Name
|
||||
}
|
||||
if doc.ExpiryDate != nil {
|
||||
expiryDate := doc.ExpiryDate.Format("2006-01-02")
|
||||
response.ExpiryDate = &expiryDate
|
||||
}
|
||||
if doc.PurchaseDate != nil {
|
||||
purchaseDate := doc.PurchaseDate.Format("2006-01-02")
|
||||
response.PurchaseDate = &purchaseDate
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
802
internal/admin/handlers/lookup_handler.go
Normal file
802
internal/admin/handlers/lookup_handler.go
Normal file
@@ -0,0 +1,802 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminLookupHandler handles admin lookup table management endpoints
|
||||
type AdminLookupHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminLookupHandler creates a new admin lookup handler
|
||||
func NewAdminLookupHandler(db *gorm.DB) *AdminLookupHandler {
|
||||
return &AdminLookupHandler{db: db}
|
||||
}
|
||||
|
||||
// ========== Task Categories ==========
|
||||
|
||||
type TaskCategoryResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
}
|
||||
|
||||
type CreateUpdateCategoryRequest struct {
|
||||
Name string `json:"name" binding:"required,max=50"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon" binding:"max=50"`
|
||||
Color string `json:"color" binding:"max=7"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) ListCategories(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
responses := make([]TaskCategoryResponse, len(categories))
|
||||
for i, cat := range categories {
|
||||
responses[i] = TaskCategoryResponse{
|
||||
ID: cat.ID,
|
||||
Name: cat.Name,
|
||||
Description: cat.Description,
|
||||
Icon: cat.Icon,
|
||||
Color: cat.Color,
|
||||
DisplayOrder: cat.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) CreateCategory(c *gin.Context) {
|
||||
var req CreateUpdateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
category := models.TaskCategory{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Icon: req.Icon,
|
||||
Color: req.Color,
|
||||
}
|
||||
if req.DisplayOrder != nil {
|
||||
category.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Create(&category).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create category"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, TaskCategoryResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
Icon: category.Icon,
|
||||
Color: category.Color,
|
||||
DisplayOrder: category.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) UpdateCategory(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch category"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateUpdateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
category.Name = req.Name
|
||||
category.Description = req.Description
|
||||
category.Icon = req.Icon
|
||||
category.Color = req.Color
|
||||
if req.DisplayOrder != nil {
|
||||
category.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Save(&category).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update category"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TaskCategoryResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
Icon: category.Icon,
|
||||
Color: category.Color,
|
||||
DisplayOrder: category.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) DeleteCategory(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.TaskCategory{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete category"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
|
||||
}
|
||||
|
||||
// ========== Task Priorities ==========
|
||||
|
||||
type TaskPriorityResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Level int `json:"level"`
|
||||
Color string `json:"color"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
}
|
||||
|
||||
type CreateUpdatePriorityRequest struct {
|
||||
Name string `json:"name" binding:"required,max=20"`
|
||||
Level int `json:"level" binding:"required,min=1,max=10"`
|
||||
Color string `json:"color" binding:"max=7"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) ListPriorities(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
responses := make([]TaskPriorityResponse, len(priorities))
|
||||
for i, p := range priorities {
|
||||
responses[i] = TaskPriorityResponse{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Level: p.Level,
|
||||
Color: p.Color,
|
||||
DisplayOrder: p.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) CreatePriority(c *gin.Context) {
|
||||
var req CreateUpdatePriorityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
priority := models.TaskPriority{
|
||||
Name: req.Name,
|
||||
Level: req.Level,
|
||||
Color: req.Color,
|
||||
}
|
||||
if req.DisplayOrder != nil {
|
||||
priority.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Create(&priority).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create priority"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, TaskPriorityResponse{
|
||||
ID: priority.ID,
|
||||
Name: priority.Name,
|
||||
Level: priority.Level,
|
||||
Color: priority.Color,
|
||||
DisplayOrder: priority.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) UpdatePriority(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid priority ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch priority"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateUpdatePriorityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
priority.Name = req.Name
|
||||
priority.Level = req.Level
|
||||
priority.Color = req.Color
|
||||
if req.DisplayOrder != nil {
|
||||
priority.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Save(&priority).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update priority"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TaskPriorityResponse{
|
||||
ID: priority.ID,
|
||||
Name: priority.Name,
|
||||
Level: priority.Level,
|
||||
Color: priority.Color,
|
||||
DisplayOrder: priority.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) DeletePriority(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid priority ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.TaskPriority{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete priority"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Priority deleted successfully"})
|
||||
}
|
||||
|
||||
// ========== Task Statuses ==========
|
||||
|
||||
type TaskStatusResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
}
|
||||
|
||||
type CreateUpdateStatusRequest struct {
|
||||
Name string `json:"name" binding:"required,max=20"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color" binding:"max=7"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) ListStatuses(c *gin.Context) {
|
||||
var statuses []models.TaskStatus
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch statuses"})
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]TaskStatusResponse, len(statuses))
|
||||
for i, s := range statuses {
|
||||
responses[i] = TaskStatusResponse{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Description: s.Description,
|
||||
Color: s.Color,
|
||||
DisplayOrder: s.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) CreateStatus(c *gin.Context) {
|
||||
var req CreateUpdateStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
status := models.TaskStatus{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Color: req.Color,
|
||||
}
|
||||
if req.DisplayOrder != nil {
|
||||
status.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Create(&status).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create status"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, TaskStatusResponse{
|
||||
ID: status.ID,
|
||||
Name: status.Name,
|
||||
Description: status.Description,
|
||||
Color: status.Color,
|
||||
DisplayOrder: status.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) UpdateStatus(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var status models.TaskStatus
|
||||
if err := h.db.First(&status, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Status not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch status"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateUpdateStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
status.Name = req.Name
|
||||
status.Description = req.Description
|
||||
status.Color = req.Color
|
||||
if req.DisplayOrder != nil {
|
||||
status.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Save(&status).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update status"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TaskStatusResponse{
|
||||
ID: status.ID,
|
||||
Name: status.Name,
|
||||
Description: status.Description,
|
||||
Color: status.Color,
|
||||
DisplayOrder: status.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) DeleteStatus(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
h.db.Model(&models.Task{}).Where("status_id = ?", id).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete status that is in use by tasks"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.TaskStatus{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete status"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Status deleted successfully"})
|
||||
}
|
||||
|
||||
// ========== Task Frequencies ==========
|
||||
|
||||
type TaskFrequencyResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Days *int `json:"days"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
}
|
||||
|
||||
type CreateUpdateFrequencyRequest struct {
|
||||
Name string `json:"name" binding:"required,max=20"`
|
||||
Days *int `json:"days"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) ListFrequencies(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
responses := make([]TaskFrequencyResponse, len(frequencies))
|
||||
for i, f := range frequencies {
|
||||
responses[i] = TaskFrequencyResponse{
|
||||
ID: f.ID,
|
||||
Name: f.Name,
|
||||
Days: f.Days,
|
||||
DisplayOrder: f.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) CreateFrequency(c *gin.Context) {
|
||||
var req CreateUpdateFrequencyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
frequency := models.TaskFrequency{
|
||||
Name: req.Name,
|
||||
Days: req.Days,
|
||||
}
|
||||
if req.DisplayOrder != nil {
|
||||
frequency.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Create(&frequency).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create frequency"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, TaskFrequencyResponse{
|
||||
ID: frequency.ID,
|
||||
Name: frequency.Name,
|
||||
Days: frequency.Days,
|
||||
DisplayOrder: frequency.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) UpdateFrequency(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid frequency ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch frequency"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateUpdateFrequencyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
frequency.Name = req.Name
|
||||
frequency.Days = req.Days
|
||||
if req.DisplayOrder != nil {
|
||||
frequency.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Save(&frequency).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update frequency"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TaskFrequencyResponse{
|
||||
ID: frequency.ID,
|
||||
Name: frequency.Name,
|
||||
Days: frequency.Days,
|
||||
DisplayOrder: frequency.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) DeleteFrequency(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid frequency ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.TaskFrequency{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete frequency"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Frequency deleted successfully"})
|
||||
}
|
||||
|
||||
// ========== Residence Types ==========
|
||||
|
||||
type ResidenceTypeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CreateUpdateResidenceTypeRequest struct {
|
||||
Name string `json:"name" binding:"required,max=20"`
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) ListResidenceTypes(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
responses := make([]ResidenceTypeResponse, len(types))
|
||||
for i, t := range types {
|
||||
responses[i] = ResidenceTypeResponse{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) CreateResidenceType(c *gin.Context) {
|
||||
var req CreateUpdateResidenceTypeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, ResidenceTypeResponse{
|
||||
ID: residenceType.ID,
|
||||
Name: residenceType.Name,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) UpdateResidenceType(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence type ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence type"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateUpdateResidenceTypeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ResidenceTypeResponse{
|
||||
ID: residenceType.ID,
|
||||
Name: residenceType.Name,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) DeleteResidenceType(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence type ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.ResidenceType{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residence type"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Residence type deleted successfully"})
|
||||
}
|
||||
|
||||
// ========== Contractor Specialties ==========
|
||||
|
||||
type ContractorSpecialtyResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
}
|
||||
|
||||
type CreateUpdateSpecialtyRequest struct {
|
||||
Name string `json:"name" binding:"required,max=50"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon" binding:"max=50"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) ListSpecialties(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
responses := make([]ContractorSpecialtyResponse, len(specialties))
|
||||
for i, s := range specialties {
|
||||
responses[i] = ContractorSpecialtyResponse{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Description: s.Description,
|
||||
Icon: s.Icon,
|
||||
DisplayOrder: s.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) CreateSpecialty(c *gin.Context) {
|
||||
var req CreateUpdateSpecialtyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
specialty := models.ContractorSpecialty{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Icon: req.Icon,
|
||||
}
|
||||
if req.DisplayOrder != nil {
|
||||
specialty.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Create(&specialty).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create specialty"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, ContractorSpecialtyResponse{
|
||||
ID: specialty.ID,
|
||||
Name: specialty.Name,
|
||||
Description: specialty.Description,
|
||||
Icon: specialty.Icon,
|
||||
DisplayOrder: specialty.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) UpdateSpecialty(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid specialty ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch specialty"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateUpdateSpecialtyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
specialty.Name = req.Name
|
||||
specialty.Description = req.Description
|
||||
specialty.Icon = req.Icon
|
||||
if req.DisplayOrder != nil {
|
||||
specialty.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
if err := h.db.Save(&specialty).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update specialty"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ContractorSpecialtyResponse{
|
||||
ID: specialty.ID,
|
||||
Name: specialty.Name,
|
||||
Description: specialty.Description,
|
||||
Icon: specialty.Icon,
|
||||
DisplayOrder: specialty.DisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminLookupHandler) DeleteSpecialty(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid specialty ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.ContractorSpecialty{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete specialty"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Specialty deleted successfully"})
|
||||
}
|
||||
|
||||
// Ensure dto import is used
|
||||
var _ = dto.PaginationParams{}
|
||||
404
internal/admin/handlers/notification_handler.go
Normal file
404
internal/admin/handlers/notification_handler.go
Normal file
@@ -0,0 +1,404 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
"github.com/treytartt/mycrib-api/internal/push"
|
||||
"github.com/treytartt/mycrib-api/internal/services"
|
||||
)
|
||||
|
||||
// AdminNotificationHandler handles admin notification management endpoints
|
||||
type AdminNotificationHandler struct {
|
||||
db *gorm.DB
|
||||
emailService *services.EmailService
|
||||
pushClient *push.GorushClient
|
||||
}
|
||||
|
||||
// NewAdminNotificationHandler creates a new admin notification handler
|
||||
func NewAdminNotificationHandler(db *gorm.DB, emailService *services.EmailService, pushClient *push.GorushClient) *AdminNotificationHandler {
|
||||
return &AdminNotificationHandler{
|
||||
db: db,
|
||||
emailService: emailService,
|
||||
pushClient: pushClient,
|
||||
}
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/notifications
|
||||
func (h *AdminNotificationHandler) List(c *gin.Context) {
|
||||
var filters dto.NotificationFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var notifications []models.Notification
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.Notification{}).
|
||||
Preload("User")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where("title ILIKE ? OR body ILIKE ?", search, search)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.UserID != nil {
|
||||
query = query.Where("user_id = ?", *filters.UserID)
|
||||
}
|
||||
if filters.NotificationType != nil {
|
||||
query = query.Where("notification_type = ?", *filters.NotificationType)
|
||||
}
|
||||
if filters.Sent != nil {
|
||||
query = query.Where("sent = ?", *filters.Sent)
|
||||
}
|
||||
if filters.Read != nil {
|
||||
query = query.Where("read = ?", *filters.Read)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(¬ifications).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notifications"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]dto.NotificationResponse, len(notifications))
|
||||
for i, notif := range notifications {
|
||||
responses[i] = h.toNotificationResponse(¬if)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var notification models.Notification
|
||||
if err := h.db.
|
||||
Preload("User").
|
||||
First(¬ification, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Notification not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, h.toNotificationDetailResponse(¬ification))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/notifications/:id
|
||||
func (h *AdminNotificationHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var notification models.Notification
|
||||
if err := h.db.First(¬ification, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Notification not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hard delete notifications
|
||||
if err := h.db.Delete(¬ification).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Notification deleted successfully"})
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/notifications/:id
|
||||
func (h *AdminNotificationHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var notification models.Notification
|
||||
if err := h.db.First(¬ification, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Notification not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateNotificationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
if req.Title != nil {
|
||||
notification.Title = *req.Title
|
||||
}
|
||||
if req.Body != nil {
|
||||
notification.Body = *req.Body
|
||||
}
|
||||
if req.Read != nil {
|
||||
notification.Read = *req.Read
|
||||
if *req.Read && notification.ReadAt == nil {
|
||||
notification.ReadAt = &now
|
||||
} else if !*req.Read {
|
||||
notification.ReadAt = nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.db.Save(¬ification).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("User").First(¬ification, id)
|
||||
c.JSON(http.StatusOK, h.toNotificationResponse(¬ification))
|
||||
}
|
||||
|
||||
// GetStats handles GET /api/admin/notifications/stats
|
||||
func (h *AdminNotificationHandler) GetStats(c *gin.Context) {
|
||||
var total, sent, read, pending int64
|
||||
|
||||
h.db.Model(&models.Notification{}).Count(&total)
|
||||
h.db.Model(&models.Notification{}).Where("sent = ?", true).Count(&sent)
|
||||
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{
|
||||
"total": total,
|
||||
"sent": sent,
|
||||
"read": read,
|
||||
"pending": pending,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminNotificationHandler) toNotificationResponse(notif *models.Notification) dto.NotificationResponse {
|
||||
response := dto.NotificationResponse{
|
||||
ID: notif.ID,
|
||||
UserID: notif.UserID,
|
||||
NotificationType: string(notif.NotificationType),
|
||||
Title: notif.Title,
|
||||
Body: notif.Body,
|
||||
Sent: notif.Sent,
|
||||
Read: notif.Read,
|
||||
CreatedAt: notif.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if notif.User.ID != 0 {
|
||||
response.UserEmail = notif.User.Email
|
||||
response.Username = notif.User.Username
|
||||
}
|
||||
if notif.SentAt != nil {
|
||||
sentAt := notif.SentAt.Format("2006-01-02T15:04:05Z")
|
||||
response.SentAt = &sentAt
|
||||
}
|
||||
if notif.ReadAt != nil {
|
||||
readAt := notif.ReadAt.Format("2006-01-02T15:04:05Z")
|
||||
response.ReadAt = &readAt
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *AdminNotificationHandler) toNotificationDetailResponse(notif *models.Notification) dto.NotificationDetailResponse {
|
||||
return dto.NotificationDetailResponse{
|
||||
NotificationResponse: h.toNotificationResponse(notif),
|
||||
Data: notif.Data,
|
||||
TaskID: notif.TaskID,
|
||||
}
|
||||
}
|
||||
|
||||
// SendTestNotification handles POST /api/admin/notifications/send-test
|
||||
func (h *AdminNotificationHandler) SendTestNotification(c *gin.Context) {
|
||||
var req dto.SendTestNotificationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user's device tokens
|
||||
var iosDevices []models.APNSDevice
|
||||
var androidDevices []models.GCMDevice
|
||||
|
||||
h.db.Where("user_id = ? AND active = ?", req.UserID, true).Find(&iosDevices)
|
||||
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
|
||||
}
|
||||
|
||||
// Create notification record
|
||||
now := time.Now().UTC()
|
||||
notification := models.Notification{
|
||||
UserID: req.UserID,
|
||||
NotificationType: models.NotificationType("test"),
|
||||
Title: req.Title,
|
||||
Body: req.Body,
|
||||
Data: `{"test": true}`,
|
||||
Sent: false,
|
||||
}
|
||||
|
||||
if err := h.db.Create(¬ification).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create notification record"})
|
||||
return
|
||||
}
|
||||
|
||||
// Collect tokens
|
||||
var iosTokens, androidTokens []string
|
||||
for _, d := range iosDevices {
|
||||
iosTokens = append(iosTokens, d.RegistrationID)
|
||||
}
|
||||
for _, d := range androidDevices {
|
||||
androidTokens = append(androidTokens, d.RegistrationID)
|
||||
}
|
||||
|
||||
// Send push notification
|
||||
if h.pushClient != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pushData := map[string]string{
|
||||
"notification_id": strconv.FormatUint(uint64(notification.ID), 10),
|
||||
"test": "true",
|
||||
}
|
||||
|
||||
err := h.pushClient.SendToAll(ctx, iosTokens, androidTokens, req.Title, req.Body, pushData)
|
||||
if err != nil {
|
||||
// Update notification with error
|
||||
h.db.Model(¬ification).Updates(map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to send push notification",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Push notification service not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
// Mark as sent
|
||||
h.db.Model(¬ification).Updates(map[string]interface{}{
|
||||
"sent": true,
|
||||
"sent_at": now,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Test notification sent successfully",
|
||||
"notification_id": notification.ID,
|
||||
"devices": gin.H{
|
||||
"ios": len(iosTokens),
|
||||
"android": len(androidTokens),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SendTestEmail handles POST /api/admin/emails/send-test
|
||||
func (h *AdminNotificationHandler) SendTestEmail(c *gin.Context) {
|
||||
var req dto.SendTestEmailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
|
||||
return
|
||||
}
|
||||
|
||||
if user.Email == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "User has no email address"})
|
||||
return
|
||||
}
|
||||
|
||||
// Send email
|
||||
if h.emailService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Email service not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create HTML body with basic styling
|
||||
htmlBody := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>` + req.Subject + `</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #333;">` + req.Subject + `</h2>
|
||||
<div style="color: #666; line-height: 1.6;">` + req.Body + `</div>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
|
||||
<p style="color: #999; font-size: 12px;">This is a test email sent from MyCrib Admin Panel.</p>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
err := h.emailService.SendEmail(user.Email, req.Subject, htmlBody, req.Body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to send email",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Test email sent successfully",
|
||||
"to": user.Email,
|
||||
})
|
||||
}
|
||||
290
internal/admin/handlers/notification_prefs_handler.go
Normal file
290
internal/admin/handlers/notification_prefs_handler.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminNotificationPrefsHandler handles notification preference management
|
||||
type AdminNotificationPrefsHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminNotificationPrefsHandler creates a new handler
|
||||
func NewAdminNotificationPrefsHandler(db *gorm.DB) *AdminNotificationPrefsHandler {
|
||||
return &AdminNotificationPrefsHandler{db: db}
|
||||
}
|
||||
|
||||
// NotificationPrefResponse represents a notification preference in the admin API
|
||||
type NotificationPrefResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
TaskDueSoon bool `json:"task_due_soon"`
|
||||
TaskOverdue bool `json:"task_overdue"`
|
||||
TaskCompleted bool `json:"task_completed"`
|
||||
TaskAssigned bool `json:"task_assigned"`
|
||||
ResidenceShared bool `json:"residence_shared"`
|
||||
WarrantyExpiring bool `json:"warranty_expiring"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/notification-prefs
|
||||
func (h *AdminNotificationPrefsHandler) List(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var prefs []models.NotificationPreference
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.NotificationPreference{})
|
||||
|
||||
// Apply search on user data via subquery
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where("user_id IN (?)",
|
||||
h.db.Model(&models.User{}).Select("id").Where(
|
||||
"username ILIKE ? OR email ILIKE ?", search, search,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&prefs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preferences"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user info for each preference
|
||||
userIDs := make([]uint, len(prefs))
|
||||
for i, p := range prefs {
|
||||
userIDs[i] = p.UserID
|
||||
}
|
||||
|
||||
var users []models.User
|
||||
userMap := make(map[uint]models.User)
|
||||
if len(userIDs) > 0 {
|
||||
h.db.Where("id IN ?", userIDs).Find(&users)
|
||||
for _, u := range users {
|
||||
userMap[u.ID] = u
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]NotificationPrefResponse, len(prefs))
|
||||
for i, pref := range prefs {
|
||||
user := userMap[pref.UserID]
|
||||
responses[i] = NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preference"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
h.db.First(&user, pref.UserID)
|
||||
|
||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateNotificationPrefRequest represents an update request
|
||||
type UpdateNotificationPrefRequest struct {
|
||||
TaskDueSoon *bool `json:"task_due_soon"`
|
||||
TaskOverdue *bool `json:"task_overdue"`
|
||||
TaskCompleted *bool `json:"task_completed"`
|
||||
TaskAssigned *bool `json:"task_assigned"`
|
||||
ResidenceShared *bool `json:"residence_shared"`
|
||||
WarrantyExpiring *bool `json:"warranty_expiring"`
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/notification-prefs/:id
|
||||
func (h *AdminNotificationPrefsHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preference"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateNotificationPrefRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
if req.TaskDueSoon != nil {
|
||||
pref.TaskDueSoon = *req.TaskDueSoon
|
||||
}
|
||||
if req.TaskOverdue != nil {
|
||||
pref.TaskOverdue = *req.TaskOverdue
|
||||
}
|
||||
if req.TaskCompleted != nil {
|
||||
pref.TaskCompleted = *req.TaskCompleted
|
||||
}
|
||||
if req.TaskAssigned != nil {
|
||||
pref.TaskAssigned = *req.TaskAssigned
|
||||
}
|
||||
if req.ResidenceShared != nil {
|
||||
pref.ResidenceShared = *req.ResidenceShared
|
||||
}
|
||||
if req.WarrantyExpiring != nil {
|
||||
pref.WarrantyExpiring = *req.WarrantyExpiring
|
||||
}
|
||||
|
||||
if err := h.db.Save(&pref).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification preference"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
h.db.First(&user, pref.UserID)
|
||||
|
||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/notification-prefs/:id
|
||||
func (h *AdminNotificationPrefsHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.NotificationPreference{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preference"})
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Notification preference not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Notification preference deleted"})
|
||||
}
|
||||
|
||||
// GetByUser handles GET /api/admin/notification-prefs/user/:user_id
|
||||
func (h *AdminNotificationPrefsHandler) GetByUser(c *gin.Context) {
|
||||
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch notification preference"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
h.db.First(&user, pref.UserID)
|
||||
|
||||
c.JSON(http.StatusOK, NotificationPrefResponse{
|
||||
ID: pref.ID,
|
||||
UserID: pref.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
TaskDueSoon: pref.TaskDueSoon,
|
||||
TaskOverdue: pref.TaskOverdue,
|
||||
TaskCompleted: pref.TaskCompleted,
|
||||
TaskAssigned: pref.TaskAssigned,
|
||||
ResidenceShared: pref.ResidenceShared,
|
||||
WarrantyExpiring: pref.WarrantyExpiring,
|
||||
CreatedAt: pref.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: pref.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
321
internal/admin/handlers/residence_handler.go
Normal file
321
internal/admin/handlers/residence_handler.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminResidenceHandler handles admin residence management endpoints
|
||||
type AdminResidenceHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminResidenceHandler creates a new admin residence handler
|
||||
func NewAdminResidenceHandler(db *gorm.DB) *AdminResidenceHandler {
|
||||
return &AdminResidenceHandler{db: db}
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/residences
|
||||
func (h *AdminResidenceHandler) List(c *gin.Context) {
|
||||
var filters dto.ResidenceFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var residences []models.Residence
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.Residence{}).
|
||||
Preload("Owner").
|
||||
Preload("PropertyType")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where(
|
||||
"name ILIKE ? OR street_address ILIKE ? OR city ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *filters.IsActive)
|
||||
}
|
||||
if filters.OwnerID != nil {
|
||||
query = query.Where("owner_id = ?", *filters.OwnerID)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&residences).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residences"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]dto.ResidenceResponse, len(residences))
|
||||
for i, res := range residences {
|
||||
responses[i] = h.toResidenceResponse(&res)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence"})
|
||||
return
|
||||
}
|
||||
|
||||
response := dto.ResidenceDetailResponse{
|
||||
ResidenceResponse: h.toResidenceResponse(&residence),
|
||||
}
|
||||
|
||||
// Add owner info
|
||||
response.Owner = &dto.UserSummary{
|
||||
ID: residence.Owner.ID,
|
||||
Username: residence.Owner.Username,
|
||||
Email: residence.Owner.Email,
|
||||
}
|
||||
|
||||
// Add shared users
|
||||
for _, user := range residence.Users {
|
||||
response.SharedUsers = append(response.SharedUsers, dto.UserSummary{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
// Get counts
|
||||
var taskCount, documentCount int64
|
||||
h.db.Model(&models.Task{}).Where("residence_id = ?", residence.ID).Count(&taskCount)
|
||||
h.db.Model(&models.Document{}).Where("residence_id = ?", residence.ID).Count(&documentCount)
|
||||
response.TaskCount = int(taskCount)
|
||||
response.DocumentCount = int(documentCount)
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/residences/:id
|
||||
func (h *AdminResidenceHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateResidenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
residence.Name = *req.Name
|
||||
}
|
||||
if req.StreetAddress != nil {
|
||||
residence.StreetAddress = *req.StreetAddress
|
||||
}
|
||||
if req.City != nil {
|
||||
residence.City = *req.City
|
||||
}
|
||||
if req.StateProvince != nil {
|
||||
residence.StateProvince = *req.StateProvince
|
||||
}
|
||||
if req.PostalCode != nil {
|
||||
residence.PostalCode = *req.PostalCode
|
||||
}
|
||||
if req.Country != nil {
|
||||
residence.Country = *req.Country
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
residence.IsActive = *req.IsActive
|
||||
}
|
||||
if req.IsPrimary != nil {
|
||||
residence.IsPrimary = *req.IsPrimary
|
||||
}
|
||||
|
||||
if err := h.db.Save(&residence).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update residence"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Owner").Preload("PropertyType").First(&residence, id)
|
||||
c.JSON(http.StatusOK, h.toResidenceResponse(&residence))
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/residences
|
||||
func (h *AdminResidenceHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateResidenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
residence := models.Residence{
|
||||
OwnerID: req.OwnerID,
|
||||
Name: req.Name,
|
||||
PropertyTypeID: req.PropertyTypeID,
|
||||
StreetAddress: req.StreetAddress,
|
||||
ApartmentUnit: req.ApartmentUnit,
|
||||
City: req.City,
|
||||
StateProvince: req.StateProvince,
|
||||
PostalCode: req.PostalCode,
|
||||
Country: req.Country,
|
||||
Description: req.Description,
|
||||
IsPrimary: req.IsPrimary,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if req.Bedrooms != nil {
|
||||
residence.Bedrooms = req.Bedrooms
|
||||
}
|
||||
if req.Bathrooms != nil {
|
||||
d := decimal.NewFromFloat(*req.Bathrooms)
|
||||
residence.Bathrooms = &d
|
||||
}
|
||||
if req.SquareFootage != nil {
|
||||
residence.SquareFootage = req.SquareFootage
|
||||
}
|
||||
if req.YearBuilt != nil {
|
||||
residence.YearBuilt = req.YearBuilt
|
||||
}
|
||||
|
||||
if err := h.db.Create(&residence).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create residence"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Owner").Preload("PropertyType").First(&residence, residence.ID)
|
||||
c.JSON(http.StatusCreated, h.toResidenceResponse(&residence))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/residences/:id
|
||||
func (h *AdminResidenceHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Residence deactivated successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/residences/bulk
|
||||
func (h *AdminResidenceHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Residences deactivated successfully", "count": len(req.IDs)})
|
||||
}
|
||||
|
||||
func (h *AdminResidenceHandler) toResidenceResponse(res *models.Residence) dto.ResidenceResponse {
|
||||
response := dto.ResidenceResponse{
|
||||
ID: res.ID,
|
||||
Name: res.Name,
|
||||
OwnerID: res.OwnerID,
|
||||
StreetAddress: res.StreetAddress,
|
||||
City: res.City,
|
||||
StateProvince: res.StateProvince,
|
||||
PostalCode: res.PostalCode,
|
||||
Country: res.Country,
|
||||
IsPrimary: res.IsPrimary,
|
||||
IsActive: res.IsActive,
|
||||
CreatedAt: res.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if res.Owner.ID != 0 {
|
||||
response.OwnerName = res.Owner.Username
|
||||
}
|
||||
if res.PropertyType != nil {
|
||||
response.PropertyType = &res.PropertyType.Name
|
||||
}
|
||||
if res.Bedrooms != nil {
|
||||
response.Bedrooms = res.Bedrooms
|
||||
}
|
||||
if res.Bathrooms != nil {
|
||||
f, _ := res.Bathrooms.Float64()
|
||||
response.Bathrooms = &f
|
||||
}
|
||||
if res.SquareFootage != nil {
|
||||
response.SquareFootage = res.SquareFootage
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
207
internal/admin/handlers/subscription_handler.go
Normal file
207
internal/admin/handlers/subscription_handler.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminSubscriptionHandler handles admin subscription management endpoints
|
||||
type AdminSubscriptionHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminSubscriptionHandler creates a new admin subscription handler
|
||||
func NewAdminSubscriptionHandler(db *gorm.DB) *AdminSubscriptionHandler {
|
||||
return &AdminSubscriptionHandler{db: db}
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/subscriptions
|
||||
func (h *AdminSubscriptionHandler) List(c *gin.Context) {
|
||||
var filters dto.SubscriptionFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var subscriptions []models.UserSubscription
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.UserSubscription{}).
|
||||
Preload("User")
|
||||
|
||||
// Apply search (search by user email)
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("JOIN users ON users.id = subscription_usersubscription.user_id").
|
||||
Where("users.email ILIKE ? OR users.username ILIKE ?", search, search)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.UserID != nil {
|
||||
query = query.Where("user_id = ?", *filters.UserID)
|
||||
}
|
||||
if filters.Tier != nil {
|
||||
query = query.Where("tier = ?", *filters.Tier)
|
||||
}
|
||||
if filters.Platform != nil {
|
||||
query = query.Where("platform = ?", *filters.Platform)
|
||||
}
|
||||
if filters.AutoRenew != nil {
|
||||
query = query.Where("auto_renew = ?", *filters.AutoRenew)
|
||||
}
|
||||
if filters.Active != nil {
|
||||
if *filters.Active {
|
||||
query = query.Where("expires_at IS NULL OR expires_at > NOW()")
|
||||
} else {
|
||||
query = query.Where("expires_at IS NOT NULL AND expires_at <= NOW()")
|
||||
}
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&subscriptions).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscriptions"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]dto.SubscriptionResponse, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
responses[i] = h.toSubscriptionResponse(&sub)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subscription ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var subscription models.UserSubscription
|
||||
if err := h.db.
|
||||
Preload("User").
|
||||
First(&subscription, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Subscription not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscription"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, h.toSubscriptionDetailResponse(&subscription))
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/subscriptions/:id
|
||||
func (h *AdminSubscriptionHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subscription ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscription"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateSubscriptionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Tier != nil {
|
||||
subscription.Tier = models.SubscriptionTier(*req.Tier)
|
||||
}
|
||||
if req.AutoRenew != nil {
|
||||
subscription.AutoRenew = *req.AutoRenew
|
||||
}
|
||||
|
||||
if err := h.db.Save(&subscription).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update subscription"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("User").First(&subscription, id)
|
||||
c.JSON(http.StatusOK, h.toSubscriptionResponse(&subscription))
|
||||
}
|
||||
|
||||
// GetStats handles GET /api/admin/subscriptions/stats
|
||||
func (h *AdminSubscriptionHandler) GetStats(c *gin.Context) {
|
||||
var total, free, premium, pro int64
|
||||
|
||||
h.db.Model(&models.UserSubscription{}).Count(&total)
|
||||
h.db.Model(&models.UserSubscription{}).Where("tier = ?", "free").Count(&free)
|
||||
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{
|
||||
"total": total,
|
||||
"free": free,
|
||||
"premium": premium,
|
||||
"pro": pro,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminSubscriptionHandler) toSubscriptionResponse(sub *models.UserSubscription) dto.SubscriptionResponse {
|
||||
response := dto.SubscriptionResponse{
|
||||
ID: sub.ID,
|
||||
UserID: sub.UserID,
|
||||
Tier: string(sub.Tier),
|
||||
Platform: sub.Platform,
|
||||
AutoRenew: sub.AutoRenew,
|
||||
CreatedAt: sub.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if sub.User.ID != 0 {
|
||||
response.UserEmail = sub.User.Email
|
||||
response.Username = sub.User.Username
|
||||
}
|
||||
if sub.SubscribedAt != nil {
|
||||
subscribedAt := sub.SubscribedAt.Format("2006-01-02T15:04:05Z")
|
||||
response.SubscribedAt = &subscribedAt
|
||||
}
|
||||
if sub.ExpiresAt != nil {
|
||||
expiresAt := sub.ExpiresAt.Format("2006-01-02T15:04:05Z")
|
||||
response.ExpiresAt = &expiresAt
|
||||
}
|
||||
if sub.CancelledAt != nil {
|
||||
cancelledAt := sub.CancelledAt.Format("2006-01-02T15:04:05Z")
|
||||
response.CancelledAt = &cancelledAt
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *AdminSubscriptionHandler) toSubscriptionDetailResponse(sub *models.UserSubscription) dto.SubscriptionDetailResponse {
|
||||
return dto.SubscriptionDetailResponse{
|
||||
SubscriptionResponse: h.toSubscriptionResponse(sub),
|
||||
}
|
||||
}
|
||||
338
internal/admin/handlers/task_handler.go
Normal file
338
internal/admin/handlers/task_handler.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminTaskHandler handles admin task management endpoints
|
||||
type AdminTaskHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminTaskHandler creates a new admin task handler
|
||||
func NewAdminTaskHandler(db *gorm.DB) *AdminTaskHandler {
|
||||
return &AdminTaskHandler{db: db}
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/tasks
|
||||
func (h *AdminTaskHandler) List(c *gin.Context) {
|
||||
var filters dto.TaskFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var tasks []models.Task
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.Task{}).
|
||||
Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where("title ILIKE ? OR description ILIKE ?", search, search)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.ResidenceID != nil {
|
||||
query = query.Where("residence_id = ?", *filters.ResidenceID)
|
||||
}
|
||||
if filters.CategoryID != nil {
|
||||
query = query.Where("category_id = ?", *filters.CategoryID)
|
||||
}
|
||||
if filters.PriorityID != nil {
|
||||
query = query.Where("priority_id = ?", *filters.PriorityID)
|
||||
}
|
||||
if filters.StatusID != nil {
|
||||
query = query.Where("status_id = ?", *filters.StatusID)
|
||||
}
|
||||
if filters.IsCancelled != nil {
|
||||
query = query.Where("is_cancelled = ?", *filters.IsCancelled)
|
||||
}
|
||||
if filters.IsArchived != nil {
|
||||
query = query.Where("is_archived = ?", *filters.IsArchived)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&tasks).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tasks"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]dto.TaskResponse, len(tasks))
|
||||
for i, task := range tasks {
|
||||
responses[i] = h.toTaskResponse(&task)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
if err := h.db.
|
||||
Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
First(&task, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task"})
|
||||
return
|
||||
}
|
||||
|
||||
response := dto.TaskDetailResponse{
|
||||
TaskResponse: h.toTaskResponse(&task),
|
||||
}
|
||||
|
||||
if task.AssignedTo != nil {
|
||||
response.AssignedTo = &dto.UserSummary{
|
||||
ID: task.AssignedTo.ID,
|
||||
Username: task.AssignedTo.Username,
|
||||
Email: task.AssignedTo.Email,
|
||||
}
|
||||
}
|
||||
|
||||
response.CompletionCount = len(task.Completions)
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/tasks/:id
|
||||
func (h *AdminTaskHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
task.Title = *req.Title
|
||||
}
|
||||
if req.Description != nil {
|
||||
task.Description = *req.Description
|
||||
}
|
||||
if req.CategoryID != nil {
|
||||
task.CategoryID = req.CategoryID
|
||||
}
|
||||
if req.PriorityID != nil {
|
||||
task.PriorityID = req.PriorityID
|
||||
}
|
||||
if req.StatusID != nil {
|
||||
task.StatusID = req.StatusID
|
||||
}
|
||||
if req.IsCancelled != nil {
|
||||
task.IsCancelled = *req.IsCancelled
|
||||
}
|
||||
if req.IsArchived != nil {
|
||||
task.IsArchived = *req.IsArchived
|
||||
}
|
||||
|
||||
if err := h.db.Save(&task).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update task"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").Preload("Status").First(&task, id)
|
||||
c.JSON(http.StatusOK, h.toTaskResponse(&task))
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/tasks
|
||||
func (h *AdminTaskHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
task := models.Task{
|
||||
ResidenceID: req.ResidenceID,
|
||||
CreatedByID: req.CreatedByID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
CategoryID: req.CategoryID,
|
||||
PriorityID: req.PriorityID,
|
||||
StatusID: req.StatusID,
|
||||
FrequencyID: req.FrequencyID,
|
||||
AssignedToID: req.AssignedToID,
|
||||
ContractorID: req.ContractorID,
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
}
|
||||
|
||||
if req.DueDate != nil {
|
||||
dueDate, err := time.Parse("2006-01-02", *req.DueDate)
|
||||
if err == nil {
|
||||
task.DueDate = &dueDate
|
||||
}
|
||||
}
|
||||
if req.EstimatedCost != nil {
|
||||
d := decimal.NewFromFloat(*req.EstimatedCost)
|
||||
task.EstimatedCost = &d
|
||||
}
|
||||
|
||||
if err := h.db.Create(&task).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create task"})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").Preload("Status").First(&task, task.ID)
|
||||
c.JSON(http.StatusCreated, h.toTaskResponse(&task))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/tasks/:id
|
||||
func (h *AdminTaskHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task"})
|
||||
return
|
||||
}
|
||||
|
||||
// Soft delete - archive and cancel
|
||||
task.IsArchived = true
|
||||
task.IsCancelled = true
|
||||
if err := h.db.Save(&task).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Task archived successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/tasks/bulk
|
||||
func (h *AdminTaskHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Soft delete - archive and cancel all
|
||||
if err := h.db.Model(&models.Task{}).Where("id IN ?", req.IDs).Updates(map[string]interface{}{
|
||||
"is_archived": true,
|
||||
"is_cancelled": true,
|
||||
}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete tasks"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Tasks archived successfully", "count": len(req.IDs)})
|
||||
}
|
||||
|
||||
func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
||||
response := dto.TaskResponse{
|
||||
ID: task.ID,
|
||||
ResidenceID: task.ResidenceID,
|
||||
Title: task.Title,
|
||||
Description: task.Description,
|
||||
IsCancelled: task.IsCancelled,
|
||||
IsArchived: task.IsArchived,
|
||||
CreatedAt: task.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if task.Residence.ID != 0 {
|
||||
response.ResidenceName = task.Residence.Name
|
||||
}
|
||||
if task.CreatedBy.ID != 0 {
|
||||
response.CreatedByName = task.CreatedBy.Username
|
||||
}
|
||||
if task.Category != nil {
|
||||
response.CategoryName = &task.Category.Name
|
||||
}
|
||||
if task.Priority != nil {
|
||||
response.PriorityName = &task.Priority.Name
|
||||
}
|
||||
if task.Status != nil {
|
||||
response.StatusName = &task.Status.Name
|
||||
}
|
||||
if task.DueDate != nil {
|
||||
dueDate := task.DueDate.Format("2006-01-02")
|
||||
response.DueDate = &dueDate
|
||||
}
|
||||
if task.EstimatedCost != nil {
|
||||
cost, _ := task.EstimatedCost.Float64()
|
||||
response.EstimatedCost = &cost
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
349
internal/admin/handlers/user_handler.go
Normal file
349
internal/admin/handlers/user_handler.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/dto"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminUserHandler handles admin user management endpoints
|
||||
type AdminUserHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminUserHandler creates a new admin user handler
|
||||
func NewAdminUserHandler(db *gorm.DB) *AdminUserHandler {
|
||||
return &AdminUserHandler{db: db}
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/users
|
||||
func (h *AdminUserHandler) List(c *gin.Context) {
|
||||
var filters dto.UserFilters
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var users []models.User
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.User{}).Preload("Profile")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where(
|
||||
"username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
|
||||
search, search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if filters.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *filters.IsActive)
|
||||
}
|
||||
if filters.IsStaff != nil {
|
||||
query = query.Where("is_staff = ?", *filters.IsStaff)
|
||||
}
|
||||
if filters.IsSuperuser != nil {
|
||||
query = query.Where("is_superuser = ?", *filters.IsSuperuser)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "date_joined"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]dto.UserResponse, len(users))
|
||||
for i, user := range users {
|
||||
responses[i] = h.toUserResponse(&user)
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build detailed response
|
||||
response := dto.UserDetailResponse{
|
||||
UserResponse: h.toUserResponse(&user),
|
||||
}
|
||||
|
||||
// Add residences
|
||||
for _, res := range user.OwnedResidences {
|
||||
response.Residences = append(response.Residences, dto.ResidenceSummary{
|
||||
ID: res.ID,
|
||||
Name: res.Name,
|
||||
IsOwner: true,
|
||||
IsActive: res.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/users
|
||||
func (h *AdminUserHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
IsActive: true,
|
||||
IsStaff: false,
|
||||
}
|
||||
|
||||
if req.IsActive != nil {
|
||||
user.IsActive = *req.IsActive
|
||||
}
|
||||
if req.IsStaff != nil {
|
||||
user.IsStaff = *req.IsStaff
|
||||
}
|
||||
if req.IsSuperuser != nil {
|
||||
user.IsSuperuser = *req.IsSuperuser
|
||||
}
|
||||
|
||||
if err := user.SetPassword(req.Password); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Create(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create profile with phone number
|
||||
profile := models.UserProfile{
|
||||
UserID: user.ID,
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
}
|
||||
h.db.Create(&profile)
|
||||
|
||||
// Reload with profile
|
||||
h.db.Preload("Profile").First(&user, user.ID)
|
||||
c.JSON(http.StatusCreated, h.toUserResponse(&user))
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/users/:id
|
||||
func (h *AdminUserHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Check username uniqueness if changing
|
||||
if req.Username != nil && *req.Username != user.Username {
|
||||
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
|
||||
}
|
||||
user.Username = *req.Username
|
||||
}
|
||||
|
||||
// Check email uniqueness if changing
|
||||
if req.Email != nil && *req.Email != user.Email {
|
||||
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
|
||||
}
|
||||
user.Email = *req.Email
|
||||
}
|
||||
|
||||
if req.FirstName != nil {
|
||||
user.FirstName = *req.FirstName
|
||||
}
|
||||
if req.LastName != nil {
|
||||
user.LastName = *req.LastName
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
user.IsActive = *req.IsActive
|
||||
}
|
||||
if req.IsStaff != nil {
|
||||
user.IsStaff = *req.IsStaff
|
||||
}
|
||||
if req.IsSuperuser != nil {
|
||||
user.IsSuperuser = *req.IsSuperuser
|
||||
}
|
||||
if req.Password != nil {
|
||||
if err := user.SetPassword(*req.Password); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.db.Save(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update phone number in profile if provided
|
||||
if req.PhoneNumber != nil {
|
||||
var profile models.UserProfile
|
||||
if err := h.db.Where("user_id = ?", user.ID).First(&profile).Error; err == nil {
|
||||
profile.PhoneNumber = *req.PhoneNumber
|
||||
h.db.Save(&profile)
|
||||
}
|
||||
}
|
||||
|
||||
h.db.Preload("Profile").First(&user, id)
|
||||
c.JSON(http.StatusOK, h.toUserResponse(&user))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/users/:id
|
||||
func (h *AdminUserHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User deactivated successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/users/bulk
|
||||
func (h *AdminUserHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Users deactivated successfully", "count": len(req.IDs)})
|
||||
}
|
||||
|
||||
// toUserResponse converts a User model to UserResponse DTO
|
||||
func (h *AdminUserHandler) toUserResponse(user *models.User) dto.UserResponse {
|
||||
response := dto.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
IsActive: user.IsActive,
|
||||
IsStaff: user.IsStaff,
|
||||
IsSuperuser: user.IsSuperuser,
|
||||
DateJoined: user.DateJoined.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if user.LastLogin != nil {
|
||||
lastLogin := user.LastLogin.Format("2006-01-02T15:04:05Z")
|
||||
response.LastLogin = &lastLogin
|
||||
}
|
||||
|
||||
if user.Profile != nil {
|
||||
response.Verified = user.Profile.Verified
|
||||
if user.Profile.PhoneNumber != "" {
|
||||
response.PhoneNumber = &user.Profile.PhoneNumber
|
||||
}
|
||||
}
|
||||
|
||||
// Get counts
|
||||
var residenceCount, taskCount int64
|
||||
h.db.Model(&models.Residence{}).Where("owner_id = ?", user.ID).Count(&residenceCount)
|
||||
h.db.Model(&models.Task{}).Where("created_by_id = ?", user.ID).Count(&taskCount)
|
||||
response.ResidenceCount = int(residenceCount)
|
||||
response.TaskCount = int(taskCount)
|
||||
|
||||
return response
|
||||
}
|
||||
326
internal/admin/routes.go
Normal file
326
internal/admin/routes.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/admin/handlers"
|
||||
"github.com/treytartt/mycrib-api/internal/config"
|
||||
"github.com/treytartt/mycrib-api/internal/middleware"
|
||||
"github.com/treytartt/mycrib-api/internal/push"
|
||||
"github.com/treytartt/mycrib-api/internal/repositories"
|
||||
"github.com/treytartt/mycrib-api/internal/services"
|
||||
)
|
||||
|
||||
// Dependencies holds optional services for admin routes
|
||||
type Dependencies struct {
|
||||
EmailService *services.EmailService
|
||||
PushClient *push.GorushClient
|
||||
}
|
||||
|
||||
// SetupRoutes configures all admin routes
|
||||
func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Dependencies) {
|
||||
// Create repositories
|
||||
adminRepo := repositories.NewAdminRepository(db)
|
||||
|
||||
// Create handlers
|
||||
authHandler := handlers.NewAdminAuthHandler(adminRepo, cfg)
|
||||
|
||||
// Admin API group
|
||||
admin := router.Group("/api/admin")
|
||||
{
|
||||
// Public auth routes (no auth required)
|
||||
auth := admin.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", authHandler.Login)
|
||||
}
|
||||
|
||||
// Protected routes (require admin JWT)
|
||||
protected := admin.Group("")
|
||||
protected.Use(middleware.AdminAuthMiddleware(cfg, adminRepo))
|
||||
{
|
||||
// Auth routes that require authentication
|
||||
protectedAuth := protected.Group("/auth")
|
||||
{
|
||||
protectedAuth.POST("/logout", authHandler.Logout)
|
||||
protectedAuth.GET("/me", authHandler.Me)
|
||||
protectedAuth.POST("/refresh", authHandler.RefreshToken)
|
||||
}
|
||||
|
||||
// User management
|
||||
userHandler := handlers.NewAdminUserHandler(db)
|
||||
users := protected.Group("/users")
|
||||
{
|
||||
users.GET("", userHandler.List)
|
||||
users.POST("", userHandler.Create)
|
||||
users.GET("/:id", userHandler.Get)
|
||||
users.PUT("/:id", userHandler.Update)
|
||||
users.DELETE("/:id", userHandler.Delete)
|
||||
users.DELETE("/bulk", userHandler.BulkDelete)
|
||||
}
|
||||
|
||||
// Residence management
|
||||
residenceHandler := handlers.NewAdminResidenceHandler(db)
|
||||
residences := protected.Group("/residences")
|
||||
{
|
||||
residences.GET("", residenceHandler.List)
|
||||
residences.POST("", residenceHandler.Create)
|
||||
residences.DELETE("/bulk", residenceHandler.BulkDelete)
|
||||
residences.GET("/:id", residenceHandler.Get)
|
||||
residences.PUT("/:id", residenceHandler.Update)
|
||||
residences.DELETE("/:id", residenceHandler.Delete)
|
||||
}
|
||||
|
||||
// Task management
|
||||
taskHandler := handlers.NewAdminTaskHandler(db)
|
||||
tasks := protected.Group("/tasks")
|
||||
{
|
||||
tasks.GET("", taskHandler.List)
|
||||
tasks.POST("", taskHandler.Create)
|
||||
tasks.DELETE("/bulk", taskHandler.BulkDelete)
|
||||
tasks.GET("/:id", taskHandler.Get)
|
||||
tasks.PUT("/:id", taskHandler.Update)
|
||||
tasks.DELETE("/:id", taskHandler.Delete)
|
||||
}
|
||||
|
||||
// Contractor management
|
||||
contractorHandler := handlers.NewAdminContractorHandler(db)
|
||||
contractors := protected.Group("/contractors")
|
||||
{
|
||||
contractors.GET("", contractorHandler.List)
|
||||
contractors.POST("", contractorHandler.Create)
|
||||
contractors.DELETE("/bulk", contractorHandler.BulkDelete)
|
||||
contractors.GET("/:id", contractorHandler.Get)
|
||||
contractors.PUT("/:id", contractorHandler.Update)
|
||||
contractors.DELETE("/:id", contractorHandler.Delete)
|
||||
}
|
||||
|
||||
// Document management
|
||||
documentHandler := handlers.NewAdminDocumentHandler(db)
|
||||
documents := protected.Group("/documents")
|
||||
{
|
||||
documents.GET("", documentHandler.List)
|
||||
documents.POST("", documentHandler.Create)
|
||||
documents.DELETE("/bulk", documentHandler.BulkDelete)
|
||||
documents.GET("/:id", documentHandler.Get)
|
||||
documents.PUT("/:id", documentHandler.Update)
|
||||
documents.DELETE("/:id", documentHandler.Delete)
|
||||
}
|
||||
|
||||
// Notification management
|
||||
var emailService *services.EmailService
|
||||
var pushClient *push.GorushClient
|
||||
if deps != nil {
|
||||
emailService = deps.EmailService
|
||||
pushClient = deps.PushClient
|
||||
}
|
||||
notificationHandler := handlers.NewAdminNotificationHandler(db, emailService, pushClient)
|
||||
notifications := protected.Group("/notifications")
|
||||
{
|
||||
notifications.GET("", notificationHandler.List)
|
||||
notifications.GET("/stats", notificationHandler.GetStats)
|
||||
notifications.POST("/send-test", notificationHandler.SendTestNotification)
|
||||
notifications.GET("/:id", notificationHandler.Get)
|
||||
notifications.PUT("/:id", notificationHandler.Update)
|
||||
notifications.DELETE("/:id", notificationHandler.Delete)
|
||||
}
|
||||
|
||||
// Email test endpoint
|
||||
emails := protected.Group("/emails")
|
||||
{
|
||||
emails.POST("/send-test", notificationHandler.SendTestEmail)
|
||||
}
|
||||
|
||||
// Subscription management
|
||||
subscriptionHandler := handlers.NewAdminSubscriptionHandler(db)
|
||||
subscriptions := protected.Group("/subscriptions")
|
||||
{
|
||||
subscriptions.GET("", subscriptionHandler.List)
|
||||
subscriptions.GET("/stats", subscriptionHandler.GetStats)
|
||||
subscriptions.GET("/:id", subscriptionHandler.Get)
|
||||
subscriptions.PUT("/:id", subscriptionHandler.Update)
|
||||
}
|
||||
|
||||
// Dashboard stats
|
||||
dashboardHandler := handlers.NewAdminDashboardHandler(db)
|
||||
protected.GET("/dashboard/stats", dashboardHandler.GetStats)
|
||||
|
||||
// Auth token management
|
||||
authTokenHandler := handlers.NewAdminAuthTokenHandler(db)
|
||||
authTokens := protected.Group("/auth-tokens")
|
||||
{
|
||||
authTokens.GET("", authTokenHandler.List)
|
||||
authTokens.DELETE("/bulk", authTokenHandler.BulkDelete)
|
||||
authTokens.GET("/:id", authTokenHandler.Get)
|
||||
authTokens.DELETE("/:id", authTokenHandler.Delete)
|
||||
}
|
||||
|
||||
// Task completion management
|
||||
completionHandler := handlers.NewAdminCompletionHandler(db)
|
||||
completions := protected.Group("/completions")
|
||||
{
|
||||
completions.GET("", completionHandler.List)
|
||||
completions.DELETE("/bulk", completionHandler.BulkDelete)
|
||||
completions.GET("/:id", completionHandler.Get)
|
||||
completions.PUT("/:id", completionHandler.Update)
|
||||
completions.DELETE("/:id", completionHandler.Delete)
|
||||
}
|
||||
|
||||
// Lookup tables management
|
||||
lookupHandler := handlers.NewAdminLookupHandler(db)
|
||||
|
||||
// Task Categories
|
||||
categories := protected.Group("/lookups/categories")
|
||||
{
|
||||
categories.GET("", lookupHandler.ListCategories)
|
||||
categories.POST("", lookupHandler.CreateCategory)
|
||||
categories.PUT("/:id", lookupHandler.UpdateCategory)
|
||||
categories.DELETE("/:id", lookupHandler.DeleteCategory)
|
||||
}
|
||||
|
||||
// Task Priorities
|
||||
priorities := protected.Group("/lookups/priorities")
|
||||
{
|
||||
priorities.GET("", lookupHandler.ListPriorities)
|
||||
priorities.POST("", lookupHandler.CreatePriority)
|
||||
priorities.PUT("/:id", lookupHandler.UpdatePriority)
|
||||
priorities.DELETE("/:id", lookupHandler.DeletePriority)
|
||||
}
|
||||
|
||||
// Task Statuses
|
||||
statuses := protected.Group("/lookups/statuses")
|
||||
{
|
||||
statuses.GET("", lookupHandler.ListStatuses)
|
||||
statuses.POST("", lookupHandler.CreateStatus)
|
||||
statuses.PUT("/:id", lookupHandler.UpdateStatus)
|
||||
statuses.DELETE("/:id", lookupHandler.DeleteStatus)
|
||||
}
|
||||
|
||||
// Task Frequencies
|
||||
frequencies := protected.Group("/lookups/frequencies")
|
||||
{
|
||||
frequencies.GET("", lookupHandler.ListFrequencies)
|
||||
frequencies.POST("", lookupHandler.CreateFrequency)
|
||||
frequencies.PUT("/:id", lookupHandler.UpdateFrequency)
|
||||
frequencies.DELETE("/:id", lookupHandler.DeleteFrequency)
|
||||
}
|
||||
|
||||
// Residence Types
|
||||
residenceTypes := protected.Group("/lookups/residence-types")
|
||||
{
|
||||
residenceTypes.GET("", lookupHandler.ListResidenceTypes)
|
||||
residenceTypes.POST("", lookupHandler.CreateResidenceType)
|
||||
residenceTypes.PUT("/:id", lookupHandler.UpdateResidenceType)
|
||||
residenceTypes.DELETE("/:id", lookupHandler.DeleteResidenceType)
|
||||
}
|
||||
|
||||
// Contractor Specialties
|
||||
specialties := protected.Group("/lookups/specialties")
|
||||
{
|
||||
specialties.GET("", lookupHandler.ListSpecialties)
|
||||
specialties.POST("", lookupHandler.CreateSpecialty)
|
||||
specialties.PUT("/:id", lookupHandler.UpdateSpecialty)
|
||||
specialties.DELETE("/:id", lookupHandler.DeleteSpecialty)
|
||||
}
|
||||
|
||||
// Admin user management
|
||||
adminUserHandler := handlers.NewAdminUserManagementHandler(db)
|
||||
adminUsers := protected.Group("/admin-users")
|
||||
{
|
||||
adminUsers.GET("", adminUserHandler.List)
|
||||
adminUsers.POST("", adminUserHandler.Create)
|
||||
adminUsers.GET("/:id", adminUserHandler.Get)
|
||||
adminUsers.PUT("/:id", adminUserHandler.Update)
|
||||
adminUsers.DELETE("/:id", adminUserHandler.Delete)
|
||||
}
|
||||
|
||||
// Notification preferences management
|
||||
notifPrefsHandler := handlers.NewAdminNotificationPrefsHandler(db)
|
||||
notifPrefs := protected.Group("/notification-prefs")
|
||||
{
|
||||
notifPrefs.GET("", notifPrefsHandler.List)
|
||||
notifPrefs.GET("/:id", notifPrefsHandler.Get)
|
||||
notifPrefs.PUT("/:id", notifPrefsHandler.Update)
|
||||
notifPrefs.DELETE("/:id", notifPrefsHandler.Delete)
|
||||
notifPrefs.GET("/user/:user_id", notifPrefsHandler.GetByUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serve admin panel static files
|
||||
setupStaticFiles(router)
|
||||
}
|
||||
|
||||
// setupStaticFiles configures serving the admin panel static files
|
||||
func setupStaticFiles(router *gin.Engine) {
|
||||
// Determine the static files directory
|
||||
// Check multiple possible locations
|
||||
possiblePaths := []string{
|
||||
"admin/out", // Development: from project root
|
||||
"./admin/out", // Development: relative
|
||||
"/app/admin/out", // Docker: absolute path
|
||||
}
|
||||
|
||||
var staticDir string
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
staticDir = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if staticDir == "" {
|
||||
// Admin panel not built yet, skip static file serving
|
||||
return
|
||||
}
|
||||
|
||||
// Serve static files at /admin/*
|
||||
router.GET("/admin/*filepath", func(c *gin.Context) {
|
||||
filePath := c.Param("filepath")
|
||||
|
||||
// Clean the path
|
||||
filePath = strings.TrimPrefix(filePath, "/")
|
||||
if filePath == "" {
|
||||
filePath = "index.html"
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(staticDir, filePath)
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||
// For SPA routing, serve index.html for non-existent paths
|
||||
// But only if it's not a file request (no extension or .html)
|
||||
ext := filepath.Ext(filePath)
|
||||
if ext == "" || ext == ".html" {
|
||||
// Try to find the specific page's index.html
|
||||
pagePath := filepath.Join(staticDir, filePath, "index.html")
|
||||
if _, err := os.Stat(pagePath); err == nil {
|
||||
c.File(pagePath)
|
||||
return
|
||||
}
|
||||
// Fall back to root index.html
|
||||
c.File(filepath.Join(staticDir, "index.html"))
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// If it's a directory, serve index.html
|
||||
info, _ := os.Stat(fullPath)
|
||||
if info.IsDir() {
|
||||
indexPath := filepath.Join(fullPath, "index.html")
|
||||
if _, err := os.Stat(indexPath); err == nil {
|
||||
c.File(indexPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.File(fullPath)
|
||||
})
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
)
|
||||
|
||||
// GetContractorsTable returns the contractors table configuration
|
||||
func GetContractorsTable(ctx *context.Context) table.Table {
|
||||
contractors := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := contractors.GetInfo()
|
||||
info.SetTable("task_contractor")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Company", "company", db.Varchar).FieldFilterable()
|
||||
info.AddField("Phone", "phone", db.Varchar).FieldFilterable()
|
||||
info.AddField("Email", "email", db.Varchar).FieldFilterable()
|
||||
info.AddField("Residence ID", "residence_id", db.Int).FieldFilterable()
|
||||
info.AddField("Specialty ID", "specialty_id", db.Int).FieldFilterable()
|
||||
info.AddField("Is Favorite", "is_favorite", db.Bool).FieldFilterable()
|
||||
info.AddField("Notes", "notes", db.Text)
|
||||
info.AddField("Created At", "created_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Updated At", "updated_at", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := contractors.GetForm()
|
||||
formList.SetTable("task_contractor")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Company", "company", db.Varchar, form.Text)
|
||||
formList.AddField("Phone", "phone", db.Varchar, form.Text)
|
||||
formList.AddField("Email", "email", db.Varchar, form.Email)
|
||||
formList.AddField("Website", "website", db.Varchar, form.Url)
|
||||
formList.AddField("Address", "address", db.Varchar, form.Text)
|
||||
formList.AddField("City", "city", db.Varchar, form.Text)
|
||||
formList.AddField("State", "state", db.Varchar, form.Text)
|
||||
formList.AddField("Zip Code", "zip_code", db.Varchar, form.Text)
|
||||
formList.AddField("Residence ID", "residence_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("Specialty ID", "specialty_id", db.Int, form.Number)
|
||||
formList.AddField("Is Favorite", "is_favorite", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Notes", "notes", db.Text, form.TextArea)
|
||||
|
||||
return contractors
|
||||
}
|
||||
|
||||
// GetContractorSpecialtiesTable returns the contractor specialties lookup table configuration
|
||||
func GetContractorSpecialtiesTable(ctx *context.Context) table.Table {
|
||||
specialties := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := specialties.GetInfo()
|
||||
info.SetTable("task_contractorspecialty")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Icon (iOS)", "icon_ios", db.Varchar)
|
||||
info.AddField("Icon (Android)", "icon_android", db.Varchar)
|
||||
info.AddField("Display Order", "display_order", db.Int).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := specialties.GetForm()
|
||||
formList.SetTable("task_contractorspecialty")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Icon (iOS)", "icon_ios", db.Varchar, form.Text)
|
||||
formList.AddField("Icon (Android)", "icon_android", db.Varchar, form.Text)
|
||||
formList.AddField("Display Order", "display_order", db.Int, form.Number).FieldDefault("0")
|
||||
|
||||
return specialties
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
)
|
||||
|
||||
// GetDocumentsTable returns the documents table configuration
|
||||
func GetDocumentsTable(ctx *context.Context) table.Table {
|
||||
documents := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := documents.GetInfo()
|
||||
info.SetTable("task_document")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Title", "title", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Type", "document_type", db.Varchar).FieldFilterable()
|
||||
info.AddField("Residence ID", "residence_id", db.Int).FieldFilterable()
|
||||
info.AddField("File URL", "file_url", db.Varchar)
|
||||
info.AddField("Is Active", "is_active", db.Bool).FieldFilterable()
|
||||
info.AddField("Expiration Date", "expiration_date", db.Date).FieldSortable()
|
||||
info.AddField("Created By ID", "created_by_id", db.Int).FieldFilterable()
|
||||
info.AddField("Created At", "created_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Updated At", "updated_at", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := documents.GetForm()
|
||||
formList.SetTable("task_document")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Title", "title", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Description", "description", db.Text, form.TextArea)
|
||||
formList.AddField("Type", "document_type", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Value: "warranty", Text: "Warranty"},
|
||||
{Value: "contract", Text: "Contract"},
|
||||
{Value: "receipt", Text: "Receipt"},
|
||||
{Value: "manual", Text: "Manual"},
|
||||
{Value: "insurance", Text: "Insurance"},
|
||||
{Value: "other", Text: "Other"},
|
||||
})
|
||||
formList.AddField("Residence ID", "residence_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("File URL", "file_url", db.Varchar, form.Url)
|
||||
formList.AddField("Is Active", "is_active", db.Bool, form.Switch).FieldDefault("true")
|
||||
formList.AddField("Expiration Date", "expiration_date", db.Date, form.Date)
|
||||
formList.AddField("Purchase Date", "purchase_date", db.Date, form.Date)
|
||||
formList.AddField("Purchase Amount", "purchase_amount", db.Decimal, form.Currency)
|
||||
formList.AddField("Vendor", "vendor", db.Varchar, form.Text)
|
||||
formList.AddField("Serial Number", "serial_number", db.Varchar, form.Text)
|
||||
formList.AddField("Model Number", "model_number", db.Varchar, form.Text)
|
||||
formList.AddField("Created By ID", "created_by_id", db.Int, form.Number)
|
||||
formList.AddField("Notes", "notes", db.Text, form.TextArea)
|
||||
|
||||
return documents
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
)
|
||||
|
||||
// GetNotificationsTable returns the notifications table configuration
|
||||
func GetNotificationsTable(ctx *context.Context) table.Table {
|
||||
notifications := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := notifications.GetInfo()
|
||||
info.SetTable("notifications_notification")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("User ID", "user_id", db.Int).FieldFilterable()
|
||||
info.AddField("Title", "title", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Message", "message", db.Text)
|
||||
info.AddField("Type", "notification_type", db.Varchar).FieldFilterable()
|
||||
info.AddField("Is Read", "is_read", db.Bool).FieldFilterable()
|
||||
info.AddField("Task ID", "task_id", db.Int).FieldFilterable()
|
||||
info.AddField("Residence ID", "residence_id", db.Int).FieldFilterable()
|
||||
info.AddField("Created At", "created_at", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := notifications.GetForm()
|
||||
formList.SetTable("notifications_notification")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("User ID", "user_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("Title", "title", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Message", "message", db.Text, form.TextArea).FieldMust()
|
||||
formList.AddField("Type", "notification_type", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Value: "task_assigned", Text: "Task Assigned"},
|
||||
{Value: "task_completed", Text: "Task Completed"},
|
||||
{Value: "task_due", Text: "Task Due"},
|
||||
{Value: "task_overdue", Text: "Task Overdue"},
|
||||
{Value: "residence_shared", Text: "Residence Shared"},
|
||||
{Value: "system", Text: "System"},
|
||||
})
|
||||
formList.AddField("Is Read", "is_read", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Task ID", "task_id", db.Int, form.Number)
|
||||
formList.AddField("Residence ID", "residence_id", db.Int, form.Number)
|
||||
formList.AddField("Data JSON", "data", db.Text, form.TextArea)
|
||||
formList.AddField("Action URL", "action_url", db.Varchar, form.Url)
|
||||
|
||||
return notifications
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
)
|
||||
|
||||
// GetResidencesTable returns the residences table configuration
|
||||
func GetResidencesTable(ctx *context.Context) table.Table {
|
||||
residences := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := residences.GetInfo()
|
||||
info.SetTable("residence_residence")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Address", "address", db.Varchar).FieldFilterable()
|
||||
info.AddField("City", "city", db.Varchar).FieldFilterable()
|
||||
info.AddField("State", "state", db.Varchar).FieldFilterable()
|
||||
info.AddField("Zip Code", "zip_code", db.Varchar).FieldFilterable()
|
||||
info.AddField("Owner ID", "owner_id", db.Int).FieldFilterable()
|
||||
info.AddField("Type ID", "residence_type_id", db.Int).FieldFilterable()
|
||||
info.AddField("Is Active", "is_active", db.Bool).FieldFilterable()
|
||||
info.AddField("Created At", "created_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Updated At", "updated_at", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := residences.GetForm()
|
||||
formList.SetTable("residence_residence")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Address", "address", db.Varchar, form.Text)
|
||||
formList.AddField("City", "city", db.Varchar, form.Text)
|
||||
formList.AddField("State", "state", db.Varchar, form.Text)
|
||||
formList.AddField("Zip Code", "zip_code", db.Varchar, form.Text)
|
||||
formList.AddField("Owner ID", "owner_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("Type ID", "residence_type_id", db.Int, form.Number)
|
||||
formList.AddField("Is Active", "is_active", db.Bool, form.Switch).FieldDefault("true")
|
||||
formList.AddField("Share Code", "share_code", db.Varchar, form.Text)
|
||||
formList.AddField("Share Code Expires", "share_code_expires_at", db.Timestamp, form.Datetime)
|
||||
|
||||
return residences
|
||||
}
|
||||
|
||||
// GetResidenceTypesTable returns the residence types lookup table configuration
|
||||
func GetResidenceTypesTable(ctx *context.Context) table.Table {
|
||||
types := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := types.GetInfo()
|
||||
info.SetTable("residence_residencetype")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Icon (iOS)", "icon_ios", db.Varchar)
|
||||
info.AddField("Icon (Android)", "icon_android", db.Varchar)
|
||||
info.AddField("Display Order", "display_order", db.Int).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := types.GetForm()
|
||||
formList.SetTable("residence_residencetype")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Icon (iOS)", "icon_ios", db.Varchar, form.Text)
|
||||
formList.AddField("Icon (Android)", "icon_android", db.Varchar, form.Text)
|
||||
formList.AddField("Display Order", "display_order", db.Int, form.Number).FieldDefault("0")
|
||||
|
||||
return types
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
)
|
||||
|
||||
// GetSubscriptionsTable returns the user subscriptions table configuration
|
||||
func GetSubscriptionsTable(ctx *context.Context) table.Table {
|
||||
subscriptions := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := subscriptions.GetInfo()
|
||||
info.SetTable("subscription_usersubscription")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("User ID", "user_id", db.Int).FieldFilterable()
|
||||
info.AddField("Tier", "tier", db.Varchar).FieldFilterable()
|
||||
info.AddField("Subscribed At", "subscribed_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Expires At", "expires_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Cancelled At", "cancelled_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Auto Renew", "auto_renew", db.Bool).FieldFilterable()
|
||||
info.AddField("Platform", "platform", db.Varchar).FieldFilterable()
|
||||
info.AddField("Created At", "created_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Updated At", "updated_at", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := subscriptions.GetForm()
|
||||
formList.SetTable("subscription_usersubscription")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("User ID", "user_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("Tier", "tier", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Value: "free", Text: "Free"},
|
||||
{Value: "pro", Text: "Pro"},
|
||||
}).FieldDefault("free")
|
||||
formList.AddField("Subscribed At", "subscribed_at", db.Timestamp, form.Datetime)
|
||||
formList.AddField("Expires At", "expires_at", db.Timestamp, form.Datetime)
|
||||
formList.AddField("Cancelled At", "cancelled_at", db.Timestamp, form.Datetime)
|
||||
formList.AddField("Auto Renew", "auto_renew", db.Bool, form.Switch).FieldDefault("true")
|
||||
formList.AddField("Platform", "platform", db.Varchar, form.SelectSingle).
|
||||
FieldOptions(types.FieldOptions{
|
||||
{Value: "", Text: "None"},
|
||||
{Value: "ios", Text: "iOS"},
|
||||
{Value: "android", Text: "Android"},
|
||||
})
|
||||
formList.AddField("Apple Receipt Data", "apple_receipt_data", db.Text, form.TextArea)
|
||||
formList.AddField("Google Purchase Token", "google_purchase_token", db.Text, form.TextArea)
|
||||
|
||||
return subscriptions
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package tables
|
||||
|
||||
import "github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
|
||||
// Generators is a map of table generators
|
||||
var Generators = map[string]table.Generator{
|
||||
"users": GetUsersTable,
|
||||
"residences": GetResidencesTable,
|
||||
"tasks": GetTasksTable,
|
||||
"task_completions": GetTaskCompletionsTable,
|
||||
"contractors": GetContractorsTable,
|
||||
"documents": GetDocumentsTable,
|
||||
"notifications": GetNotificationsTable,
|
||||
"user_subscriptions": GetSubscriptionsTable,
|
||||
"task_categories": GetTaskCategoriesTable,
|
||||
"task_priorities": GetTaskPrioritiesTable,
|
||||
"task_statuses": GetTaskStatusesTable,
|
||||
"task_frequencies": GetTaskFrequenciesTable,
|
||||
"contractor_specialties": GetContractorSpecialtiesTable,
|
||||
"residence_types": GetResidenceTypesTable,
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
)
|
||||
|
||||
// GetTasksTable returns the tasks table configuration
|
||||
func GetTasksTable(ctx *context.Context) table.Table {
|
||||
tasks := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := tasks.GetInfo()
|
||||
info.SetTable("task_task")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Title", "title", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Description", "description", db.Text)
|
||||
info.AddField("Residence ID", "residence_id", db.Int).FieldFilterable()
|
||||
info.AddField("Category ID", "category_id", db.Int).FieldFilterable()
|
||||
info.AddField("Priority ID", "priority_id", db.Int).FieldFilterable()
|
||||
info.AddField("Status ID", "status_id", db.Int).FieldFilterable()
|
||||
info.AddField("Frequency ID", "frequency_id", db.Int).FieldFilterable()
|
||||
info.AddField("Due Date", "due_date", db.Date).FieldFilterable().FieldSortable()
|
||||
info.AddField("Created By ID", "created_by_id", db.Int).FieldFilterable()
|
||||
info.AddField("Assigned To ID", "assigned_to_id", db.Int).FieldFilterable()
|
||||
info.AddField("Is Recurring", "is_recurring", db.Bool).FieldFilterable()
|
||||
info.AddField("Is Cancelled", "is_cancelled", db.Bool).FieldFilterable()
|
||||
info.AddField("Is Archived", "is_archived", db.Bool).FieldFilterable()
|
||||
info.AddField("Estimated Cost", "estimated_cost", db.Decimal)
|
||||
info.AddField("Created At", "created_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Updated At", "updated_at", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := tasks.GetForm()
|
||||
formList.SetTable("task_task")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Title", "title", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Description", "description", db.Text, form.TextArea)
|
||||
formList.AddField("Residence ID", "residence_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("Category ID", "category_id", db.Int, form.Number)
|
||||
formList.AddField("Priority ID", "priority_id", db.Int, form.Number)
|
||||
formList.AddField("Status ID", "status_id", db.Int, form.Number)
|
||||
formList.AddField("Frequency ID", "frequency_id", db.Int, form.Number)
|
||||
formList.AddField("Due Date", "due_date", db.Date, form.Date)
|
||||
formList.AddField("Created By ID", "created_by_id", db.Int, form.Number)
|
||||
formList.AddField("Assigned To ID", "assigned_to_id", db.Int, form.Number)
|
||||
formList.AddField("Contractor ID", "contractor_id", db.Int, form.Number)
|
||||
formList.AddField("Is Recurring", "is_recurring", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Is Cancelled", "is_cancelled", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Is Archived", "is_archived", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Estimated Cost", "estimated_cost", db.Decimal, form.Currency)
|
||||
formList.AddField("Notes", "notes", db.Text, form.TextArea)
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
// GetTaskCompletionsTable returns the task completions table configuration
|
||||
func GetTaskCompletionsTable(ctx *context.Context) table.Table {
|
||||
completions := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := completions.GetInfo()
|
||||
info.SetTable("task_taskcompletion")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Task ID", "task_id", db.Int).FieldFilterable()
|
||||
info.AddField("User ID", "user_id", db.Int).FieldFilterable()
|
||||
info.AddField("Completed At", "completed_at", db.Timestamp).FieldSortable()
|
||||
info.AddField("Notes", "notes", db.Text)
|
||||
info.AddField("Actual Cost", "actual_cost", db.Decimal)
|
||||
info.AddField("Receipt URL", "receipt_url", db.Varchar)
|
||||
info.AddField("Created At", "created_at", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := completions.GetForm()
|
||||
formList.SetTable("task_taskcompletion")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Task ID", "task_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("User ID", "user_id", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("Completed At", "completed_at", db.Timestamp, form.Datetime)
|
||||
formList.AddField("Notes", "notes", db.Text, form.TextArea)
|
||||
formList.AddField("Actual Cost", "actual_cost", db.Decimal, form.Currency)
|
||||
formList.AddField("Receipt URL", "receipt_url", db.Varchar, form.Url)
|
||||
|
||||
return completions
|
||||
}
|
||||
|
||||
// GetTaskCategoriesTable returns the task categories lookup table configuration
|
||||
func GetTaskCategoriesTable(ctx *context.Context) table.Table {
|
||||
categories := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := categories.GetInfo()
|
||||
info.SetTable("task_taskcategory")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Icon (iOS)", "icon_ios", db.Varchar)
|
||||
info.AddField("Icon (Android)", "icon_android", db.Varchar)
|
||||
info.AddField("Color", "color", db.Varchar)
|
||||
info.AddField("Display Order", "display_order", db.Int).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := categories.GetForm()
|
||||
formList.SetTable("task_taskcategory")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Icon (iOS)", "icon_ios", db.Varchar, form.Text)
|
||||
formList.AddField("Icon (Android)", "icon_android", db.Varchar, form.Text)
|
||||
formList.AddField("Color", "color", db.Varchar, form.Color)
|
||||
formList.AddField("Display Order", "display_order", db.Int, form.Number).FieldDefault("0")
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
// GetTaskPrioritiesTable returns the task priorities lookup table configuration
|
||||
func GetTaskPrioritiesTable(ctx *context.Context) table.Table {
|
||||
priorities := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := priorities.GetInfo()
|
||||
info.SetTable("task_taskpriority")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Level", "level", db.Int).FieldSortable()
|
||||
info.AddField("Color", "color", db.Varchar)
|
||||
info.AddField("Display Order", "display_order", db.Int).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := priorities.GetForm()
|
||||
formList.SetTable("task_taskpriority")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Level", "level", db.Int, form.Number).FieldMust()
|
||||
formList.AddField("Color", "color", db.Varchar, form.Color)
|
||||
formList.AddField("Display Order", "display_order", db.Int, form.Number).FieldDefault("0")
|
||||
|
||||
return priorities
|
||||
}
|
||||
|
||||
// GetTaskStatusesTable returns the task statuses lookup table configuration
|
||||
func GetTaskStatusesTable(ctx *context.Context) table.Table {
|
||||
statuses := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := statuses.GetInfo()
|
||||
info.SetTable("task_taskstatus")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Is Terminal", "is_terminal", db.Bool).FieldFilterable()
|
||||
info.AddField("Color", "color", db.Varchar)
|
||||
info.AddField("Display Order", "display_order", db.Int).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := statuses.GetForm()
|
||||
formList.SetTable("task_taskstatus")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Is Terminal", "is_terminal", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Color", "color", db.Varchar, form.Color)
|
||||
formList.AddField("Display Order", "display_order", db.Int, form.Number).FieldDefault("0")
|
||||
|
||||
return statuses
|
||||
}
|
||||
|
||||
// GetTaskFrequenciesTable returns the task frequencies lookup table configuration
|
||||
func GetTaskFrequenciesTable(ctx *context.Context) table.Table {
|
||||
frequencies := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := frequencies.GetInfo()
|
||||
info.SetTable("task_taskfrequency")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Name", "name", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("Days", "days", db.Int).FieldSortable()
|
||||
info.AddField("Display Order", "display_order", db.Int).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := frequencies.GetForm()
|
||||
formList.SetTable("task_taskfrequency")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Name", "name", db.Varchar, form.Text).FieldMust()
|
||||
formList.AddField("Days", "days", db.Int, form.Number)
|
||||
formList.AddField("Display Order", "display_order", db.Int, form.Number).FieldDefault("0")
|
||||
|
||||
return frequencies
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"github.com/GoAdminGroup/go-admin/context"
|
||||
"github.com/GoAdminGroup/go-admin/modules/db"
|
||||
"github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
|
||||
"github.com/GoAdminGroup/go-admin/template/types/form"
|
||||
)
|
||||
|
||||
// GetUsersTable returns the users table configuration
|
||||
func GetUsersTable(ctx *context.Context) table.Table {
|
||||
users := table.NewDefaultTable(ctx, table.DefaultConfigWithDriver(db.DriverPostgresql))
|
||||
|
||||
info := users.GetInfo()
|
||||
info.SetTable("auth_user")
|
||||
info.AddField("ID", "id", db.Int).FieldFilterable()
|
||||
info.AddField("Email", "email", db.Varchar).FieldFilterable().FieldSortable()
|
||||
info.AddField("First Name", "first_name", db.Varchar).FieldFilterable()
|
||||
info.AddField("Last Name", "last_name", db.Varchar).FieldFilterable()
|
||||
info.AddField("Is Active", "is_active", db.Bool).FieldFilterable()
|
||||
info.AddField("Is Staff", "is_staff", db.Bool).FieldFilterable()
|
||||
info.AddField("Is Superuser", "is_superuser", db.Bool).FieldFilterable()
|
||||
info.AddField("Email Verified", "email_verified", db.Bool).FieldFilterable()
|
||||
info.AddField("Date Joined", "date_joined", db.Timestamp).FieldSortable()
|
||||
info.AddField("Last Login", "last_login", db.Timestamp).FieldSortable()
|
||||
|
||||
info.SetFilterFormLayout(form.LayoutThreeCol)
|
||||
|
||||
formList := users.GetForm()
|
||||
formList.SetTable("auth_user")
|
||||
formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd().FieldNotAllowEdit()
|
||||
formList.AddField("Email", "email", db.Varchar, form.Email).FieldMust()
|
||||
formList.AddField("First Name", "first_name", db.Varchar, form.Text)
|
||||
formList.AddField("Last Name", "last_name", db.Varchar, form.Text)
|
||||
formList.AddField("Is Active", "is_active", db.Bool, form.Switch).FieldDefault("true")
|
||||
formList.AddField("Is Staff", "is_staff", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Is Superuser", "is_superuser", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Email Verified", "email_verified", db.Bool, form.Switch).FieldDefault("false")
|
||||
formList.AddField("Timezone", "timezone", db.Varchar, form.Text).FieldDefault("UTC")
|
||||
|
||||
return users
|
||||
}
|
||||
Reference in New Issue
Block a user