fix: port assignment bulk update

This commit is contained in:
HasanMu 2025-07-16 08:58:19 +07:00
parent 689505fa6a
commit 0c25a3a941
7 changed files with 1465 additions and 1351 deletions

View File

@ -13,9 +13,9 @@ import (
type Server struct { type Server struct {
ucManager manager.UsecaseManager ucManager manager.UsecaseManager
engine *gin.Engine engine *gin.Engine
host string host string
cfg *config.Config cfg *config.Config
} }
func NewServer() *Server { func NewServer() *Server {
@ -41,66 +41,62 @@ func NewServer() *Server {
return &Server{ return &Server{
ucManager: ucManager, ucManager: ucManager,
engine: engine, engine: engine,
host: host, host: host,
cfg: cfg, cfg: cfg,
} }
} }
func (s *Server) setupController() { func (s *Server) setupController() {
s.engine.Static("/uploads", "./uploads")
rg := s.engine.Group("/api/v1")
// Add bypass controller FIRST - before any auth middleware
controller.NewBypassController(s.ucManager.NewUserUsecase(), rg).Route()
// Add auth controller (public endpoints)
controller.NewAuthController(s.ucManager.NewAuthUsecase(), rg).Route()
publicRegistration := rg.Group("/user-registration") s.engine.Static("/uploads", "./uploads")
{
publicRegistration.POST("/register", controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).RegisterUser) rg := s.engine.Group("/api/v1")
}
// Add bypass controller FIRST - before any auth middleware
controller.NewBypassController(s.ucManager.NewUserUsecase(), rg).Route()
// Add auth controller (public endpoints)
controller.NewAuthController(s.ucManager.NewAuthUsecase(), rg).Route()
publicRegistration := rg.Group("/user-registration")
{
publicRegistration.POST("/register", controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).RegisterUser)
}
// Add user registration controller (public endpoints)
// controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).Route()
// Add user registration controller (public endpoints)
// controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).Route()
// Now apply auth middleware to protected routes // Now apply auth middleware to protected routes
protected := rg.Use(middleware.ConditionalAuthMiddleware(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), s.cfg)).(*gin.RouterGroup) protected := rg.Use(middleware.ConditionalAuthMiddleware(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), s.cfg)).(*gin.RouterGroup)
{ {
protectedRegistration := protected.Group("/user-registration") protectedRegistration := protected.Group("/user-registration")
{ {
protectedRegistration.GET("/pending", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).GetPendingUsers) protectedRegistration.GET("/pending", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).GetPendingUsers)
protectedRegistration.PUT("/approve/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).ApproveUser) protectedRegistration.PUT("/approve/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).ApproveUser)
protectedRegistration.PUT("/reject/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).RejectUser) protectedRegistration.PUT("/reject/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).RejectUser)
} }
controller.NewUserRoleManagementController(s.ucManager.NewUserUsecase(), protected).Route() controller.NewUserRoleManagementController(s.ucManager.NewUserUsecase(), protected).Route()
controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), protected).Route() controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), protected).Route()
controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), protected).Route() controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), protected).Route()
controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), protected, s.cfg).Route() controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), protected, s.cfg).Route()
controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), protected, s.cfg).Route() controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), protected, s.cfg).Route()
controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), protected, s.cfg).Route() controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), protected, s.cfg).Route()
controller.NewTowerController(s.ucManager.NewTowerUsecase(), protected, s.cfg).Route() controller.NewTowerController(s.ucManager.NewTowerUsecase(), protected, s.cfg).Route()
controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), protected).Route() controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), protected).Route()
controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), protected, s.cfg).Route() controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), protected, s.cfg).Route()
controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), protected).Route() controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), protected).Route()
controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), protected, s.cfg).Route() controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), protected, s.cfg).Route()
controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), protected, s.cfg).Route() controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), protected, s.cfg).Route()
controller.NewOLTController(s.ucManager.NewOLTUsecase(), protected).Route() controller.NewOLTController(s.ucManager.NewOLTUsecase(), protected).Route()
} }
} }
func (s *Server) Run() { func (s *Server) Run() {
s.setupController() s.setupController()
if err := s.engine.Run(s.host); err != nil { if err := s.engine.Run(s.host); err != nil {
panic(err) panic(err)
} }
} }

