Fix admin panel edit forms and expand API responses
- Fix dropdown population on all edit pages (residence, task, contractor, document) - Add formInitialized state pattern to prevent empty dropdowns - Increase pagination max limit from 100 to 10000 for admin queries - Expand handler responses to include all editable fields - Add settings page with seed data and limitations toggle - Fix user form and API client types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ 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"`
|
||||
PerPage int `form:"per_page" binding:"omitempty,min=1,max=10000"`
|
||||
Search string `form:"search"`
|
||||
SortBy string `form:"sort_by"`
|
||||
SortDir string `form:"sort_dir" binding:"omitempty,oneof=asc desc"`
|
||||
@@ -22,8 +22,8 @@ func (p *PaginationParams) GetPerPage() int {
|
||||
if p.PerPage < 1 {
|
||||
return 20
|
||||
}
|
||||
if p.PerPage > 100 {
|
||||
return 100
|
||||
if p.PerPage > 10000 {
|
||||
return 10000
|
||||
}
|
||||
return p.PerPage
|
||||
}
|
||||
@@ -74,6 +74,7 @@ type UpdateUserRequest struct {
|
||||
IsActive *bool `json:"is_active"`
|
||||
IsStaff *bool `json:"is_staff"`
|
||||
IsSuperuser *bool `json:"is_superuser"`
|
||||
Verified *bool `json:"verified"`
|
||||
}
|
||||
|
||||
// BulkDeleteRequest for bulk delete operations
|
||||
@@ -90,14 +91,25 @@ type ResidenceFilters struct {
|
||||
|
||||
// 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"`
|
||||
OwnerID *uint `json:"owner_id"`
|
||||
Name *string `json:"name" binding:"omitempty,max=200"`
|
||||
PropertyTypeID *uint `json:"property_type_id"`
|
||||
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
|
||||
ApartmentUnit *string `json:"apartment_unit" binding:"omitempty,max=50"`
|
||||
City *string `json:"city" binding:"omitempty,max=100"`
|
||||
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
|
||||
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
|
||||
Country *string `json:"country" binding:"omitempty,max=100"`
|
||||
Bedrooms *int `json:"bedrooms"`
|
||||
Bathrooms *float64 `json:"bathrooms"`
|
||||
SquareFootage *int `json:"square_footage"`
|
||||
LotSize *float64 `json:"lot_size"`
|
||||
YearBuilt *int `json:"year_built"`
|
||||
Description *string `json:"description"`
|
||||
PurchaseDate *string `json:"purchase_date"`
|
||||
PurchasePrice *float64 `json:"purchase_price"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
IsPrimary *bool `json:"is_primary"`
|
||||
}
|
||||
|
||||
// TaskFilters holds task-specific filter parameters
|
||||
@@ -113,13 +125,22 @@ type TaskFilters struct {
|
||||
|
||||
// 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"`
|
||||
ResidenceID *uint `json:"residence_id"`
|
||||
CreatedByID *uint `json:"created_by_id"`
|
||||
AssignedToID *uint `json:"assigned_to_id"`
|
||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||
Description *string `json:"description"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
PriorityID *uint `json:"priority_id"`
|
||||
StatusID *uint `json:"status_id"`
|
||||
FrequencyID *uint `json:"frequency_id"`
|
||||
DueDate *string `json:"due_date"`
|
||||
EstimatedCost *float64 `json:"estimated_cost"`
|
||||
ActualCost *float64 `json:"actual_cost"`
|
||||
ContractorID *uint `json:"contractor_id"`
|
||||
ParentTaskID *uint `json:"parent_task_id"`
|
||||
IsCancelled *bool `json:"is_cancelled"`
|
||||
IsArchived *bool `json:"is_archived"`
|
||||
}
|
||||
|
||||
// ContractorFilters holds contractor-specific filter parameters
|
||||
@@ -132,14 +153,22 @@ type ContractorFilters struct {
|
||||
|
||||
// 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"`
|
||||
ResidenceID *uint `json:"residence_id"`
|
||||
CreatedByID *uint `json:"created_by_id"`
|
||||
Name *string `json:"name" binding:"omitempty,max=200"`
|
||||
Company *string `json:"company" binding:"omitempty,max=200"`
|
||||
Phone *string `json:"phone" binding:"omitempty,max=20"`
|
||||
Email *string `json:"email" binding:"omitempty,email"`
|
||||
Website *string `json:"website" binding:"omitempty,max=200"`
|
||||
Notes *string `json:"notes"`
|
||||
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
|
||||
City *string `json:"city" binding:"omitempty,max=100"`
|
||||
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
|
||||
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
|
||||
Rating *float64 `json:"rating"`
|
||||
IsFavorite *bool `json:"is_favorite"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
SpecialtyIDs []uint `json:"specialty_ids"`
|
||||
}
|
||||
|
||||
// DocumentFilters holds document-specific filter parameters
|
||||
@@ -152,12 +181,29 @@ type DocumentFilters struct {
|
||||
|
||||
// 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"`
|
||||
ResidenceID *uint `json:"residence_id"`
|
||||
CreatedByID *uint `json:"created_by_id"`
|
||||
Title *string `json:"title" binding:"omitempty,max=200"`
|
||||
Description *string `json:"description"`
|
||||
DocumentType *string `json:"document_type"`
|
||||
FileURL *string `json:"file_url" binding:"omitempty,max=500"`
|
||||
FileName *string `json:"file_name" binding:"omitempty,max=255"`
|
||||
FileSize *int64 `json:"file_size"`
|
||||
MimeType *string `json:"mime_type" binding:"omitempty,max=100"`
|
||||
PurchaseDate *string `json:"purchase_date"`
|
||||
ExpiryDate *string `json:"expiry_date"`
|
||||
PurchasePrice *float64 `json:"purchase_price"`
|
||||
Vendor *string `json:"vendor" binding:"omitempty,max=200"`
|
||||
SerialNumber *string `json:"serial_number" binding:"omitempty,max=100"`
|
||||
ModelNumber *string `json:"model_number" binding:"omitempty,max=100"`
|
||||
Provider *string `json:"provider" binding:"omitempty,max=200"`
|
||||
ProviderContact *string `json:"provider_contact" binding:"omitempty,max=200"`
|
||||
ClaimPhone *string `json:"claim_phone" binding:"omitempty,max=50"`
|
||||
ClaimEmail *string `json:"claim_email" binding:"omitempty,email"`
|
||||
ClaimWebsite *string `json:"claim_website" binding:"omitempty,max=500"`
|
||||
Notes *string `json:"notes"`
|
||||
TaskID *uint `json:"task_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// NotificationFilters holds notification-specific filter parameters
|
||||
@@ -188,8 +234,12 @@ type SubscriptionFilters struct {
|
||||
|
||||
// UpdateSubscriptionRequest for updating a subscription
|
||||
type UpdateSubscriptionRequest struct {
|
||||
Tier *string `json:"tier" binding:"omitempty,oneof=free premium pro"`
|
||||
AutoRenew *bool `json:"auto_renew"`
|
||||
Tier *string `json:"tier" binding:"omitempty,oneof=free premium pro"`
|
||||
AutoRenew *bool `json:"auto_renew"`
|
||||
Platform *string `json:"platform" binding:"omitempty,max=20"`
|
||||
SubscribedAt *string `json:"subscribed_at"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
CancelledAt *string `json:"cancelled_at"`
|
||||
}
|
||||
|
||||
// CreateResidenceRequest for creating a new residence
|
||||
|
||||
@@ -76,22 +76,30 @@ type UserSummary struct {
|
||||
|
||||
// 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"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID uint `json:"owner_id"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
PropertyTypeID *uint `json:"property_type_id,omitempty"`
|
||||
PropertyType *string `json:"property_type,omitempty"`
|
||||
StreetAddress string `json:"street_address"`
|
||||
ApartmentUnit string `json:"apartment_unit"`
|
||||
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"`
|
||||
LotSize *float64 `json:"lot_size,omitempty"`
|
||||
YearBuilt *int `json:"year_built,omitempty"`
|
||||
Description string `json:"description"`
|
||||
PurchaseDate *string `json:"purchase_date,omitempty"`
|
||||
PurchasePrice *float64 `json:"purchase_price,omitempty"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ResidenceDetailResponse includes more details for single residence view
|
||||
@@ -105,20 +113,32 @@ type ResidenceDetailResponse struct {
|
||||
|
||||
// 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"`
|
||||
ID uint `json:"id"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
CreatedByID uint `json:"created_by_id"`
|
||||
CreatedByName string `json:"created_by_name"`
|
||||
AssignedToID *uint `json:"assigned_to_id,omitempty"`
|
||||
AssignedToName *string `json:"assigned_to_name,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CategoryID *uint `json:"category_id,omitempty"`
|
||||
CategoryName *string `json:"category_name,omitempty"`
|
||||
PriorityID *uint `json:"priority_id,omitempty"`
|
||||
PriorityName *string `json:"priority_name,omitempty"`
|
||||
StatusID *uint `json:"status_id,omitempty"`
|
||||
StatusName *string `json:"status_name,omitempty"`
|
||||
FrequencyID *uint `json:"frequency_id,omitempty"`
|
||||
FrequencyName *string `json:"frequency_name,omitempty"`
|
||||
DueDate *string `json:"due_date,omitempty"`
|
||||
EstimatedCost *float64 `json:"estimated_cost,omitempty"`
|
||||
ActualCost *float64 `json:"actual_cost,omitempty"`
|
||||
ContractorID *uint `json:"contractor_id,omitempty"`
|
||||
ParentTaskID *uint `json:"parent_task_id,omitempty"`
|
||||
IsCancelled bool `json:"is_cancelled"`
|
||||
IsArchived bool `json:"is_archived"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TaskDetailResponse includes more details for single task view
|
||||
@@ -133,17 +153,25 @@ type ContractorResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
CreatedByID uint `json:"created_by_id"`
|
||||
CreatedByName string `json:"created_by_name"`
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Website string `json:"website"`
|
||||
Notes string `json:"notes"`
|
||||
StreetAddress string `json:"street_address"`
|
||||
City string `json:"city"`
|
||||
StateProvince string `json:"state_province"`
|
||||
PostalCode string `json:"postal_code"`
|
||||
Rating *float64 `json:"rating,omitempty"`
|
||||
Specialties []string `json:"specialties,omitempty"`
|
||||
SpecialtyIDs []uint `json:"specialty_ids,omitempty"`
|
||||
IsFavorite bool `json:"is_favorite"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ContractorDetailResponse includes more details for single contractor view
|
||||
@@ -161,20 +189,35 @@ type DocumentImageResponse struct {
|
||||
|
||||
// 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"`
|
||||
Images []DocumentImageResponse `json:"images"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID uint `json:"id"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
CreatedByID uint `json:"created_by_id"`
|
||||
CreatedByName string `json:"created_by_name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DocumentType string `json:"document_type"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize *int64 `json:"file_size,omitempty"`
|
||||
MimeType string `json:"mime_type"`
|
||||
PurchaseDate *string `json:"purchase_date,omitempty"`
|
||||
ExpiryDate *string `json:"expiry_date,omitempty"`
|
||||
PurchasePrice *float64 `json:"purchase_price,omitempty"`
|
||||
Vendor string `json:"vendor"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
ModelNumber string `json:"model_number"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderContact string `json:"provider_contact"`
|
||||
ClaimPhone string `json:"claim_phone"`
|
||||
ClaimEmail string `json:"claim_email"`
|
||||
ClaimWebsite string `json:"claim_website"`
|
||||
Notes string `json:"notes"`
|
||||
TaskID *uint `json:"task_id,omitempty"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Images []DocumentImageResponse `json:"images"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// DocumentDetailResponse includes more details for single document view
|
||||
|
||||
@@ -142,6 +142,24 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify residence if changing
|
||||
if req.ResidenceID != nil {
|
||||
var residence models.Residence
|
||||
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
|
||||
return
|
||||
}
|
||||
contractor.ResidenceID = *req.ResidenceID
|
||||
}
|
||||
// Verify created_by if changing
|
||||
if req.CreatedByID != nil {
|
||||
var user models.User
|
||||
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
|
||||
return
|
||||
}
|
||||
contractor.CreatedByID = *req.CreatedByID
|
||||
}
|
||||
if req.Name != nil {
|
||||
contractor.Name = *req.Name
|
||||
}
|
||||
@@ -160,6 +178,21 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
|
||||
if req.Notes != nil {
|
||||
contractor.Notes = *req.Notes
|
||||
}
|
||||
if req.StreetAddress != nil {
|
||||
contractor.StreetAddress = *req.StreetAddress
|
||||
}
|
||||
if req.City != nil {
|
||||
contractor.City = *req.City
|
||||
}
|
||||
if req.StateProvince != nil {
|
||||
contractor.StateProvince = *req.StateProvince
|
||||
}
|
||||
if req.PostalCode != nil {
|
||||
contractor.PostalCode = *req.PostalCode
|
||||
}
|
||||
if req.Rating != nil {
|
||||
contractor.Rating = req.Rating
|
||||
}
|
||||
if req.IsFavorite != nil {
|
||||
contractor.IsFavorite = *req.IsFavorite
|
||||
}
|
||||
@@ -167,6 +200,18 @@ func (h *AdminContractorHandler) Update(c *gin.Context) {
|
||||
contractor.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
// Update specialties if provided
|
||||
if req.SpecialtyIDs != nil {
|
||||
// Clear existing specialties
|
||||
h.db.Model(&contractor).Association("Specialties").Clear()
|
||||
// Add new specialties
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.db.Save(&contractor).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update contractor"})
|
||||
return
|
||||
@@ -278,29 +323,39 @@ func (h *AdminContractorHandler) BulkDelete(c *gin.Context) {
|
||||
|
||||
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"),
|
||||
ID: contractor.ID,
|
||||
ResidenceID: contractor.ResidenceID,
|
||||
CreatedByID: contractor.CreatedByID,
|
||||
Name: contractor.Name,
|
||||
Company: contractor.Company,
|
||||
Phone: contractor.Phone,
|
||||
Email: contractor.Email,
|
||||
Website: contractor.Website,
|
||||
Notes: contractor.Notes,
|
||||
StreetAddress: contractor.StreetAddress,
|
||||
City: contractor.City,
|
||||
StateProvince: contractor.StateProvince,
|
||||
PostalCode: contractor.PostalCode,
|
||||
IsFavorite: contractor.IsFavorite,
|
||||
IsActive: contractor.IsActive,
|
||||
CreatedAt: contractor.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: contractor.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if contractor.Residence.ID != 0 {
|
||||
response.ResidenceName = contractor.Residence.Name
|
||||
}
|
||||
if contractor.CreatedBy.ID != 0 {
|
||||
response.CreatedByName = contractor.CreatedBy.Username
|
||||
}
|
||||
if contractor.Rating != nil {
|
||||
response.Rating = contractor.Rating
|
||||
}
|
||||
|
||||
// Add specialties
|
||||
// Add specialties (names and IDs)
|
||||
for _, s := range contractor.Specialties {
|
||||
response.Specialties = append(response.Specialties, s.Name)
|
||||
response.SpecialtyIDs = append(response.SpecialtyIDs, s.ID)
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
@@ -144,12 +144,59 @@ func (h *AdminDocumentHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify residence if changing
|
||||
if req.ResidenceID != nil {
|
||||
var residence models.Residence
|
||||
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
|
||||
return
|
||||
}
|
||||
document.ResidenceID = *req.ResidenceID
|
||||
}
|
||||
// Verify created_by if changing
|
||||
if req.CreatedByID != nil {
|
||||
var user models.User
|
||||
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
|
||||
return
|
||||
}
|
||||
document.CreatedByID = *req.CreatedByID
|
||||
}
|
||||
if req.Title != nil {
|
||||
document.Title = *req.Title
|
||||
}
|
||||
if req.Description != nil {
|
||||
document.Description = *req.Description
|
||||
}
|
||||
if req.DocumentType != nil {
|
||||
document.DocumentType = models.DocumentType(*req.DocumentType)
|
||||
}
|
||||
if req.FileURL != nil {
|
||||
document.FileURL = *req.FileURL
|
||||
}
|
||||
if req.FileName != nil {
|
||||
document.FileName = *req.FileName
|
||||
}
|
||||
if req.FileSize != nil {
|
||||
document.FileSize = req.FileSize
|
||||
}
|
||||
if req.MimeType != nil {
|
||||
document.MimeType = *req.MimeType
|
||||
}
|
||||
if req.PurchaseDate != nil {
|
||||
if purchaseDate, err := time.Parse("2006-01-02", *req.PurchaseDate); err == nil {
|
||||
document.PurchaseDate = &purchaseDate
|
||||
}
|
||||
}
|
||||
if req.ExpiryDate != nil {
|
||||
if expiryDate, err := time.Parse("2006-01-02", *req.ExpiryDate); err == nil {
|
||||
document.ExpiryDate = &expiryDate
|
||||
}
|
||||
}
|
||||
if req.PurchasePrice != nil {
|
||||
d := decimal.NewFromFloat(*req.PurchasePrice)
|
||||
document.PurchasePrice = &d
|
||||
}
|
||||
if req.Vendor != nil {
|
||||
document.Vendor = *req.Vendor
|
||||
}
|
||||
@@ -159,6 +206,27 @@ func (h *AdminDocumentHandler) Update(c *gin.Context) {
|
||||
if req.ModelNumber != nil {
|
||||
document.ModelNumber = *req.ModelNumber
|
||||
}
|
||||
if req.Provider != nil {
|
||||
document.Provider = *req.Provider
|
||||
}
|
||||
if req.ProviderContact != nil {
|
||||
document.ProviderContact = *req.ProviderContact
|
||||
}
|
||||
if req.ClaimPhone != nil {
|
||||
document.ClaimPhone = *req.ClaimPhone
|
||||
}
|
||||
if req.ClaimEmail != nil {
|
||||
document.ClaimEmail = *req.ClaimEmail
|
||||
}
|
||||
if req.ClaimWebsite != nil {
|
||||
document.ClaimWebsite = *req.ClaimWebsite
|
||||
}
|
||||
if req.Notes != nil {
|
||||
document.Notes = *req.Notes
|
||||
}
|
||||
if req.TaskID != nil {
|
||||
document.TaskID = req.TaskID
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
document.IsActive = *req.IsActive
|
||||
}
|
||||
@@ -289,22 +357,38 @@ func (h *AdminDocumentHandler) BulkDelete(c *gin.Context) {
|
||||
|
||||
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,
|
||||
Images: make([]dto.DocumentImageResponse, 0),
|
||||
CreatedAt: doc.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
ID: doc.ID,
|
||||
ResidenceID: doc.ResidenceID,
|
||||
CreatedByID: doc.CreatedByID,
|
||||
Title: doc.Title,
|
||||
Description: doc.Description,
|
||||
DocumentType: string(doc.DocumentType),
|
||||
FileURL: doc.FileURL,
|
||||
FileName: doc.FileName,
|
||||
FileSize: doc.FileSize,
|
||||
MimeType: doc.MimeType,
|
||||
Vendor: doc.Vendor,
|
||||
SerialNumber: doc.SerialNumber,
|
||||
ModelNumber: doc.ModelNumber,
|
||||
Provider: doc.Provider,
|
||||
ProviderContact: doc.ProviderContact,
|
||||
ClaimPhone: doc.ClaimPhone,
|
||||
ClaimEmail: doc.ClaimEmail,
|
||||
ClaimWebsite: doc.ClaimWebsite,
|
||||
Notes: doc.Notes,
|
||||
TaskID: doc.TaskID,
|
||||
IsActive: doc.IsActive,
|
||||
Images: make([]dto.DocumentImageResponse, 0),
|
||||
CreatedAt: doc.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: doc.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if doc.Residence.ID != 0 {
|
||||
response.ResidenceName = doc.Residence.Name
|
||||
}
|
||||
if doc.CreatedBy.ID != 0 {
|
||||
response.CreatedByName = doc.CreatedBy.Username
|
||||
}
|
||||
if doc.ExpiryDate != nil {
|
||||
expiryDate := doc.ExpiryDate.Format("2006-01-02")
|
||||
response.ExpiryDate = &expiryDate
|
||||
@@ -313,6 +397,10 @@ func (h *AdminDocumentHandler) toDocumentResponse(doc *models.Document) dto.Docu
|
||||
purchaseDate := doc.PurchaseDate.Format("2006-01-02")
|
||||
response.PurchaseDate = &purchaseDate
|
||||
}
|
||||
if doc.PurchasePrice != nil {
|
||||
price, _ := doc.PurchasePrice.Float64()
|
||||
response.PurchasePrice = &price
|
||||
}
|
||||
|
||||
// Convert images
|
||||
for _, img := range doc.Images {
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
@@ -153,12 +154,27 @@ func (h *AdminResidenceHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.OwnerID != nil {
|
||||
// Verify owner exists
|
||||
var owner models.User
|
||||
if err := h.db.First(&owner, *req.OwnerID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Owner not found"})
|
||||
return
|
||||
}
|
||||
residence.OwnerID = *req.OwnerID
|
||||
}
|
||||
if req.Name != nil {
|
||||
residence.Name = *req.Name
|
||||
}
|
||||
if req.PropertyTypeID != nil {
|
||||
residence.PropertyTypeID = req.PropertyTypeID
|
||||
}
|
||||
if req.StreetAddress != nil {
|
||||
residence.StreetAddress = *req.StreetAddress
|
||||
}
|
||||
if req.ApartmentUnit != nil {
|
||||
residence.ApartmentUnit = *req.ApartmentUnit
|
||||
}
|
||||
if req.City != nil {
|
||||
residence.City = *req.City
|
||||
}
|
||||
@@ -171,6 +187,35 @@ func (h *AdminResidenceHandler) Update(c *gin.Context) {
|
||||
if req.Country != nil {
|
||||
residence.Country = *req.Country
|
||||
}
|
||||
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.LotSize != nil {
|
||||
d := decimal.NewFromFloat(*req.LotSize)
|
||||
residence.LotSize = &d
|
||||
}
|
||||
if req.YearBuilt != nil {
|
||||
residence.YearBuilt = req.YearBuilt
|
||||
}
|
||||
if req.Description != nil {
|
||||
residence.Description = *req.Description
|
||||
}
|
||||
if req.PurchaseDate != nil {
|
||||
if purchaseDate, err := time.Parse("2006-01-02", *req.PurchaseDate); err == nil {
|
||||
residence.PurchaseDate = &purchaseDate
|
||||
}
|
||||
}
|
||||
if req.PurchasePrice != nil {
|
||||
d := decimal.NewFromFloat(*req.PurchasePrice)
|
||||
residence.PurchasePrice = &d
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
residence.IsActive = *req.IsActive
|
||||
}
|
||||
@@ -287,17 +332,21 @@ func (h *AdminResidenceHandler) BulkDelete(c *gin.Context) {
|
||||
|
||||
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"),
|
||||
ID: res.ID,
|
||||
Name: res.Name,
|
||||
OwnerID: res.OwnerID,
|
||||
PropertyTypeID: res.PropertyTypeID,
|
||||
StreetAddress: res.StreetAddress,
|
||||
ApartmentUnit: res.ApartmentUnit,
|
||||
City: res.City,
|
||||
StateProvince: res.StateProvince,
|
||||
PostalCode: res.PostalCode,
|
||||
Country: res.Country,
|
||||
Description: res.Description,
|
||||
IsPrimary: res.IsPrimary,
|
||||
IsActive: res.IsActive,
|
||||
CreatedAt: res.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: res.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if res.Owner.ID != 0 {
|
||||
@@ -316,6 +365,21 @@ func (h *AdminResidenceHandler) toResidenceResponse(res *models.Residence) dto.R
|
||||
if res.SquareFootage != nil {
|
||||
response.SquareFootage = res.SquareFootage
|
||||
}
|
||||
if res.LotSize != nil {
|
||||
f, _ := res.LotSize.Float64()
|
||||
response.LotSize = &f
|
||||
}
|
||||
if res.YearBuilt != nil {
|
||||
response.YearBuilt = res.YearBuilt
|
||||
}
|
||||
if res.PurchaseDate != nil {
|
||||
purchaseDate := res.PurchaseDate.Format("2006-01-02")
|
||||
response.PurchaseDate = &purchaseDate
|
||||
}
|
||||
if res.PurchasePrice != nil {
|
||||
f, _ := res.PurchasePrice.Float64()
|
||||
response.PurchasePrice = &f
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -215,3 +215,270 @@ func isCommentOnly(stmt string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ClearAllDataResponse represents the response after clearing data
|
||||
type ClearAllDataResponse struct {
|
||||
Message string `json:"message"`
|
||||
UsersDeleted int64 `json:"users_deleted"`
|
||||
PreservedUsers int64 `json:"preserved_users"`
|
||||
}
|
||||
|
||||
// ClearAllData handles POST /api/admin/settings/clear-all-data
|
||||
// This clears all data except super admin accounts and lookup tables
|
||||
func (h *AdminSettingsHandler) ClearAllData(c *gin.Context) {
|
||||
// Start a transaction
|
||||
tx := h.db.Begin()
|
||||
if tx.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start transaction"})
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get IDs of users to preserve (superusers)
|
||||
var preservedUserIDs []uint
|
||||
if err := tx.Model(&models.User{}).
|
||||
Where("is_superuser = ?", true).
|
||||
Pluck("id", &preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get superuser IDs"})
|
||||
return
|
||||
}
|
||||
|
||||
// Count users that will be deleted
|
||||
var usersToDelete int64
|
||||
if err := tx.Model(&models.User{}).
|
||||
Where("is_superuser = ?", false).
|
||||
Count(&usersToDelete).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count users"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete in order to respect foreign key constraints
|
||||
// Order matters: delete from child tables first
|
||||
|
||||
// 1. Delete task completion images
|
||||
if err := tx.Exec("DELETE FROM task_taskcompletionimage").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task completion images: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Delete task completions
|
||||
if err := tx.Exec("DELETE FROM task_taskcompletion").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task completions: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Delete document images
|
||||
if err := tx.Exec("DELETE FROM task_documentimage").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete document images: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Delete documents
|
||||
if err := tx.Exec("DELETE FROM task_document").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete documents: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 5. Delete tasks (must be before contractors since tasks reference contractors)
|
||||
if err := tx.Exec("DELETE FROM task_task").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete tasks: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 6. Delete contractor specialties (many-to-many)
|
||||
if err := tx.Exec("DELETE FROM task_contractor_specialties").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractor specialties: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 7. Delete contractors
|
||||
if err := tx.Exec("DELETE FROM task_contractor").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete contractors: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 8. Delete residence_users (many-to-many for shared residences)
|
||||
if err := tx.Exec("DELETE FROM residence_residence_users").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residence users: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 9. Delete residences
|
||||
if err := tx.Exec("DELETE FROM residence_residence").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete residences: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 10. Delete notifications for non-superusers
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM notifications_notification WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notifications: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM notifications_notification").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notifications: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Delete push devices for non-superusers (both APNS and GCM)
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM push_notifications_apnsdevice WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete APNS devices: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := tx.Exec("DELETE FROM push_notifications_gcmdevice WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete GCM devices: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM push_notifications_apnsdevice").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete APNS devices: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := tx.Exec("DELETE FROM push_notifications_gcmdevice").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete GCM devices: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 12. Delete notification preferences for non-superusers
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM notifications_notificationpreference WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preferences: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM notifications_notificationpreference").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete notification preferences: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 13. Delete user subscriptions for non-superusers
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM subscription_usersubscription WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete subscriptions: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM subscription_usersubscription").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete subscriptions: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 14. Delete password reset codes for non-superusers
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM user_passwordresetcode WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM user_passwordresetcode").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 15. Delete confirmation codes for non-superusers
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM user_confirmationcode WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM user_confirmationcode").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 16. Delete auth tokens for non-superusers
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM user_authtoken WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete auth tokens: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM user_authtoken").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete auth tokens: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 17. Delete user profiles for non-superusers
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM user_userprofile WHERE user_id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profiles: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM user_userprofile").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user profiles: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 18. Finally, delete non-superuser users
|
||||
if len(preservedUserIDs) > 0 {
|
||||
if err := tx.Exec("DELETE FROM auth_user WHERE id NOT IN (?)", preservedUserIDs).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete users: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := tx.Exec("DELETE FROM auth_user WHERE is_superuser = false").Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete users: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ClearAllDataResponse{
|
||||
Message: "All data cleared successfully (superadmin accounts preserved)",
|
||||
UsersDeleted: usersToDelete,
|
||||
PreservedUsers: int64(len(preservedUserIDs)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,6 +162,33 @@ func (h *AdminTaskHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify residence if changing
|
||||
if req.ResidenceID != nil {
|
||||
var residence models.Residence
|
||||
if err := h.db.First(&residence, *req.ResidenceID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Residence not found"})
|
||||
return
|
||||
}
|
||||
task.ResidenceID = *req.ResidenceID
|
||||
}
|
||||
// Verify created_by if changing
|
||||
if req.CreatedByID != nil {
|
||||
var user models.User
|
||||
if err := h.db.First(&user, *req.CreatedByID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Created by user not found"})
|
||||
return
|
||||
}
|
||||
task.CreatedByID = *req.CreatedByID
|
||||
}
|
||||
// Verify assigned_to if changing
|
||||
if req.AssignedToID != nil {
|
||||
var user models.User
|
||||
if err := h.db.First(&user, *req.AssignedToID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Assigned to user not found"})
|
||||
return
|
||||
}
|
||||
task.AssignedToID = req.AssignedToID
|
||||
}
|
||||
if req.Title != nil {
|
||||
task.Title = *req.Title
|
||||
}
|
||||
@@ -177,6 +204,28 @@ func (h *AdminTaskHandler) Update(c *gin.Context) {
|
||||
if req.StatusID != nil {
|
||||
task.StatusID = req.StatusID
|
||||
}
|
||||
if req.FrequencyID != nil {
|
||||
task.FrequencyID = req.FrequencyID
|
||||
}
|
||||
if req.DueDate != nil {
|
||||
if dueDate, err := time.Parse("2006-01-02", *req.DueDate); err == nil {
|
||||
task.DueDate = &dueDate
|
||||
}
|
||||
}
|
||||
if req.EstimatedCost != nil {
|
||||
d := decimal.NewFromFloat(*req.EstimatedCost)
|
||||
task.EstimatedCost = &d
|
||||
}
|
||||
if req.ActualCost != nil {
|
||||
d := decimal.NewFromFloat(*req.ActualCost)
|
||||
task.ActualCost = &d
|
||||
}
|
||||
if req.ContractorID != nil {
|
||||
task.ContractorID = req.ContractorID
|
||||
}
|
||||
if req.ParentTaskID != nil {
|
||||
task.ParentTaskID = req.ParentTaskID
|
||||
}
|
||||
if req.IsCancelled != nil {
|
||||
task.IsCancelled = *req.IsCancelled
|
||||
}
|
||||
@@ -303,11 +352,19 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
||||
response := dto.TaskResponse{
|
||||
ID: task.ID,
|
||||
ResidenceID: task.ResidenceID,
|
||||
CreatedByID: task.CreatedByID,
|
||||
Title: task.Title,
|
||||
Description: task.Description,
|
||||
CategoryID: task.CategoryID,
|
||||
PriorityID: task.PriorityID,
|
||||
StatusID: task.StatusID,
|
||||
FrequencyID: task.FrequencyID,
|
||||
ContractorID: task.ContractorID,
|
||||
ParentTaskID: task.ParentTaskID,
|
||||
IsCancelled: task.IsCancelled,
|
||||
IsArchived: task.IsArchived,
|
||||
CreatedAt: task.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: task.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
if task.Residence.ID != 0 {
|
||||
@@ -316,6 +373,12 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
||||
if task.CreatedBy.ID != 0 {
|
||||
response.CreatedByName = task.CreatedBy.Username
|
||||
}
|
||||
if task.AssignedToID != nil {
|
||||
response.AssignedToID = task.AssignedToID
|
||||
if task.AssignedTo != nil {
|
||||
response.AssignedToName = &task.AssignedTo.Username
|
||||
}
|
||||
}
|
||||
if task.Category != nil {
|
||||
response.CategoryName = &task.Category.Name
|
||||
}
|
||||
@@ -325,6 +388,9 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
||||
if task.Status != nil {
|
||||
response.StatusName = &task.Status.Name
|
||||
}
|
||||
if task.Frequency != nil {
|
||||
response.FrequencyName = &task.Frequency.Name
|
||||
}
|
||||
if task.DueDate != nil {
|
||||
dueDate := task.DueDate.Format("2006-01-02")
|
||||
response.DueDate = &dueDate
|
||||
@@ -333,6 +399,10 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
|
||||
cost, _ := task.EstimatedCost.Float64()
|
||||
response.EstimatedCost = &cost
|
||||
}
|
||||
if task.ActualCost != nil {
|
||||
cost, _ := task.ActualCost.Float64()
|
||||
response.ActualCost = &cost
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -254,12 +254,27 @@ func (h *AdminUserHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update phone number in profile if provided
|
||||
if req.PhoneNumber != nil {
|
||||
// Update profile fields if provided
|
||||
if req.PhoneNumber != nil || req.Verified != nil {
|
||||
var profile models.UserProfile
|
||||
if err := h.db.Where("user_id = ?", user.ID).First(&profile).Error; err == nil {
|
||||
profile.PhoneNumber = *req.PhoneNumber
|
||||
if req.PhoneNumber != nil {
|
||||
profile.PhoneNumber = *req.PhoneNumber
|
||||
}
|
||||
if req.Verified != nil {
|
||||
profile.Verified = *req.Verified
|
||||
}
|
||||
h.db.Save(&profile)
|
||||
} else {
|
||||
// Create profile if it doesn't exist
|
||||
profile = models.UserProfile{UserID: user.ID}
|
||||
if req.PhoneNumber != nil {
|
||||
profile.PhoneNumber = *req.PhoneNumber
|
||||
}
|
||||
if req.Verified != nil {
|
||||
profile.Verified = *req.Verified
|
||||
}
|
||||
h.db.Create(&profile)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe
|
||||
settings.PUT("", settingsHandler.UpdateSettings)
|
||||
settings.POST("/seed-lookups", settingsHandler.SeedLookups)
|
||||
settings.POST("/seed-test-data", settingsHandler.SeedTestData)
|
||||
settings.POST("/clear-all-data", settingsHandler.ClearAllData)
|
||||
}
|
||||
|
||||
// Limitations management (tier limits, upgrade triggers)
|
||||
|
||||
Reference in New Issue
Block a user