Merge pull request 'adding without connection and without towers' (#22) from feature/responses-v2 into dev

Reviewed-on: winter-access/backend_nam#22
This commit is contained in:
areeqakbr 2025-06-18 09:16:42 +00:00
commit 7bf13d6e1a
6 changed files with 209 additions and 2 deletions

View File

@ -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")

View File

@ -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"`

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -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,