View File

@ -15,40 +15,44 @@ type InfraManager interface {
} }
type infraManager struct { type infraManager struct {
db *gorm.DB db *gorm.DB
cfg *config.Config cfg *config.Config
} }
func (im *infraManager) openConn() error { func (im *infraManager) openConn() error {
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s TimeZone=Asia/Shanghai", dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s TimeZone=Asia/Shanghai",
im.cfg.DBHost, im.cfg.DBUser, im.cfg.DBPass, im.cfg.DBName, im.cfg.DBPort) im.cfg.DBHost, im.cfg.DBUser, im.cfg.DBPass, im.cfg.DBName, im.cfg.DBPort)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return err
}
err = im.autoMigrate(db) // Configure GORM to disable foreign key constraint during migration
if err != nil { db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
return fmt.Errorf("failed to migrate database schema: %w", err) DisableForeignKeyConstraintWhenMigrating: true,
} })
if err != nil {
im.db = db return err
return nil }
err = im.autoMigrate(db)
if err != nil {
return fmt.Errorf("failed to migrate database schema: %w", err)
}
im.db = db
return nil
} }
func (im *infraManager) autoMigrate(db *gorm.DB) error { func (im *infraManager) autoMigrate(db *gorm.DB) error {
return db.AutoMigrate( return db.AutoMigrate(
&entity.Role{}, &entity.Role{},
&entity.User{}, &entity.User{},
&entity.Device{}, &entity.Device{},
&entity.Backbone{}, &entity.Tower{},
&entity.Fishbone{}, &entity.OLT{},
&entity.Tower{}, &entity.DevicePort{},
&entity.DevicePort{}, &entity.Backbone{},
&entity.CountAssets{}, &entity.Fishbone{},
&entity.ActivityLog{}, &entity.CountAssets{},
&entity.OLT{}, &entity.ActivityLog{},
) )
} }
func NewInfraManager(cfg *config.Config) (InfraManager, error) { func NewInfraManager(cfg *config.Config) (InfraManager, error) {

View File

@ -1,66 +1,67 @@
package middleware package middleware
import ( import (
"net/http" "net/http"
"strings" "strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
) )
func CORSMiddleware() gin.HandlerFunc { func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin") origin := c.Request.Header.Get("Origin")
// Define allowed origins (add your React app's URL)
allowedOrigins := []string{
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
"http://103.110.8.103:80",// Add production URL
"http://103.110.8.103",
"http://nam.winteraccess.id", // Define allowed origins (add your React app's URL)
allowedOrigins := []string{
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:5173", // Vite development server
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
"http://103.110.8.103:80", // Add production URL
"http://103.110.8.103",
"http://nam.winteraccess.id",
"https://nam.winteraccess.id",
}
} // Check if origin is in allowed list
isAllowed := false
// Check if origin is in allowed list for _, allowed := range allowedOrigins {
isAllowed := false if origin == allowed {
for _, allowed := range allowedOrigins { isAllowed = true
if origin == allowed { break
isAllowed = true }
break }
}
}
// For development, also allow localhost variations
if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") {
isAllowed = true
}
if origin != "" && isAllowed {
// For allowed origins, set specific origin and enable credentials
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
} else if origin == "" {
// For requests without origin (direct API calls, mobile apps, etc.)
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
// Don't set credentials for wildcard
} else {
// For disallowed origins, still set basic CORS but no credentials
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
}
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE, PATCH")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
// Handle preflight requests
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next() // For development, also allow localhost variations
} if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") {
isAllowed = true
}
if origin != "" && isAllowed {
// For allowed origins, set specific origin and enable credentials
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
} else if origin == "" {
// For requests without origin (direct API calls, mobile apps, etc.)
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
// Don't set credentials for wildcard
} else {
// For disallowed origins, still set basic CORS but no credentials
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
}
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE, PATCH")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
// Handle preflight requests
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
} }

