Merge pull request 'fixing for device details and logic error on devices' (#18) from feature/responses-v2 into dev

Reviewed-on: winter-access/backend_nam#18
This commit is contained in:
areeqakbr 2025-06-16 04:44:04 +00:00
commit 80b8a7da28
5 changed files with 111 additions and 136 deletions

3
.gitignore vendored
View File

@ -42,4 +42,5 @@ node_modules/
# Coverage files # Coverage files
coverage.out coverage.out
uploads/towers uploads/towers
uploads/*

View File

@ -6,21 +6,21 @@ import (
) )
type DeviceDetails struct { type DeviceDetails struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceCode string `json:"device_code" gorm:"unique"` DeviceCode string `json:"device_code" gorm:"unique"`
DeviceType DeviceType `json:"device_type"` DeviceType DeviceType `json:"device_type"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
PortAmount int `json:"port_amount"` PortAmount int `json:"port_amount"`
Status DeviceStatus `json:"status"` Status DeviceStatus `json:"status"`
Region *string `json:"region,omitempty" gorm:"type:varchar(255)"` Region *string `json:"region,omitempty" gorm:"type:varchar(255)"`
Province *string `json:"province,omitempty" gorm:"type:varchar(255)"` Province *string `json:"province,omitempty" gorm:"type:varchar(255)"`
City *string `json:"city,omitempty" gorm:"type:varchar(255)"` City *string `json:"city,omitempty" gorm:"type:varchar(255)"`
District *string `json:"district,omitempty" gorm:"type:varchar(255)"` District *string `json:"district,omitempty" gorm:"type:varchar(255)"`
ImageURL *string `json:"image_url,omitempty" gorm:"type:text"` ImageURL *string `json:"image_url,omitempty" gorm:"type:text"`
AdditionalImages StringSlice `gorm:"type:text[]"` ImageURLs StringSlice `json:"additional_images" gorm:"type:jsonb"` // Multiple images stored as JSONB
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
// Fixed Relationships - Use direct foreign keys // Fixed Relationships - Use direct foreign keys
DevicePort DevicePort `json:"device_port" gorm:"foreignKey:DeviceID;references:ID"` DevicePort DevicePort `json:"device_port" gorm:"foreignKey:DeviceID;references:ID"`
@ -31,8 +31,47 @@ type DeviceDetails struct {
Towers []Tower `json:"towers" gorm:"foreignKey:DeviceID;references:ID"` Towers []Tower `json:"towers" gorm:"foreignKey:DeviceID;references:ID"`
} }
func (DeviceDetails) TableName() string {
return "devices" // Use same table as Device entity
func (d *DeviceDetails) GetAllImageURLs() []string {
var allImages []string
// Add main image URL if exists
if d.ImageURL != nil && *d.ImageURL != "" {
allImages = append(allImages, *d.ImageURL)
}
// Add additional images
for _, img := range d.ImageURLs {
if img != "" {
// Avoid duplicates
isDuplicate := false
for _, existingImg := range allImages {
if existingImg == img {
isDuplicate = true
break
}
}
if !isDuplicate {
allImages = append(allImages, img)
}
}
}
return allImages
}
func (d *DeviceDetails) SetMultipleImages(imageURLs []string) {
if len(imageURLs) == 0 {
return
}
// Set primary image
primaryImage := imageURLs[0]
d.ImageURL = &primaryImage
// Set all images
d.ImageURLs = StringSlice(imageURLs)
} }
// Helper method to get all backbones connected to this device // Helper method to get all backbones connected to this device
@ -51,3 +90,6 @@ func (d *DeviceDetails) GetAllFishbones() []Fishbone {
return allFishbones return allFishbones
} }
func (DeviceDetails) TableName() string {
return "devices" // Use same table as Device entity
}

View File

@ -71,12 +71,10 @@ func (u *deviceDetailsUseCase) UpdateDeviceDetailsWithMultipleImages(id uuid.UUI
updates["latitude"] = *deviceDTO.Latitude updates["latitude"] = *deviceDTO.Latitude
} }
if deviceDTO.PortAmount != nil { if deviceDTO.PortAmount != nil {
// Validate port amount change
if *deviceDTO.PortAmount < 0 { if *deviceDTO.PortAmount < 0 {
return fmt.Errorf("port amount cannot be negative") return fmt.Errorf("port amount cannot be negative")
} }
// If not setting to 0, check current usage
if *deviceDTO.PortAmount > 0 { if *deviceDTO.PortAmount > 0 {
currentUsed, _, err := u.deviceDetailsRepo.GetPortUsageByDevice(id) currentUsed, _, err := u.deviceDetailsRepo.GetPortUsageByDevice(id)
if err != nil { if err != nil {
@ -124,27 +122,13 @@ func (u *deviceDetailsUseCase) UpdateDeviceDetailsWithMultipleImages(id uuid.UUI
if shouldReplace { if shouldReplace {
// Replace all images - delete old ones // Replace all images - delete old ones
if currentDevice.ImageURL != nil && *currentDevice.ImageURL != "" { for _, oldImageURL := range currentDevice.GetAllImageURLs() {
helper.DeleteDeviceImage(*currentDevice.ImageURL) helper.DeleteDeviceImage(oldImageURL)
}
// Delete other images if they exist
for _, imgURL := range currentDevice.AdditionalImages {
if imgURL != "" && (currentDevice.ImageURL == nil || imgURL != *currentDevice.ImageURL) {
helper.DeleteDeviceImage(imgURL)
}
} }
finalImageURLs = newImageURLs finalImageURLs = newImageURLs
} else { } else {
// Append to existing images // Append to existing images
existingImages := make([]string, 0) existingImages := currentDevice.GetAllImageURLs()
if currentDevice.ImageURL != nil && *currentDevice.ImageURL != "" {
existingImages = append(existingImages, *currentDevice.ImageURL)
}
for _, imgURL := range currentDevice.AdditionalImages {
if imgURL != "" && (currentDevice.ImageURL == nil || imgURL != *currentDevice.ImageURL) {
existingImages = append(existingImages, imgURL)
}
}
finalImageURLs = append(existingImages, newImageURLs...) finalImageURLs = append(existingImages, newImageURLs...)
} }
@ -153,8 +137,8 @@ func (u *deviceDetailsUseCase) UpdateDeviceDetailsWithMultipleImages(id uuid.UUI
updates["image_url"] = finalImageURLs[0] updates["image_url"] = finalImageURLs[0]
} }
// Update all images // Update all images using image_urls column (not additional_images)
updates["additional_images"] = entity.StringSlice(finalImageURLs) updates["image_urls"] = entity.StringSlice(finalImageURLs)
} }
if len(updates) == 0 { if len(updates) == 0 {

View File

@ -78,7 +78,7 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error {
} }
// Validate device types // Validate device types
if startDevice.DeviceType != "closure" { if startDevice.DeviceType != "CLOSURE" || startDevice.DeviceType != "closure" {
return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType) return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType)
} }

View File

@ -82,35 +82,37 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
fishboneInfos = append(fishboneInfos, info) fishboneInfos = append(fishboneInfos, info)
} }
// Convert tower connections - safely handle nullable ExternalTower // Convert tower connections
towerInfos := make([]res.TowerConnectionDetail, 0) towerInfos := make([]res.TowerConnectionDetail, 0)
for _, tower := range device.Towers { for _, tower := range device.Towers {
distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude) distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude)
// Safely handle nullable ExternalTower field
var externalTower *bool var externalTower *bool
if tower.ExternalTower != nil { if tower.ExternalTower != nil {
externalTower = tower.ExternalTower externalTower = tower.ExternalTower
} // If tower.ExternalTower is nil, externalTower remains nil }
// Safely handle nullable ImageURL // Get all tower image URLs
allTowerImageURLs := tower.GetAllImageURLs()
// Primary image URL
var imageURL *string var imageURL *string
if tower.ImageURL != "" { if tower.ImageURL != "" {
imageURL = &tower.ImageURL imageURL = &tower.ImageURL
} }
info := res.TowerConnectionDetail{ info := res.TowerConnectionDetail{
ID: tower.ID, ID: tower.ID,
TowerCode: tower.TowerCode, TowerCode: tower.TowerCode,
Distance: distance, Distance: distance,
ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil ExternalTower: externalTower,
ImageURL: imageURL, ImageURL: imageURL,
ImageURLs: tower.GetAllImageURLs(), // Get all images as a slice ImageURLs: allTowerImageURLs,
} }
towerInfos = append(towerInfos, info) towerInfos = append(towerInfos, info)
} }
// Convert entity.PortAssignmentResponse to res.PortAssignmentResponse // Convert port assignments
entityPortAssignments := device.DevicePort.GetPortAssignmentsWithDetails() entityPortAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
resPortAssignments := make([]res.PortAssignmentResponse, len(entityPortAssignments)) resPortAssignments := make([]res.PortAssignmentResponse, len(entityPortAssignments))
for i, pa := range entityPortAssignments { for i, pa := range entityPortAssignments {
@ -120,6 +122,7 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
IsOccupied: pa.IsOccupied, IsOccupied: pa.IsOccupied,
} }
} }
portAssignments := device.DevicePort.GetPortAssignmentsWithDetails() portAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
// Ensure we show all ports up to PortAmount // Ensure we show all ports up to PortAmount
@ -135,106 +138,51 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
// Fill in missing ports // Fill in missing ports
finalPortAssignments := make([]res.PortAssignmentResponse, 0) finalPortAssignments := make([]res.PortAssignmentResponse, 0)
for i := 1; i <= device.PortAmount; i++ { for i := 1; i <= device.PortAmount; i++ {
if pa, exists := portAssignmentMap[i]; exists { if assignment, exists := portAssignmentMap[i]; exists {
finalPortAssignments = append(finalPortAssignments, pa) finalPortAssignments = append(finalPortAssignments, assignment)
} else { } else {
// Determine if this port should be occupied
isOccupied := false
// Count occupied ports before this one
occupiedBefore := 0
for j := 1; j < i; j++ {
if existingPA, exists := portAssignmentMap[j]; exists && existingPA.IsOccupied {
occupiedBefore++
}
}
// Count total ports with customers
customersCount := 0
for _, pa := range portAssignments {
if pa.CustomerName != nil && *pa.CustomerName != "" {
customersCount++
}
}
// This port should be occupied if we haven't reached port_used yet
if occupiedBefore + customersCount < device.DevicePort.PortUsed {
isOccupied = true
}
finalPortAssignments = append(finalPortAssignments, res.PortAssignmentResponse{ finalPortAssignments = append(finalPortAssignments, res.PortAssignmentResponse{
PortNumber: i, PortNumber: i,
CustomerName: nil, CustomerName: nil,
IsOccupied: isOccupied, IsOccupied: false,
}) })
} }
} }
var customerNames []string // Get customer names
customerNames := device.DevicePort.GetCustomerNamesWithPorts()
// If port_used is 0, return empty port assignments // DIRECT FIX: Use device.GetAllImageURLs() method directly
if device.PortAmount == 0 { allImageURLs := device.GetAllImageURLs()
finalPortAssignments = []res.PortAssignmentResponse{}
customerNames = []string{}
} else if device.DevicePort.PortUsed == 0 {
// If port_used is 0, return empty port assignments but keep port_amount structure
finalPortAssignments = []res.PortAssignmentResponse{}
customerNames = []string{}
} else {
// Get port assignments with details
portAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
// Convert to response format
finalPortAssignments = make([]res.PortAssignmentResponse, len(portAssignments))
for i, pa := range portAssignments {
finalPortAssignments[i] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
}
}
// Get customer names
customerNames = device.DevicePort.GetCustomerNamesOnly()
}
var deviceImageURLs []string
if device.ImageURL != nil && *device.ImageURL != "" {
deviceImageURLs = append(deviceImageURLs, *device.ImageURL)
}
// Use deviceImageURLs instead of trying to call a non-existent method
allImageURLs := deviceImageURLs
// Handle empty slice vs nil for JSON response // Handle empty slice vs nil for JSON response
if len(allImageURLs) == 0 { if len(allImageURLs) == 0 {
allImageURLs = []string{} // Return empty array instead of nil allImageURLs = []string{} // Return empty array instead of nil
} }
response := res.DeviceDetailsResponse{ response := res.DeviceDetailsResponse{
ID: device.ID, ID: device.ID,
DeviceCode: device.DeviceCode, DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType), DeviceType: string(device.DeviceType),
Address: address, Address: address,
Longitude: device.Longitude, Longitude: device.Longitude,
Latitude: device.Latitude, Latitude: device.Latitude,
Status: string(device.Status), Status: string(device.Status),
PortAmount: device.PortAmount, PortAmount: device.PortAmount,
PortUsed: device.DevicePort.PortUsed, PortUsed: device.DevicePort.PortUsed,
PortAvailable: device.DevicePort.PortAvailable, PortAvailable: device.DevicePort.PortAvailable,
CustomerNames: customerNames, // Just customer names CustomerNames: customerNames,
PortAssignments: finalPortAssignments, // Use finalPortAssignments instead of resPortAssignments PortAssignments: finalPortAssignments,
Region: device.Region, Province: device.Province,
Province: device.Province, City: device.City,
City: device.City, District: device.District,
District: device.District, ImageURL: device.ImageURL, // Primary image
ImageURL: device.ImageURL, ImageURLs: allImageURLs, // All images using device.GetAllImageURLs()
ImageURLs: allImageURLs, // Use allImageURLs to ensure empty array instead of nil Backbones: backboneInfos,
Backbones: backboneInfos, Fishbones: fishboneInfos,
Fishbones: fishboneInfos, Towers: towerInfos,
Towers: towerInfos, CreatedAt: device.CreatedAt,
CreatedAt: device.CreatedAt, UpdatedAt: device.UpdatedAt,
UpdatedAt: device.UpdatedAt,
} }
return response, nil return response, nil