From ece05cee18c356c547912b15300c12f6ab6b1e1b Mon Sep 17 00:00:00 2001 From: areeqakbr Date: Sat, 14 Jun 2025 15:30:04 +0700 Subject: [PATCH] adding more response for port assignments --- delivery/controller/device_details.go | 77 +++++ delivery/controller/fishbone_controller.go | 25 ++ delivery/controller/tower_controller.go | 75 ++++- delivery/server.go | 1 - model/dto/req/devicePort_dto.go | 19 ++ model/dto/res/device_details.go | 48 ++-- model/dto/res/device_res.go | 1 + model/dto/res/tower_res.go | 2 +- model/entity/device_port.go | 263 ++++++++++++++++- repository/device_details.go | 318 ++++++++++++++------- repository/fishbone_repo.go | 11 + usecase/backbone_usecase.go | 2 +- usecase/device_details.go | 57 +++- usecase/fishbone_usecase.go | 15 + usecase/tower_usecase.go | 3 + utils/helper/device_details.go | 118 ++++++-- 16 files changed, 860 insertions(+), 175 deletions(-) diff --git a/delivery/controller/device_details.go b/delivery/controller/device_details.go index f7a928f..ce424b0 100644 --- a/delivery/controller/device_details.go +++ b/delivery/controller/device_details.go @@ -32,9 +32,40 @@ func (c *DeviceDetailsController) Route() { deviceDetails.PUT("/:id", c.updateDeviceDetails) deviceDetails.DELETE("/:id", c.deleteDeviceDetails) deviceDetails.POST("/:id/recalculate-ports", c.recalculatePortUsage) + deviceDetails.POST("/:id/assign-customer", c.assignCustomer) + deviceDetails.PUT("/:id/port-usage", c.updatePortUsage) // New endpoint + deviceDetails.PUT("/:id/port-assignments", c.updatePortAssignments) // New endpoint } } +func (c *DeviceDetailsController) assignCustomer(ctx *gin.Context) { + id := ctx.Param("id") + deviceID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID") + return + } + + var request struct { + CustomerName string `json:"customer_name"` + PortNumber *int `json:"port_number"` + } + + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.deviceDetailsUC.AssignCustomerToPort(deviceID, request.CustomerName, request.PortNumber) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Customer assigned successfully", nil) +} + + func (c *DeviceDetailsController) getAllDeviceDetails(ctx *gin.Context) { devices, err := c.deviceDetailsUC.GetAllDeviceDetails() if err != nil { @@ -133,4 +164,50 @@ func (c *DeviceDetailsController) recalculatePortUsage(ctx *gin.Context) { } common.SingleResponses(ctx, "Port usage recalculated successfully", nil) +} + +func (c *DeviceDetailsController) updatePortUsage(ctx *gin.Context) { + id := ctx.Param("id") + deviceID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID") + return + } + + var request req.UpdatePortUsageDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.deviceDetailsUC.UpdatePortUsage(deviceID, request.PortUsed) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Port usage updated successfully", nil) +} + +func (c *DeviceDetailsController) updatePortAssignments(ctx *gin.Context) { + id := ctx.Param("id") + deviceID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID") + return + } + + var request req.UpdatePortAssignmentsDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.deviceDetailsUC.UpdatePortAssignments(deviceID, request.PortAssignments) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Port assignments updated successfully", nil) } \ No newline at end of file diff --git a/delivery/controller/fishbone_controller.go b/delivery/controller/fishbone_controller.go index 55cfaee..5d6f732 100644 --- a/delivery/controller/fishbone_controller.go +++ b/delivery/controller/fishbone_controller.go @@ -26,6 +26,7 @@ func (fc *FishboneController) Route() { rg.POST("", fc.CreateFishbone()) rg.GET("/:uuid", fc.GetFishboneByID()) rg.PUT("/:uuid", fc.UpdateFishbone()) + rg.GET("/backbone/:backbone_id", fc.GetFishboneByBackboneID()) } } @@ -36,6 +37,30 @@ func NewFishboneController(fu usecase.FishboneUseCase, rg *gin.RouterGroup) *Fis } } +func (fc *FishboneController) GetFishboneByBackboneID() gin.HandlerFunc { + return func(c *gin.Context) { + backboneIDStr := c.Param("backbone_id") + backboneID, err := uuid.Parse(backboneIDStr) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid backbone ID format") + return + } + + fishbones, err := fc.fu.GetByBackboneID(backboneID) + if err != nil { + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + return + } + + if len(fishbones) == 0 { + common.SingleResponses(c, "No fishbones found for this backbone", []interface{}{}) + return + } + + common.SingleResponses(c, "Success", fishbones) + } +} + func (fc *FishboneController) GetFishbone() gin.HandlerFunc { return func(c *gin.Context) { log.Println("Fetching all fishbones") diff --git a/delivery/controller/tower_controller.go b/delivery/controller/tower_controller.go index 61587a7..b787093 100644 --- a/delivery/controller/tower_controller.go +++ b/delivery/controller/tower_controller.go @@ -3,6 +3,7 @@ package controller import ( "net/http" "strconv" + "strings" "users_management/m/middleware" "users_management/m/model/dto/req" "users_management/m/usecase" @@ -51,6 +52,27 @@ func (tc *TowerController) GetTower() gin.HandlerFunc { func (tc *TowerController) CreateTower() gin.HandlerFunc { return func(c *gin.Context) { + + contentType := c.GetHeader("Content-Type") + + // Handle JSON request + if strings.Contains(contentType, "application/json") { + var towerDTO req.TowerDTO + if err := c.ShouldBindJSON(&towerDTO); err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid JSON format: "+err.Error()) + return + } + + // No image file for JSON requests + err := tc.tu.Post(towerDTO, nil) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(c, "Tower has been created", nil) + return + } // Parse multipart form err := c.Request.ParseMultipartForm(10 << 20) // 10MB max if err != nil { @@ -58,6 +80,8 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc { return } + + // Extract form data deviceIDStr := c.PostForm("dev_id") towerCode := c.PostForm("tower_code") @@ -148,14 +172,35 @@ func (tc *TowerController) GetTowerByID() gin.HandlerFunc { func (tc *TowerController) UpdateTower() gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("uuid") - + tower_uuid, err := uuid.Parse(id) if err != nil { common.ErrorResponses(c, http.StatusBadRequest, err.Error()) return } - // Parse multipart form + contentType := c.GetHeader("Content-Type") + + // Handle JSON request + if strings.Contains(contentType, "application/json") { + var towerUpdateDTO req.UpdateTowerDTO + if err := c.ShouldBindJSON(&towerUpdateDTO); err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid JSON format: "+err.Error()) + return + } + + // No image file for JSON requests + err := tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, nil) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(c, "Tower has been updated", nil) + return + } + + // Handle multipart form request (existing logic) err = c.Request.ParseMultipartForm(10 << 20) // 10MB max if err != nil { common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form") @@ -165,10 +210,13 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc { // Create update DTO towerUpdateDTO := req.UpdateTowerDTO{} - // Optional fields + // Optional fields - handle form data if deviceIDStr := c.PostForm("device_id"); deviceIDStr != "" { if deviceID, err := uuid.Parse(deviceIDStr); err == nil { towerUpdateDTO.DeviceID = &deviceID + } else { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID format") + return } } @@ -179,15 +227,36 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc { if longitudeStr := c.PostForm("longitude"); longitudeStr != "" { if longitude, err := strconv.ParseFloat(longitudeStr, 64); err == nil { towerUpdateDTO.Longitude = &longitude + } else { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid longitude format") + return } } if latitudeStr := c.PostForm("latitude"); latitudeStr != "" { if latitude, err := strconv.ParseFloat(latitudeStr, 64); err == nil { towerUpdateDTO.Latitude = &latitude + } else { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid latitude format") + return } } + // Handle external_tower field + if externalTowerStr := c.PostForm("external_tower"); externalTowerStr != "" { + if externalTower, err := strconv.ParseBool(externalTowerStr); err == nil { + towerUpdateDTO.ExternalTower = &externalTower + } else { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid external_tower value") + return + } + } + + // Handle image_url field (for updating image URL directly) + if imageURL := c.PostForm("image_url"); imageURL != "" { + towerUpdateDTO.ImageURL = &imageURL + } + // Get image file (optional) imageFile, _ := c.FormFile("image") diff --git a/delivery/server.go b/delivery/server.go index 7018172..129258d 100644 --- a/delivery/server.go +++ b/delivery/server.go @@ -51,7 +51,6 @@ func (s *Server) setupController() { controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route() rg.Use(middleware.AuthMiddleware(s.ucManager.NewUserUsecase())) rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase())) - rg.Use(middleware.RateLimitMiddleware()) { controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route() controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), rg).Route() diff --git a/model/dto/req/devicePort_dto.go b/model/dto/req/devicePort_dto.go index b4c0ec5..d13801f 100644 --- a/model/dto/req/devicePort_dto.go +++ b/model/dto/req/devicePort_dto.go @@ -10,4 +10,23 @@ type DevicePort struct { type UpdateDevicePort struct { DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"` PortNumber *int `json:"port_number,omitempty" validate:"omitempty,min=1"` +} + + +type AssignCustomerToPortDTO struct { + CustomerName string `json:"customer_name" binding:"required"` + PortNumber *int `json:"port_number,omitempty"` // Optional, will auto-assign if not provided +} + +type UpdatePortUsageDTO struct { + PortUsed int `json:"port_used" binding:"required,min=0"` +} + +type PortAssignmentDTO struct { + PortNumber int `json:"port_number" binding:"required,min=1"` + CustomerName *string `json:"customer_name,omitempty"` +} + +type UpdatePortAssignmentsDTO struct { + PortAssignments []PortAssignmentDTO `json:"port_assignments" binding:"required"` } \ No newline at end of file diff --git a/model/dto/res/device_details.go b/model/dto/res/device_details.go index 47396ef..12de463 100644 --- a/model/dto/res/device_details.go +++ b/model/dto/res/device_details.go @@ -6,29 +6,31 @@ import ( ) type DeviceDetailsResponse struct { - ID uuid.UUID `json:"id"` - DeviceCode string `json:"device_code"` - DeviceType string `json:"device_type"` - Address string `json:"address"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - Status string `json:"status"` - PortAmount int `json:"port_amount"` - PortUsed int `json:"port_used"` - PortAvailable int `json:"port_available"` - Region *string `json:"region,omitempty"` - Province *string `json:"province,omitempty"` - City *string `json:"city,omitempty"` - District *string `json:"district,omitempty"` - ImageURL *string `json:"image_url,omitempty"` + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType string `json:"device_type"` + Address string `json:"address"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Status string `json:"status"` + PortAmount int `json:"port_amount"` + PortUsed int `json:"port_used"` + PortAvailable int `json:"port_available"` + CustomerNames []string `json:"customer_names"` // Just customer names + PortAssignments []PortAssignmentResponse `json:"port_assignments"` // Port details with customers + Region *string `json:"region,omitempty"` + Province *string `json:"province,omitempty"` + City *string `json:"city,omitempty"` + District *string `json:"district,omitempty"` + ImageURL *string `json:"image_url,omitempty"` // Connection details - Backbones []BackboneConnectionInfo `json:"backbones"` - Fishbones []FishboneConnectionInfo `json:"fishbones"` - Towers []TowerConnectionDetail `json:"towers"` + Backbones []BackboneConnectionInfo `json:"backbones"` + Fishbones []FishboneConnectionInfo `json:"fishbones"` + Towers []TowerConnectionDetail `json:"towers"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type TowerConnectionDetail struct { @@ -37,4 +39,10 @@ type TowerConnectionDetail struct { Distance float64 `json:"distance_km"` // Distance from tower to device ExternalTower *bool `json:"external_tower"` // Indicates if this is an external tower ImageURL *string `json:"image_url,omitempty"` +} + +type PortAssignmentResponse struct { + PortNumber int `json:"port_number"` + CustomerName *string `json:"customer_name"` + IsOccupied bool `json:"is_occupied"` } \ No newline at end of file diff --git a/model/dto/res/device_res.go b/model/dto/res/device_res.go index 42d5b0b..d7ff642 100644 --- a/model/dto/res/device_res.go +++ b/model/dto/res/device_res.go @@ -15,6 +15,7 @@ type DeviceResponse struct { Address string `json:"address"` PortAmount int `json:"port_amount"` Status string `json:"status"` // Always include, even if null + CustomerNames []string `json:"customer_names"` // Always include, even if empty Province *string `json:"province"` // Always include, even if null City *string `json:"city"` // Always include, even if null District *string `json:"district"` // Always include, even if null diff --git a/model/dto/res/tower_res.go b/model/dto/res/tower_res.go index d97589b..61aeff6 100644 --- a/model/dto/res/tower_res.go +++ b/model/dto/res/tower_res.go @@ -13,7 +13,7 @@ type TowerResponse struct { Latitude float64 `json:"latitude"` Address string `json:"address"` ImageURL string `json:"image_url"` - ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable + ExternalTower *bool `json:"external_tower"` // Make nullable CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } \ No newline at end of file diff --git a/model/entity/device_port.go b/model/entity/device_port.go index ad101fc..e4b9a42 100644 --- a/model/entity/device_port.go +++ b/model/entity/device_port.go @@ -1,24 +1,267 @@ package entity import ( - "time" - "github.com/google/uuid" + "database/sql/driver" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/google/uuid" ) +type StringSlice []string + +func (s StringSlice) Value() (driver.Value, error) { + if len(s) == 0 { + return "[]", nil + } + return json.Marshal(s) +} + +func (s *StringSlice) Scan(value interface{}) error { + if value == nil { + *s = StringSlice{} + return nil + } + + var bytes []byte + switch v := value.(type) { + case []byte: // []byte and []uint8 are the same type in Go + bytes = v + case string: + bytes = []byte(v) + default: + return fmt.Errorf("cannot scan %T into StringSlice", value) + } + + // Handle empty string case + if len(bytes) == 0 { + *s = StringSlice{} + return nil + } + + return json.Unmarshal(bytes, s) +} + +type PortAssignment struct { + PortNumber int `json:"port_number"` + CustomerName *string `json:"customer_name"` // Nullable for empty ports +} + +type PortAssignmentResponse struct { + PortNumber int `json:"port_number"` + CustomerName *string `json:"customer_name"` + IsOccupied bool `json:"is_occupied"` +} + +type PortAssignments []PortAssignment + +func (p PortAssignments) Value() (driver.Value, error) { + if len(p) == 0 { + return "[]", nil + } + return json.Marshal(p) +} + +func (p *PortAssignments) Scan(value interface{}) error { + if value == nil { + *p = PortAssignments{} + return nil + } + + var bytes []byte + switch v := value.(type) { + case []byte: // []byte and []uint8 are the same type in Go + bytes = v + case string: + bytes = []byte(v) + default: + return fmt.Errorf("cannot scan %T into PortAssignments", value) + } + if len(bytes) == 0 { + *p = PortAssignments{} + return nil + } + + return json.Unmarshal(bytes, p) +} + type DevicePort struct { - ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` - DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"` - PortUsed int `json:"port_used"` - PortAvailable int `json:"port_available"` - CustomerCount int `json:"customer_count"` // Add this field - CustomerNames []string `json:"customer_names" gorm:"type:json"` // Store customer names as JSON array - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"` + PortUsed int `json:"port_used"` + PortAvailable int `json:"port_available"` + CustomerCount int `json:"customer_count"` + CustomerNames StringSlice `json:"customer_names" gorm:"type:jsonb"` + PortAssignments PortAssignments `json:"port_assignments" gorm:"type:jsonb"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Relationships Device Device `json:"device" gorm:"foreignKey:DeviceID"` } +// Helper method to get customer names with port numbers +func (dp *DevicePort) GetCustomerNamesWithPorts() []string { + var result []string + for _, assignment := range dp.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + result = append(result, fmt.Sprintf("%d: %s", assignment.PortNumber, *assignment.CustomerName)) + } + } + // If no port assignments, fall back to customer names + if len(result) == 0 { + for i, name := range dp.CustomerNames { + if name != "" { + result = append(result, fmt.Sprintf("%d: %s", i+1, name)) + } + } + } + return result +} + +// Helper method to get only customer names +func (dp *DevicePort) GetCustomerNames() []string { + var result []string + for _, assignment := range dp.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + result = append(result, *assignment.CustomerName) + } + } + // If no port assignments, fall back to customer names + if len(result) == 0 { + for _, name := range dp.CustomerNames { + if name != "" { + result = append(result, name) + } + } + } + return result +} + +func (dp *DevicePort) GetPortAssignmentsWithDetails() []PortAssignmentResponse { + // If both port_used and port_available are 0, return empty array + if dp.PortUsed == 0 && dp.PortAvailable == 0 { + return []PortAssignmentResponse{} + } + + // If port_used is 0 but port_available > 0, still return empty array + // This means the device has capacity but no ports are currently used + if dp.PortUsed == 0 { + return []PortAssignmentResponse{} + } + + var result []PortAssignmentResponse + + // Step 1: Create a map of all port assignments + portMap := make(map[int]*string) + maxPort := 0 + + if len(dp.PortAssignments) > 0 { + for _, assignment := range dp.PortAssignments { + portMap[assignment.PortNumber] = assignment.CustomerName + if assignment.PortNumber > maxPort { + maxPort = assignment.PortNumber + } + } + } else { + // Fallback: use CustomerNames array + for i, name := range dp.CustomerNames { + if name != "" { + portMap[i+1] = &name + } + if i+1 > maxPort { + maxPort = i + 1 + } + } + } + + // If no ports have assignments and port_used > 0, create empty assignments up to port_used + if maxPort == 0 && dp.PortUsed > 0 { + maxPort = dp.PortUsed + } + + // Step 2: Determine which ports should be occupied + // First, collect all ports that have customers + portsWithCustomers := make([]int, 0) + for portNum, customerName := range portMap { + if customerName != nil && *customerName != "" { + portsWithCustomers = append(portsWithCustomers, portNum) + } + } + + // Sort to get them in order + sort.Ints(portsWithCustomers) + + // Step 3: Determine occupied ports + occupiedPorts := make(map[int]bool) + + // All ports with customers are occupied + for _, portNum := range portsWithCustomers { + occupiedPorts[portNum] = true + } + + // Fill remaining slots to reach port_used + occupiedCount := len(portsWithCustomers) + if occupiedCount < dp.PortUsed { + // Need to occupy more ports (ports without customer names but still occupied) + for i := 1; i <= maxPort && occupiedCount < dp.PortUsed; i++ { + if !occupiedPorts[i] { + occupiedPorts[i] = true + occupiedCount++ + } + } + + // If we still haven't reached port_used, extend maxPort + for i := maxPort + 1; occupiedCount < dp.PortUsed; i++ { + occupiedPorts[i] = true + occupiedCount++ + maxPort = i + } + } + + // Step 4: Create the result only for ports that are either occupied or have customers + for i := 1; i <= maxPort; i++ { + // Only include ports that are occupied or have customers + if occupiedPorts[i] || (portMap[i] != nil && *portMap[i] != "") { + var customerName *string + if portMap[i] != nil { + customerName = portMap[i] + } + + result = append(result, PortAssignmentResponse{ + PortNumber: i, + CustomerName: customerName, + IsOccupied: occupiedPorts[i], + }) + } + } + + return result +} +func (dp *DevicePort) GetCustomerNamesOnly() []string { + var result []string + + // If we have PortAssignments data, use it + if len(dp.PortAssignments) > 0 { + for _, assignment := range dp.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + result = append(result, *assignment.CustomerName) + } + } + } else { + // Fallback: use CustomerNames directly + for _, name := range dp.CustomerNames { + if name != "" { + result = append(result, name) + } + } + } + + return result +} + func (DevicePort) TableName() string { return "device_ports" } \ No newline at end of file diff --git a/repository/device_details.go b/repository/device_details.go index 9e82370..6b145e5 100644 --- a/repository/device_details.go +++ b/repository/device_details.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "time" + "users_management/m/model/dto/req" "users_management/m/model/entity" "github.com/google/uuid" @@ -28,8 +29,11 @@ type DeviceDetailsRepo interface { // Validation helpers GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error) - AssignCustomerToPort(deviceID uuid.UUID, customerName string) error + AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error RemoveCustomerFromPort(deviceID uuid.UUID, customerName string) error + UpdatePortUsage(deviceID uuid.UUID, portUsed int) error + UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error + } type deviceDetailsRepo struct { @@ -42,6 +46,83 @@ func NewDeviceDetailsRepo(db *gorm.DB) DeviceDetailsRepo { } } +func (r *deviceDetailsRepo) UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // Lock both device and device_port records + var device entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", deviceID).First(&device).Error; err != nil { + return err + } + + // Only ODP devices can have port assignments + if device.DeviceType != "ODP" { + return fmt.Errorf("port assignments can only be updated for ODP devices") + } + + var devicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { + return fmt.Errorf("device port record not found: %w", err) + } + + // Validate port numbers are within device capacity + for _, assignment := range assignments { + if assignment.PortNumber < 1 || assignment.PortNumber > device.PortAmount { + return fmt.Errorf("port number %d is out of range (1-%d)", assignment.PortNumber, device.PortAmount) + } + } + + // Initialize port assignments array if needed + if len(devicePort.PortAssignments) == 0 { + devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount) + for i := 0; i < device.PortAmount; i++ { + devicePort.PortAssignments[i] = entity.PortAssignment{ + PortNumber: i + 1, + CustomerName: nil, + } + } + } + + // Check for duplicate customer names (if not null) + customerNames := make(map[string]int) // map customer name to port number + for _, assignment := range assignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + if existingPort, exists := customerNames[*assignment.CustomerName]; exists { + return fmt.Errorf("customer %s is assigned to multiple ports (%d and %d)", + *assignment.CustomerName, existingPort, assignment.PortNumber) + } + customerNames[*assignment.CustomerName] = assignment.PortNumber + } + } + + // Update port assignments + for _, assignment := range assignments { + portIndex := assignment.PortNumber - 1 + if portIndex < len(devicePort.PortAssignments) { + devicePort.PortAssignments[portIndex].PortNumber = assignment.PortNumber + devicePort.PortAssignments[portIndex].CustomerName = assignment.CustomerName + } + } + + // Update counters and backward compatibility fields + r.updateDevicePortCounters(&devicePort) + + // Calculate port usage based on assignments + portUsed := 0 + for _, assignment := range devicePort.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + portUsed++ + } + } + + devicePort.PortUsed = portUsed + devicePort.PortAvailable = device.PortAmount - portUsed + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) +} func (r *deviceDetailsRepo) Create(device entity.Device) error { @@ -154,111 +235,64 @@ func (r *deviceDetailsRepo) updatePortAmountCascade(tx *gorm.DB, deviceID uuid.U }).Error } -func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error { +func (r *deviceDetailsRepo) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error { return r.db.Transaction(func(tx *gorm.DB) error { - // Lock the device record to prevent race conditions var device entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", deviceID).First(&device).Error; err != nil { return err } - - var portUsed int - var customerCount int - var customerNames []string - - switch device.DeviceType { - case "OTB": - // For OTB: count backbones (each backbone uses 1 port) - var backboneCount int64 - if err := tx.Model(&entity.Backbone{}). - Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). - Count(&backboneCount).Error; err != nil { - return err - } - portUsed = int(backboneCount) - customerCount = 0 // OTB doesn't serve customers directly - - case "closure": - // For closure: count fishbones where this device is the start device - var fishboneCount int64 - if err := tx.Model(&entity.Fishbone{}). - Where("dev_start_id = ?", deviceID). - Count(&fishboneCount).Error; err != nil { - return err - } - portUsed = int(fishboneCount) - customerCount = 0 // Closure doesn't serve customers directly - - case "ODP": - // For ODP: sum fishbone core amounts where this device is the end device - var totalCores int64 - if err := tx.Model(&entity.Fishbone{}). - Where("dev_end_id = ?", deviceID). - Select("COALESCE(SUM(core_amount), 0)"). - Scan(&totalCores).Error; err != nil { - return err - } - portUsed = int(totalCores) - - // For ODP: customer count should equal port_used - // Get existing customer assignments - var existingDevicePort entity.DevicePort - if err := tx.Where("device_id = ?", deviceID).First(&existingDevicePort).Error; err == nil { - customerNames = existingDevicePort.CustomerNames - } - - // Ensure customer count matches port_used for ODP - customerCount = portUsed - - // If we have more customers than ports used, trim the list - if len(customerNames) > portUsed { - customerNames = customerNames[:portUsed] - } - - default: - portUsed = 0 - customerCount = 0 - } - - // Calculate port available - portAvailable := device.PortAmount - portUsed - if portAvailable < 0 { - portAvailable = 0 + + if portUsed > device.PortAmount { + return fmt.Errorf("port_used (%d) cannot exceed port_amount (%d)", portUsed, device.PortAmount) } - // Update or create DevicePort record with locking - result := tx.Set("gorm:query_option", "FOR UPDATE"). - Model(&entity.DevicePort{}). - Where("device_id = ?", deviceID). - Updates(map[string]interface{}{ - "port_used": portUsed, - "port_available": portAvailable, - "customer_count": customerCount, - "customer_names": customerNames, - "updated_at": gorm.Expr("NOW()"), - }) - - if result.Error != nil { - return result.Error + var devicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { + return fmt.Errorf("device port record not found: %w", err) } - // If no record was updated, create a new one - if result.RowsAffected == 0 { - devicePort := entity.DevicePort{ - ID: uuid.New(), - DeviceID: deviceID, - PortUsed: portUsed, - PortAvailable: portAvailable, - CustomerCount: customerCount, - CustomerNames: customerNames, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + // Initialize port assignments if empty + if len(devicePort.PortAssignments) == 0 { + devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount) + for i := 0; i < device.PortAmount; i++ { + devicePort.PortAssignments[i] = entity.PortAssignment{ + PortNumber: i + 1, + CustomerName: nil, + } } - return tx.Create(&devicePort).Error } - return nil + // Update port assignments based on new port_used value + currentCustomerCount := 0 + for _, assignment := range devicePort.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + currentCustomerCount++ + } + } + + if portUsed < currentCustomerCount { + // Need to remove some customers (keep first N customers) + customersKept := 0 + for i := range devicePort.PortAssignments { + if devicePort.PortAssignments[i].CustomerName != nil && *devicePort.PortAssignments[i].CustomerName != "" { + if customersKept < portUsed { + customersKept++ + } else { + devicePort.PortAssignments[i].CustomerName = nil + } + } + } + } + + // Update counters + devicePort.PortUsed = portUsed + devicePort.PortAvailable = device.PortAmount - portUsed + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error }) } @@ -320,7 +354,7 @@ func (r *deviceDetailsRepo) Delete(id uuid.UUID) error { }) } -func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerName string) error { +func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error { return r.db.Transaction(func(tx *gorm.DB) error { // Lock both device and device_port records var device entity.Device @@ -340,22 +374,57 @@ func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerNam return fmt.Errorf("device port record not found: %w", err) } - // Check if there are available ports - if devicePort.PortAvailable <= 0 { - return fmt.Errorf("no available ports for customer assignment") - } - - // Check if customer is already assigned - for _, existing := range devicePort.CustomerNames { - if existing == customerName { - return fmt.Errorf("customer %s is already assigned to this device", customerName) + // Initialize port assignments if empty + if len(devicePort.PortAssignments) == 0 { + devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount) + for i := 0; i < device.PortAmount; i++ { + devicePort.PortAssignments[i] = entity.PortAssignment{ + PortNumber: i + 1, + CustomerName: nil, + } } } - // Add customer to the list - devicePort.CustomerNames = append(devicePort.CustomerNames, customerName) - devicePort.CustomerCount = len(devicePort.CustomerNames) - devicePort.PortAvailable = devicePort.PortAvailable - 1 + // Determine port number to use + var targetPortNumber int + if portNumber != nil { + targetPortNumber = *portNumber + if targetPortNumber < 1 || targetPortNumber > device.PortAmount { + return fmt.Errorf("port number must be between 1 and %d", device.PortAmount) + } + } else { + // Auto-assign to first available port + targetPortNumber = -1 + for i, assignment := range devicePort.PortAssignments { + if assignment.CustomerName == nil || *assignment.CustomerName == "" { + targetPortNumber = i + 1 + break + } + } + if targetPortNumber == -1 { + return fmt.Errorf("no available ports for customer assignment") + } + } + + // Check if the specified port is already occupied + portIndex := targetPortNumber - 1 + if devicePort.PortAssignments[portIndex].CustomerName != nil && + *devicePort.PortAssignments[portIndex].CustomerName != "" { + return fmt.Errorf("port %d is already occupied by %s", targetPortNumber, *devicePort.PortAssignments[portIndex].CustomerName) + } + + // Check if customer is already assigned to another port + for i, assignment := range devicePort.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName == customerName { + return fmt.Errorf("customer %s is already assigned to port %d", customerName, i+1) + } + } + + // Assign customer to port + devicePort.PortAssignments[portIndex].CustomerName = &customerName + + // Update both PortAssignments and CustomerNames for backward compatibility + r.updateDevicePortCounters(&devicePort) devicePort.UpdatedAt = time.Now() return tx.Save(&devicePort).Error @@ -392,4 +461,37 @@ func (r *deviceDetailsRepo) RemoveCustomerFromPort(deviceID uuid.UUID, customerN return tx.Save(&devicePort).Error }) -} \ No newline at end of file +} + +func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error { + // This method is required by the DeviceDetailsRepo interface + return r.db.Transaction(func(tx *gorm.DB) error { + var devicePort entity.DevicePort + if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { + return fmt.Errorf("device port record not found: %w", err) + } + + // Update based on port assignments + r.updateDevicePortCounters(&devicePort) + devicePort.PortUsed = devicePort.CustomerCount + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) +} + +func (r *deviceDetailsRepo) updateDevicePortCounters(devicePort *entity.DevicePort) { + // Count actual customers and create customer names list + customerCount := 0 + customerNames := make([]string, 0) + + for _, assignment := range devicePort.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + customerCount++ + customerNames = append(customerNames, *assignment.CustomerName) + } + } + + devicePort.CustomerCount = customerCount + devicePort.CustomerNames = customerNames +} diff --git a/repository/fishbone_repo.go b/repository/fishbone_repo.go index 46d0953..27566d6 100644 --- a/repository/fishbone_repo.go +++ b/repository/fishbone_repo.go @@ -24,6 +24,7 @@ type FishboneRepo interface { CheckDeviceExists(id uuid.UUID) (bool, error) Delete(id uuid.UUID) error WithTransaction(fn func(*gorm.DB) error) error + GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error) } type fishboneRepo struct { @@ -44,6 +45,16 @@ func (r *fishboneRepo) Post(fishbone entity.Fishbone) error { return nil } +func (r *fishboneRepo) GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error) { + var fishbones []entity.Fishbone + err := r.db.Where("bb_id = ?", backboneID). + Preload("DeviceStart"). + Preload("DeviceEnd"). + Preload("Backbone"). + Find(&fishbones).Error + return fishbones, err +} + func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) { log.Print("Fetching all fishbones with relations") var fishbones []entity.Fishbone diff --git a/usecase/backbone_usecase.go b/usecase/backbone_usecase.go index ecfcf11..6ba58ae 100644 --- a/usecase/backbone_usecase.go +++ b/usecase/backbone_usecase.go @@ -158,7 +158,7 @@ func (u *backboneUseCase) GetByID(id uuid.UUID) (res.BackboneResponse, error) { } return backboneResp, nil -} +} func (u *backboneUseCase) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackboneDTO) error { err := u.validate.Struct(backbone) diff --git a/usecase/device_details.go b/usecase/device_details.go index bfecb26..064feef 100644 --- a/usecase/device_details.go +++ b/usecase/device_details.go @@ -1,18 +1,18 @@ package usecase import ( - "errors" - "fmt" - "time" - "users_management/m/model/dto/req" - "users_management/m/model/dto/res" - "users_management/m/model/entity" - "users_management/m/repository" - "users_management/m/utils/helper" - "users_management/m/utils/service" - - "github.com/go-playground/validator/v10" - "github.com/google/uuid" + "errors" + "fmt" + "time" + "users_management/m/model/dto/req" + "users_management/m/model/dto/res" + "users_management/m/model/entity" + "users_management/m/repository" + "users_management/m/utils/helper" + "users_management/m/utils/service" + + "github.com/go-playground/validator/v10" + "github.com/google/uuid" ) type DeviceDetailsUseCase interface { @@ -25,6 +25,9 @@ type DeviceDetailsUseCase interface { // Port management // ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error RecalculatePortUsage(deviceID uuid.UUID) error + AssignCustomerToPort(deviceID uuid.UUID, customerName string,portNumber *int) error + UpdatePortUsage(deviceID uuid.UUID, portUsed int) error + UpdatePortAssignments(deviceID uuid.UUID, portAssignments []req.PortAssignmentDTO) error } type deviceDetailsUseCase struct { @@ -41,6 +44,32 @@ func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, geo } } + +func (u *deviceDetailsUseCase) UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error { + // Validate the assignments + if len(assignments) == 0 { + return errors.New("port assignments cannot be empty") + } + + // Check for duplicate port numbers + portNumbers := make(map[int]bool) + for _, assignment := range assignments { + if portNumbers[assignment.PortNumber] { + return fmt.Errorf("duplicate port number: %d", assignment.PortNumber) + } + portNumbers[assignment.PortNumber] = true + } + + return u.deviceDetailsRepo.UpdatePortAssignments(deviceID, assignments) +} + + +func (u *deviceDetailsUseCase) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error { + return u.deviceDetailsRepo.UpdatePortUsage(deviceID, portUsed) +} + + + func (u *deviceDetailsUseCase) CreateDeviceDetails(deviceDTO req.DeviceDetailsDTO) error { err := u.validate.Struct(deviceDTO) if err != nil { @@ -173,4 +202,8 @@ func (u *deviceDetailsUseCase) DeleteDeviceDetails(id uuid.UUID) error { func (u *deviceDetailsUseCase) RecalculatePortUsage(deviceID uuid.UUID) error { return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) +} + +func (u *deviceDetailsUseCase) AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error { + return u.deviceDetailsRepo.AssignCustomerToPort(deviceID, customerName, portNumber) } \ No newline at end of file diff --git a/usecase/fishbone_usecase.go b/usecase/fishbone_usecase.go index 5a62fba..3745040 100644 --- a/usecase/fishbone_usecase.go +++ b/usecase/fishbone_usecase.go @@ -22,6 +22,7 @@ type FishboneUseCase interface { UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error DeleteFishbone(id uuid.UUID) error GetFishboneStats() (map[string]interface{}, error) + GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error) } type fishboneUseCase struct { @@ -40,6 +41,20 @@ func NewFishboneUseCase(fishboneRepo repository.FishboneRepo, backboneRepo repos } } +func (u *fishboneUseCase) GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error) { + fishbones, err := u.fishboneRepo.GetByBackboneID(backboneID) + if err != nil { + return nil, err + } + + fishboneResp, err := helper.ConvertToFishboneResponses(fishbones, nil) + if err != nil { + return nil, err + } + + return fishboneResp, nil +} + func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { diff --git a/usecase/tower_usecase.go b/usecase/tower_usecase.go index 9b822c5..3601a22 100644 --- a/usecase/tower_usecase.go +++ b/usecase/tower_usecase.go @@ -127,6 +127,9 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, image if tower.Latitude != nil { updates["Latitude"] = *tower.Latitude } + if tower.ExternalTower != nil { + updates["ExternalTower"] = *tower.ExternalTower + } // Handle image upload if imageFile != nil { diff --git a/utils/helper/device_details.go b/utils/helper/device_details.go index 858c2ca..8d4f89f 100644 --- a/utils/helper/device_details.go +++ b/utils/helper/device_details.go @@ -108,28 +108,108 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic } towerInfos = append(towerInfos, info) } + + // Convert entity.PortAssignmentResponse to res.PortAssignmentResponse + entityPortAssignments := device.DevicePort.GetPortAssignmentsWithDetails() + resPortAssignments := make([]res.PortAssignmentResponse, len(entityPortAssignments)) + for i, pa := range entityPortAssignments { + resPortAssignments[i] = res.PortAssignmentResponse{ + PortNumber: pa.PortNumber, + CustomerName: pa.CustomerName, + IsOccupied: pa.IsOccupied, + } + } + portAssignments := device.DevicePort.GetPortAssignmentsWithDetails() + + // Ensure we show all ports up to PortAmount + portAssignmentMap := make(map[int]res.PortAssignmentResponse) + for _, pa := range portAssignments { + portAssignmentMap[pa.PortNumber] = res.PortAssignmentResponse{ + PortNumber: pa.PortNumber, + CustomerName: pa.CustomerName, + IsOccupied: pa.IsOccupied, + } + } + + // Fill in missing ports + finalPortAssignments := make([]res.PortAssignmentResponse, 0) + for i := 1; i <= device.PortAmount; i++ { + if pa, exists := portAssignmentMap[i]; exists { + finalPortAssignments = append(finalPortAssignments, pa) + } 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{ + PortNumber: i, + CustomerName: nil, + IsOccupied: isOccupied, + }) + } + } + + // If port_used is 0, return empty port assignments + if device.DevicePort.PortUsed == 0 { + finalPortAssignments = []res.PortAssignmentResponse{} + } 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, + } + } + } response := res.DeviceDetailsResponse{ - ID: device.ID, - DeviceCode: device.DeviceCode, - DeviceType: string(device.DeviceType), - Address: address, - Longitude: device.Longitude, - Latitude: device.Latitude, - Status: string(device.Status), - PortAmount: device.PortAmount, - PortUsed: device.DevicePort.PortUsed, + ID: device.ID, + DeviceCode: device.DeviceCode, + DeviceType: string(device.DeviceType), + Address: address, + Longitude: device.Longitude, + Latitude: device.Latitude, + Status: string(device.Status), + PortAmount: device.PortAmount, + PortUsed: device.DevicePort.PortUsed, PortAvailable: device.DevicePort.PortAvailable, - Region: device.Region, - Province: device.Province, - City: device.City, - District: device.District, - ImageURL: device.ImageURL, - Backbones: backboneInfos, - Fishbones: fishboneInfos, - Towers: towerInfos, - CreatedAt: device.CreatedAt, - UpdatedAt: device.UpdatedAt, + CustomerNames: device.DevicePort.GetCustomerNamesOnly(), // Just customer names + PortAssignments: finalPortAssignments, // Use finalPortAssignments instead of resPortAssignments + Region: device.Region, + Province: device.Province, + City: device.City, + District: device.District, + ImageURL: device.ImageURL, + Backbones: backboneInfos, + Fishbones: fishboneInfos, + Towers: towerInfos, + CreatedAt: device.CreatedAt, + UpdatedAt: device.UpdatedAt, } return response, nil