View File

@ -7,47 +7,49 @@ import (
) )
type DeviceDetailsDTO struct { type DeviceDetailsDTO struct {
DeviceCode string `json:"device_code" validate:"required"` DeviceCode string `json:"device_code" validate:"required"`
DeviceType string `json:"device_type" validate:"required,oneof=OTB ODP"` DeviceType string `json:"device_type" validate:"required,oneof=OTB ODP"`
Longitude float64 `json:"longitude" validate:"required"` Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"` Latitude float64 `json:"latitude" validate:"required"`
PortAmount int `json:"port_amount" validate:"required,min=1,max=100"` PortAmount int `json:"port_amount" validate:"required,min=1,max=100"`
Status string `json:"status" validate:"required,oneof=active inactive maintenance"` Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
Region *string `json:"region,omitempty" validate:"omitempty,min=3"` Region *string `json:"region,omitempty" validate:"omitempty,min=3"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"` Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"` City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"` District *string `json:"district,omitempty" validate:"omitempty,min=3"`
TowerID *uuid.UUID `json:"tower_id,omitempty"` TowerID *uuid.UUID `json:"tower_id,omitempty"`
OLTID *uuid.UUID `json:"olt_id,omitempty"` OLTID *uuid.UUID `json:"olt_id,omitempty"`
} }
type UpdateDeviceDetailsDTO struct { type UpdateDeviceDetailsDTO struct {
DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"` DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"`
DeviceType *string `json:"device_type,omitempty" validate:"omitempty,oneof=OTB ODP CLOSURE"` DeviceType *string `json:"device_type,omitempty" validate:"omitempty,oneof=OTB ODP CLOSURE"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty,min=0,max=100"` PortAmount *int `json:"port_amount,omitempty" validate:"omitempty,min=0,max=100"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"` Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"`
Region *string `json:"region,omitempty" validate:"omitempty,min=3"` Region *string `json:"region,omitempty" validate:"omitempty,min=3"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"` Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"` City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"` District *string `json:"district,omitempty" validate:"omitempty,min=3"`
TowerID *uuid.UUID `json:"tower_id,omitempty"` TowerID *uuid.UUID `json:"tower_id,omitempty"`
OLTID *uuid.UUID `json:"olt_id,omitempty"` OLTID *uuid.UUID `json:"olt_id,omitempty"`
} }
type AssignMultipleCustomersDTO struct { type AssignMultipleCustomersDTO struct {
CustomerName string `json:"customer_name" binding:"required"` CustomerName string `json:"customer_name" binding:"required"`
PortNumber *int `json:"port_number,omitempty"` PortNumber *int `json:"port_number,omitempty"`
IsOccupied *bool `json:"is_occupied,omitempty"`
Status *entity.PortStatus `json:"status,omitempty"` // Add status field Status *entity.PortStatus `json:"status,omitempty"` // Add status field
Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field
} }
type UpdateCustomerByPortDTO struct { type UpdateCustomerByPortDTO struct {
PortNumber int `json:"port_number,omitempty"` PortNumber int `json:"port_number,omitempty"`
NewCustomerName *string `json:"new_customer_name,omitempty"` NewCustomerName *string `json:"new_customer_name,omitempty"`
Status *entity.PortStatus `json:"status,omitempty"` // Add status field IsOccupied *bool `json:"is_occupied,omitempty"`
Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field Status *entity.PortStatus `json:"status,omitempty"` // Add status field
Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field
} }
type BulkUpdateCustomersByPortDTO struct { type BulkUpdateCustomersByPortDTO struct {
@ -60,4 +62,4 @@ type RemoveCustomerByPortDTO struct {
type DeleteImageDTO struct { type DeleteImageDTO struct {
Filename string `json:"filename" validate:"required"` Filename string `json:"filename" validate:"required"`
} }

View File

@ -7,34 +7,34 @@ import (
) )
type DevicePort struct { type DevicePort struct {
DeviceID uuid.UUID `json:"device_id"` DeviceID uuid.UUID `json:"device_id"`
PortNumber int `json:"port_number"` PortNumber int `json:"port_number"`
} }
type UpdateDevicePort struct { type UpdateDevicePort struct {
DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"` DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"`
PortNumber *int `json:"port_number,omitempty" validate:"omitempty,min=1"` PortNumber *int `json:"port_number,omitempty" validate:"omitempty,min=1"`
} }
type AssignCustomerToPortDTO struct { type AssignCustomerToPortDTO struct {
CustomerName string `json:"customer_name" binding:"required"` CustomerName string `json:"customer_name" binding:"required"`
PortNumber *int `json:"port_number,omitempty"` // Optional, will auto-assign if not provided PortNumber *int `json:"port_number,omitempty"` // Optional, will auto-assign if not provided
Status *entity.PortStatus `json:"status,omitempty"` // Add status field Status *entity.PortStatus `json:"status,omitempty"` // Add status field
Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field
} }
type UpdatePortUsageDTO struct { type UpdatePortUsageDTO struct {
PortUsed int `json:"port_used" binding:"required,min=0"` PortUsed int `json:"port_used" binding:"required,min=0"`
} }
type PortAssignmentDTO struct { type PortAssignmentDTO struct {
PortNumber int `json:"port_number" binding:"required,min=1"` PortNumber int `json:"port_number" binding:"required,min=1"`
CustomerName *string `json:"customer_name,omitempty"` CustomerName *string `json:"customer_name,omitempty"`
Status *entity.PortStatus `json:"status,omitempty" validate:"omitempty,portstatus"` IsOccupied *bool `json:"is_occupied,omitempty"`
Bandwidth *string `json:"bandwidth,omitempty" validate:"omitempty,min=1"` Status *entity.PortStatus `json:"status,omitempty" validate:"omitempty,portstatus"`
Bandwidth *string `json:"bandwidth,omitempty" validate:"omitempty,min=1"`
} }
type UpdatePortAssignmentsDTO struct { type UpdatePortAssignmentsDTO struct {
PortAssignments []PortAssignmentDTO `json:"port_assignments" binding:"required"` PortAssignments []PortAssignmentDTO `json:"port_assignments" binding:"required"`
} }

