diff --git a/delivery/controller/device_details.go b/delivery/controller/device_details.go index 9bef0fe..23e0d16 100644 --- a/delivery/controller/device_details.go +++ b/delivery/controller/device_details.go @@ -12,6 +12,7 @@ import ( "users_management/m/config" "users_management/m/middleware" "users_management/m/model/dto/req" + "users_management/m/model/dto/res" "users_management/m/usecase" "users_management/m/utils/common" @@ -53,10 +54,108 @@ func (c *DeviceDetailsController) Route() { // delete images by filename deviceDetails.DELETE("/:id/images/:filename", c.deleteDeviceImage) + deviceDetails.GET("/without-towers", c.getDevicesWithoutTowers) + + deviceDetails.GET("/without-connections", c.getDevicesWithoutConnections) + + } } + +func (c *DeviceDetailsController) getDevicesWithoutConnections(ctx *gin.Context) { + // Get device types from query parameters (optional) + deviceTypesParam := ctx.Query("device_types") + var deviceTypes []string + + if deviceTypesParam != "" { + // Split by comma if multiple types provided + deviceTypes = strings.Split(deviceTypesParam, ",") + // Trim whitespace and normalize case + for i, dt := range deviceTypes { + trimmed := strings.TrimSpace(dt) + if strings.ToUpper(trimmed) == "OTB" { + deviceTypes[i] = "OTB" + } else if strings.ToLower(trimmed) == "closure" { + deviceTypes[i] = "closure" + } else { + deviceTypes[i] = trimmed + } + } + } + + devices, err := c.deviceDetailsUC.GetDevicesWithoutConnections(deviceTypes) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + // Categorize devices by type for better response structure + closureDevices := make([]res.DeviceDetailsResponse, 0) + otbDevices := make([]res.DeviceDetailsResponse, 0) + + for _, device := range devices { + if device.DeviceType == "closure" { + closureDevices = append(closureDevices, device) + } else if device.DeviceType == "OTB" { + otbDevices = append(otbDevices, device) + } + } + + response := gin.H{ + "devices": devices, + "total": len(devices), + "breakdown": gin.H{ + "closure": gin.H{ + "count": len(closureDevices), + "devices": closureDevices, + }, + "otb": gin.H{ + "count": len(otbDevices), + "devices": otbDevices, + }, + }, + "filter": gin.H{ + "device_types": deviceTypes, + "criteria": "without_fishbones_and_backbones", + }, + } + + common.SingleResponses(ctx, "Devices without connections retrieved successfully", response) +} +func (c *DeviceDetailsController) getDevicesWithoutTowers(ctx *gin.Context) { + // Get device types from query parameters (optional) + deviceTypesParam := ctx.Query("device_types") + var deviceTypes []string + + if deviceTypesParam != "" { + // Split by comma if multiple types provided + deviceTypes = strings.Split(deviceTypesParam, ",") + // Trim whitespace + for i, dt := range deviceTypes { + deviceTypes[i] = strings.TrimSpace(strings.ToUpper(dt)) + } + } + + devices, err := c.deviceDetailsUC.GetDevicesWithoutTowers(deviceTypes) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + response := gin.H{ + "devices": devices, + "total": len(devices), + "filter": gin.H{ + "device_types": deviceTypes, + "without_towers": true, + }, + } + + common.SingleResponses(ctx, "Devices without towers retrieved successfully", response) +} + func (c *DeviceDetailsController) deleteDeviceImage(ctx *gin.Context) { // Parse device ID id := ctx.Param("id") diff --git a/model/dto/res/backbone_res.go b/model/dto/res/backbone_res.go index f4e7fa5..81798cc 100644 --- a/model/dto/res/backbone_res.go +++ b/model/dto/res/backbone_res.go @@ -11,6 +11,8 @@ type BackboneResponse struct { BackboneCode string `json:"backbone_code"` DevStart string `json:"dev_start"` DevEnd string `json:"dev_end"` + DeviceStartID uuid.UUID `json:"device_start_id"` + DeviceEndID uuid.UUID `json:"device_end_id"` CoreAmount int `json:"core_amount"` TotalFishbone int `json:"total_fishbone"` CreatedAt time.Time `json:"created_at"` diff --git a/repository/device_details.go b/repository/device_details.go index 8a9cc37..e4ce6e0 100644 --- a/repository/device_details.go +++ b/repository/device_details.go @@ -37,6 +37,8 @@ type DeviceDetailsRepo interface { UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error + GetDevicesWithoutTowers(deviceTypes []string) ([]entity.DeviceDetails, error) + GetDevicesWithoutConnections(deviceTypes []string) ([]entity.DeviceDetails, error) } @@ -51,6 +53,34 @@ func NewDeviceDetailsRepo(db *gorm.DB) DeviceDetailsRepo { } } +func (r *deviceDetailsRepo) GetDevicesWithoutConnections(deviceTypes []string) ([]entity.DeviceDetails, error) { + var devices []entity.DeviceDetails + + query := r.db.Preload("DevicePort") + + // Filter by device types + if len(deviceTypes) > 0 { + query = query.Where("device_type IN ?", deviceTypes) + } + + // For closure devices: exclude those that have fishbones (as start device) + // For OTB devices: exclude those that have backbones (as start or end device) or fishbones (as end device) + subQueryBackbone := r.db.Table("backbone"). + Select("DISTINCT CASE WHEN dev_start_id IS NOT NULL THEN dev_start_id ELSE dev_end_id END as device_id"). + Where("dev_start_id IS NOT NULL OR dev_end_id IS NOT NULL") + + subQueryFishbone := r.db.Table("fishbone"). + Select("DISTINCT CASE WHEN dev_start_id IS NOT NULL THEN dev_start_id ELSE dev_end_id END as device_id"). + Where("dev_start_id IS NOT NULL OR dev_end_id IS NOT NULL") + + // Combine both subqueries to exclude devices that have any connections + query = query.Where("id NOT IN (?)", + r.db.Raw("(?) UNION (?)", subQueryBackbone, subQueryFishbone)) + + err := query.Find(&devices).Error + return devices, err +} + func (r *deviceDetailsRepo) UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error { return r.db.Transaction(func(tx *gorm.DB) error { // Lock both device and device_port records @@ -426,6 +456,21 @@ func (r *deviceDetailsRepo) Update(id uuid.UUID, updates map[string]interface{}) }) } +func (r *deviceDetailsRepo) GetDevicesWithoutTowers(deviceTypes []string) ([]entity.DeviceDetails, error) { + var devices []entity.DeviceDetails + + query := r.db. + Preload("DevicePort"). + Where("device_type IN ?", deviceTypes). + Where("id NOT IN (?)", + r.db.Table("towers"). + Select("dev_id"). + Where("dev_id IS NOT NULL")) + + err := query.Find(&devices).Error + return devices, err +} + func (r *deviceDetailsRepo) updatePortAmountCascade(tx *gorm.DB, deviceID uuid.UUID, newPortAmount int) error { // Get current port usage var devicePort entity.DevicePort diff --git a/usecase/device_details.go b/usecase/device_details.go index b8ea36c..b34f4c5 100644 --- a/usecase/device_details.go +++ b/usecase/device_details.go @@ -36,6 +36,8 @@ type DeviceDetailsUseCase interface { RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error UpdateDeviceDetailsWithMultipleImages(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error DeleteDeviceImage(deviceID uuid.UUID, filename string) error + GetDevicesWithoutTowers(deviceTypes []string) ([]res.DeviceDetailsResponse, error) + GetDevicesWithoutConnections(deviceTypes []string) ([]res.DeviceDetailsResponse, error) } type deviceDetailsUseCase struct { @@ -52,6 +54,33 @@ func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, geo } } +func (u *deviceDetailsUseCase) GetDevicesWithoutConnections(deviceTypes []string) ([]res.DeviceDetailsResponse, error) { + // Validate device types + validTypes := map[string]bool{"closure": true, "OTB": true} + for _, deviceType := range deviceTypes { + if !validTypes[deviceType] { + return nil, fmt.Errorf("invalid device type: %s. Only closure and OTB are allowed", deviceType) + } + } + + // If no device types provided, default to closure and OTB + if len(deviceTypes) == 0 { + deviceTypes = []string{"closure", "OTB"} + } + + devices, err := u.deviceDetailsRepo.GetDevicesWithoutConnections(deviceTypes) + if err != nil { + return nil, err + } + + responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder) + if err != nil { + return nil, err + } + + return responses, nil +} + func (u *deviceDetailsUseCase) DeleteDeviceImage(deviceID uuid.UUID, filename string) error { // Get current device currentDevice, err := u.deviceDetailsRepo.GetByID(deviceID) @@ -118,6 +147,33 @@ func (u *deviceDetailsUseCase) DeleteDeviceImage(deviceID uuid.UUID, filename st return nil } +func (u *deviceDetailsUseCase) GetDevicesWithoutTowers(deviceTypes []string) ([]res.DeviceDetailsResponse, error) { + // Validate device types + validTypes := map[string]bool{"ODP": true, "OTB": true} + for _, deviceType := range deviceTypes { + if !validTypes[deviceType] { + return nil, fmt.Errorf("invalid device type: %s. Only ODP and OTB are allowed", deviceType) + } + } + + // If no device types provided, default to ODP and OTB + if len(deviceTypes) == 0 { + deviceTypes = []string{"ODP", "OTB"} + } + + devices, err := u.deviceDetailsRepo.GetDevicesWithoutTowers(deviceTypes) + if err != nil { + return nil, err + } + + responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder) + if err != nil { + return nil, err + } + + return responses, nil +} + func (u *deviceDetailsUseCase) UpdateDeviceDetailsWithMultipleImages(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error { err := u.validate.Struct(deviceDTO) if err != nil { diff --git a/usecase/fishbone_usecase.go b/usecase/fishbone_usecase.go index e45650d..f23101e 100644 --- a/usecase/fishbone_usecase.go +++ b/usecase/fishbone_usecase.go @@ -3,6 +3,7 @@ package usecase import ( "errors" "fmt" + "strings" "time" "users_management/m/model/dto/req" "users_management/m/model/dto/res" @@ -78,11 +79,11 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { } // Validate device types - if startDevice.DeviceType != "CLOSURE" || startDevice.DeviceType != "closure" { + if strings.ToLower(string(startDevice.DeviceType)) != "closure" { return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType) } - if endDevice.DeviceType != "ODP" { + if strings.ToUpper(string(endDevice.DeviceType)) != "ODP" { return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType) } diff --git a/utils/helper/backboneHelperRes.go b/utils/helper/backboneHelperRes.go index e24c2dd..3d15861 100644 --- a/utils/helper/backboneHelperRes.go +++ b/utils/helper/backboneHelperRes.go @@ -19,6 +19,8 @@ func ConvertToBackboneResponses(backbone []entity.Backbone, totalFishbone map[uu BackboneCode: backbone.BackboneCode, DevStart: backbone.DeviceStart.DeviceCode, DevEnd: backbone.DeviceEnd.DeviceCode, + DeviceStartID: backbone.DeviceStart.ID, + DeviceEndID: backbone.DeviceEnd.ID, CoreAmount: backbone.CoreAmount, TotalFishbone: count, CreatedAt: backbone.CreatedAt, @@ -35,6 +37,8 @@ func ConvertToBackboneRespId(backbone entity.Backbone, fishboneCount int) (res.B ID: backbone.ID, DevStart: backbone.DeviceStart.DeviceCode, DevEnd: backbone.DeviceEnd.DeviceCode, + DeviceStartID: backbone.DeviceStart.ID, + DeviceEndID: backbone.DeviceEnd.ID, CoreAmount: backbone.CoreAmount, TotalFishbone: fishboneCount, CreatedAt: backbone.CreatedAt,