package entity import ( "database/sql/driver" "encoding/json" "fmt" "sort" "time" "github.com/google/uuid" ) type PortStatus string const ( PortStatusOn PortStatus = "on" PortStatusOff PortStatus = "off" PortStatusDyingGasp PortStatus = "dyingGasp" PortStatusLOS PortStatus = "los" ) 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 IsOccupied bool `json:"is_occupied"` // Explicitly store occupation status Status PortStatus `json:"status"` // Add status field Bandwidth *string `json:"bandwidth"` // Add bandwidth field (nullable) } type PortAssignmentResponse struct { PortNumber int `json:"port_number"` CustomerName *string `json:"customer_name"` IsOccupied bool `json:"is_occupied"` Status PortStatus `json:"status"` // Add status field Bandwidth *string `json:"bandwidth"` // Add bandwidth field } 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: 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"` 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 { if dp.Device.DeviceType == "OTB" { return []string{} // OTB devices do not have port assignments } 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 { if dp.Device.DeviceType == "OTB" { return []string{} // OTB devices do not have port assignments } 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 port_used is 0, return empty array if dp.Device.DeviceType == "OTB" { return []PortAssignmentResponse{} } if dp.PortUsed == 0 { return []PortAssignmentResponse{} } var result []PortAssignmentResponse // Step 1: Create a map of all port assignments portMap := make(map[int]*PortAssignment) maxPort := 0 if len(dp.PortAssignments) > 0 { for _, assignment := range dp.PortAssignments { portMap[assignment.PortNumber] = &assignment if assignment.PortNumber > maxPort { maxPort = assignment.PortNumber } } } else { // Fallback: use CustomerNames array if PortAssignments is empty for i, name := range dp.CustomerNames { portNum := i + 1 if name != "" || portNum <= dp.PortUsed { var customerName *string if name != "" { customerName = &name } assignment := PortAssignment{ PortNumber: portNum, CustomerName: customerName, Status: PortStatusOn, // Default status Bandwidth: nil, // Default bandwidth } portMap[portNum] = &assignment if portNum > maxPort { maxPort = portNum } } } } // Ensure maxPort is at least equal to port_used if maxPort < dp.PortUsed { maxPort = dp.PortUsed } // Step 2: Determine which ports should be occupied portsWithCustomers := make([]int, 0) for portNum, assignment := range portMap { if assignment.CustomerName != nil && *assignment.CustomerName != "" { portsWithCustomers = append(portsWithCustomers, portNum) } } // Sort to get them in order sort.Ints(portsWithCustomers) // Step 3: Determine occupied ports - USE EXPLICIT IsOccupied FIELD occupiedPorts := make(map[int]bool) // For modern approach: use the explicitly stored IsOccupied field for portNum, assignment := range portMap { // Use the explicitly stored IsOccupied value // If not set, fallback to checking customer name presence isOccupied := assignment.IsOccupied if !isOccupied && assignment.CustomerName != nil && *assignment.CustomerName != "" { isOccupied = true } occupiedPorts[portNum] = isOccupied } // Legacy fallback: if we don't have enough explicit port assignments, // use the old logic only for ports that don't have explicit assignment data currentOccupiedCount := 0 for _, isOccupied := range occupiedPorts { if isOccupied { currentOccupiedCount++ } } // Only apply legacy logic if we have fewer occupied ports than port_used // and only for ports that don't have explicit assignment data if currentOccupiedCount < dp.PortUsed { for i := 1; i <= maxPort && currentOccupiedCount < dp.PortUsed; i++ { if _, hasExplicitData := portMap[i]; !hasExplicitData { if !occupiedPorts[i] { occupiedPorts[i] = true currentOccupiedCount++ } } } } // Step 4: Create the result for ALL ports up to maxPort for i := 1; i <= maxPort; i++ { var customerName *string var status PortStatus = PortStatusOff // Default status for unoccupied ports var bandwidth *string if assignment := portMap[i]; assignment != nil { customerName = assignment.CustomerName status = assignment.Status bandwidth = assignment.Bandwidth } // Override status for legacy occupied ports (those without explicit assignment) if occupiedPorts[i] && portMap[i] == nil { status = PortStatusOn } result = append(result, PortAssignmentResponse{ PortNumber: i, CustomerName: customerName, IsOccupied: occupiedPorts[i], Status: status, Bandwidth: bandwidth, }) } return result } func (dp *DevicePort) GetCustomerNamesOnly() []string { if dp.Device.DeviceType == "OTB" { return []string{} // OTB devices do not have port assignments } 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" }