View File

@ -13,287 +13,306 @@ import (
type PortStatus string type PortStatus string
const ( const (
PortStatusOn PortStatus = "on" PortStatusOn PortStatus = "on"
PortStatusOff PortStatus = "off" PortStatusOff PortStatus = "off"
PortStatusDyingGasp PortStatus = "dyingGasp" PortStatusDyingGasp PortStatus = "dyingGasp"
PortStatusLOS PortStatus = "los" PortStatusLOS PortStatus = "los"
) )
type StringSlice []string type StringSlice []string
func (s StringSlice) Value() (driver.Value, error) { func (s StringSlice) Value() (driver.Value, error) {
if len(s) == 0 { if len(s) == 0 {
return "[]", nil return "[]", nil
} }
return json.Marshal(s) return json.Marshal(s)
} }
func (s *StringSlice) Scan(value interface{}) error { func (s *StringSlice) Scan(value interface{}) error {
if value == nil { if value == nil {
*s = StringSlice{} *s = StringSlice{}
return nil return nil
} }
var bytes []byte var bytes []byte
switch v := value.(type) { switch v := value.(type) {
case []byte: // []byte and []uint8 are the same type in Go case []byte: // []byte and []uint8 are the same type in Go
bytes = v bytes = v
case string: case string:
bytes = []byte(v) bytes = []byte(v)
default: default:
return fmt.Errorf("cannot scan %T into StringSlice", value) return fmt.Errorf("cannot scan %T into StringSlice", value)
} }
// Handle empty string case // Handle empty string case
if len(bytes) == 0 { if len(bytes) == 0 {
*s = StringSlice{} *s = StringSlice{}
return nil return nil
} }
return json.Unmarshal(bytes, s) return json.Unmarshal(bytes, s)
} }
type PortAssignment struct { type PortAssignment struct {
PortNumber int `json:"port_number"` PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"` // Nullable for empty ports CustomerName *string `json:"customer_name"` // Nullable for empty ports
Status PortStatus `json:"status"` // Add status field IsOccupied bool `json:"is_occupied"` // Explicitly store occupation status
Bandwidth *string `json:"bandwidth"` // Add bandwidth field (nullable) Status PortStatus `json:"status"` // Add status field
Bandwidth *string `json:"bandwidth"` // Add bandwidth field (nullable)
} }
type PortAssignmentResponse struct { type PortAssignmentResponse struct {
PortNumber int `json:"port_number"` PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"` CustomerName *string `json:"customer_name"`
IsOccupied bool `json:"is_occupied"` IsOccupied bool `json:"is_occupied"`
Status PortStatus `json:"status"` // Add status field Status PortStatus `json:"status"` // Add status field
Bandwidth *string `json:"bandwidth"` // Add bandwidth field Bandwidth *string `json:"bandwidth"` // Add bandwidth field
} }
type PortAssignments []PortAssignment type PortAssignments []PortAssignment
func (p PortAssignments) Value() (driver.Value, error) { func (p PortAssignments) Value() (driver.Value, error) {
if len(p) == 0 { if len(p) == 0 {
return "[]", nil return "[]", nil
} }
return json.Marshal(p) return json.Marshal(p)
} }
func (p *PortAssignments) Scan(value interface{}) error { func (p *PortAssignments) Scan(value interface{}) error {
if value == nil { if value == nil {
*p = PortAssignments{} *p = PortAssignments{}
return nil return nil
} }
var bytes []byte var bytes []byte
switch v := value.(type) { switch v := value.(type) {
case []byte: case []byte:
bytes = v bytes = v
case string: case string:
bytes = []byte(v) bytes = []byte(v)
default: default:
return fmt.Errorf("cannot scan %T into PortAssignments", value) return fmt.Errorf("cannot scan %T into PortAssignments", value)
} }
if len(bytes) == 0 { if len(bytes) == 0 {
*p = PortAssignments{} *p = PortAssignments{}
return nil return nil
} }
return json.Unmarshal(bytes, p) return json.Unmarshal(bytes, p)
} }
type DevicePort struct { type DevicePort struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"` DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"`
PortUsed int `json:"port_used"` PortUsed int `json:"port_used"`
PortAvailable int `json:"port_available"` PortAvailable int `json:"port_available"`
CustomerCount int `json:"customer_count"` CustomerCount int `json:"customer_count"`
CustomerNames StringSlice `json:"customer_names" gorm:"type:jsonb"` CustomerNames StringSlice `json:"customer_names" gorm:"type:jsonb"`
PortAssignments PortAssignments `json:"port_assignments" gorm:"type:jsonb"` PortAssignments PortAssignments `json:"port_assignments" gorm:"type: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"`
// Relationships // Relationships
Device Device `json:"device" gorm:"foreignKey:DeviceID"` Device Device `json:"device" gorm:"foreignKey:DeviceID"`
} }
// Helper method to get customer names with port numbers // Helper method to get customer names with port numbers
func (dp *DevicePort) GetCustomerNamesWithPorts() []string { func (dp *DevicePort) GetCustomerNamesWithPorts() []string {
if dp.Device.DeviceType == "OTB" { if dp.Device.DeviceType == "OTB" {
return []string{} // OTB devices do not have port assignments return []string{} // OTB devices do not have port assignments
} }
var result []string var result []string
for _, assignment := range dp.PortAssignments { for _, assignment := range dp.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" { if assignment.CustomerName != nil && *assignment.CustomerName != "" {
result = append(result, fmt.Sprintf("%d: %s", assignment.PortNumber, *assignment.CustomerName)) result = append(result, fmt.Sprintf("%d: %s", assignment.PortNumber, *assignment.CustomerName))
} }
} }
// If no port assignments, fall back to customer names // If no port assignments, fall back to customer names
if len(result) == 0 { if len(result) == 0 {
for i, name := range dp.CustomerNames { for i, name := range dp.CustomerNames {
if name != "" { if name != "" {
result = append(result, fmt.Sprintf("%d: %s", i+1, name)) result = append(result, fmt.Sprintf("%d: %s", i+1, name))
} }
} }
} }
return result return result
} }
// Helper method to get only customer names // Helper method to get only customer names
func (dp *DevicePort) GetCustomerNames() []string { func (dp *DevicePort) GetCustomerNames() []string {
if dp.Device.DeviceType == "OTB" { if dp.Device.DeviceType == "OTB" {
return []string{} // OTB devices do not have port assignments return []string{} // OTB devices do not have port assignments
} }
var result []string var result []string
for _, assignment := range dp.PortAssignments { for _, assignment := range dp.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" { if assignment.CustomerName != nil && *assignment.CustomerName != "" {
result = append(result, *assignment.CustomerName) result = append(result, *assignment.CustomerName)
} }
} }
// If no port assignments, fall back to customer names // If no port assignments, fall back to customer names
if len(result) == 0 { if len(result) == 0 {
for _, name := range dp.CustomerNames { for _, name := range dp.CustomerNames {
if name != "" { if name != "" {
result = append(result, name) result = append(result, name)
} }
} }
} }
return result return result
} }
func (dp *DevicePort) GetPortAssignmentsWithDetails() []PortAssignmentResponse { func (dp *DevicePort) GetPortAssignmentsWithDetails() []PortAssignmentResponse {
// If port_used is 0, return empty array // If port_used is 0, return empty array
if dp.Device.DeviceType == "OTB" { if dp.Device.DeviceType == "OTB" {
return []PortAssignmentResponse{} return []PortAssignmentResponse{}
} }
if dp.PortUsed == 0 { if dp.PortUsed == 0 {
return []PortAssignmentResponse{} 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
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++
}
}
}
// 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
} else if occupiedPorts[i] {
// Port is occupied but no assignment data - use defaults
status = PortStatusOn
}
result = append(result, PortAssignmentResponse{
PortNumber: i,
CustomerName: customerName,
IsOccupied: occupiedPorts[i],
Status: status,
Bandwidth: bandwidth,
})
}
return result 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 { func (dp *DevicePort) GetCustomerNamesOnly() []string {
if dp.Device.DeviceType == "OTB" { if dp.Device.DeviceType == "OTB" {
return []string{} // OTB devices do not have port assignments return []string{} // OTB devices do not have port assignments
} }
var result []string var result []string
// If we have PortAssignments data, use it // If we have PortAssignments data, use it
if len(dp.PortAssignments) > 0 { if len(dp.PortAssignments) > 0 {
for _, assignment := range dp.PortAssignments { for _, assignment := range dp.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" { if assignment.CustomerName != nil && *assignment.CustomerName != "" {
result = append(result, *assignment.CustomerName) result = append(result, *assignment.CustomerName)
} }
} }
} else { } else {
// Fallback: use CustomerNames directly // Fallback: use CustomerNames directly
for _, name := range dp.CustomerNames { for _, name := range dp.CustomerNames {
if name != "" { if name != "" {
result = append(result, name) result = append(result, name)
} }
} }
} }
return result return result
} }
func (DevicePort) TableName() string { func (DevicePort) TableName() string {
return "device_ports" return "device_ports"
} }

File diff suppressed because it is too large Load Diff