diff --git a/delivery/server.go b/delivery/server.go index 8ce1aaa..03ce8dd 100644 --- a/delivery/server.go +++ b/delivery/server.go @@ -13,9 +13,9 @@ import ( type Server struct { ucManager manager.UsecaseManager - engine *gin.Engine - host string - cfg *config.Config + engine *gin.Engine + host string + cfg *config.Config } func NewServer() *Server { @@ -41,66 +41,62 @@ func NewServer() *Server { return &Server{ ucManager: ucManager, - engine: engine, - host: host, - cfg: cfg, + engine: engine, + host: host, + cfg: cfg, } } 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") - { - publicRegistration.POST("/register", controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).RegisterUser) - } + 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") + { + 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 protected := rg.Use(middleware.ConditionalAuthMiddleware(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), s.cfg)).(*gin.RouterGroup) { - protectedRegistration := protected.Group("/user-registration") - { - 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("/reject/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).RejectUser) - } + protectedRegistration := protected.Group("/user-registration") + { + 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("/reject/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).RejectUser) + } controller.NewUserRoleManagementController(s.ucManager.NewUserUsecase(), protected).Route() controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), protected).Route() - controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), protected).Route() - controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), protected, s.cfg).Route() - controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), protected, s.cfg).Route() - controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), protected, s.cfg).Route() - controller.NewTowerController(s.ucManager.NewTowerUsecase(), protected, s.cfg).Route() - controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), protected).Route() - controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), protected, s.cfg).Route() - controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), protected).Route() - controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), protected, s.cfg).Route() - controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), protected, s.cfg).Route() + controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), protected).Route() + controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), protected, s.cfg).Route() + controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), protected, s.cfg).Route() + controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), protected, s.cfg).Route() + controller.NewTowerController(s.ucManager.NewTowerUsecase(), protected, s.cfg).Route() + controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), protected).Route() + controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), protected, s.cfg).Route() + controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), protected).Route() + controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), protected, s.cfg).Route() + controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), protected, s.cfg).Route() controller.NewOLTController(s.ucManager.NewOLTUsecase(), protected).Route() - - } + + } } - - func (s *Server) Run() { s.setupController() if err := s.engine.Run(s.host); err != nil { panic(err) } -} \ No newline at end of file +} diff --git a/manager/infra_manager.go b/manager/infra_manager.go index 835c67f..3aadcd8 100644 --- a/manager/infra_manager.go +++ b/manager/infra_manager.go @@ -15,40 +15,44 @@ type InfraManager interface { } type infraManager struct { - db *gorm.DB + db *gorm.DB cfg *config.Config } func (im *infraManager) openConn() error { - 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) - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - return err - } + 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) - err = im.autoMigrate(db) - if err != nil { - return fmt.Errorf("failed to migrate database schema: %w", err) - } - - im.db = db - return nil + // Configure GORM to disable foreign key constraint during migration + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + }) + if err != nil { + return err + } + + 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 { - return db.AutoMigrate( + return db.AutoMigrate( &entity.Role{}, - &entity.User{}, - &entity.Device{}, - &entity.Backbone{}, - &entity.Fishbone{}, - &entity.Tower{}, - &entity.DevicePort{}, - &entity.CountAssets{}, - &entity.ActivityLog{}, - &entity.OLT{}, - ) + &entity.User{}, + &entity.Device{}, + &entity.Tower{}, + &entity.OLT{}, + &entity.DevicePort{}, + &entity.Backbone{}, + &entity.Fishbone{}, + &entity.CountAssets{}, + &entity.ActivityLog{}, + ) } func NewInfraManager(cfg *config.Config) (InfraManager, error) { diff --git a/middleware/cors_middleware.go b/middleware/cors_middleware.go index fe387bc..11a1aaa 100644 --- a/middleware/cors_middleware.go +++ b/middleware/cors_middleware.go @@ -1,66 +1,67 @@ package middleware import ( - "net/http" - "strings" - "github.com/gin-gonic/gin" + "net/http" + "strings" + + "github.com/gin-gonic/gin" ) func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - 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", + return func(c *gin.Context) { + origin := c.Request.Header.Get("Origin") - "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 - for _, allowed := range allowedOrigins { - if origin == allowed { - 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 - } + // Check if origin is in allowed list + isAllowed := false + for _, allowed := range allowedOrigins { + if origin == allowed { + isAllowed = true + break + } + } - 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() + } } diff --git a/model/dto/req/deviceDetails.go b/model/dto/req/deviceDetails.go index a941e3a..637e4d7 100644 --- a/model/dto/req/deviceDetails.go +++ b/model/dto/req/deviceDetails.go @@ -7,47 +7,49 @@ import ( ) type DeviceDetailsDTO struct { - DeviceCode string `json:"device_code" validate:"required"` - DeviceType string `json:"device_type" validate:"required,oneof=OTB ODP"` - Longitude float64 `json:"longitude" validate:"required"` - Latitude float64 `json:"latitude" validate:"required"` - PortAmount int `json:"port_amount" validate:"required,min=1,max=100"` - Status string `json:"status" validate:"required,oneof=active inactive maintenance"` - Region *string `json:"region,omitempty" validate:"omitempty,min=3"` - Province *string `json:"province,omitempty" validate:"omitempty,min=3"` - City *string `json:"city,omitempty" validate:"omitempty,min=3"` - District *string `json:"district,omitempty" validate:"omitempty,min=3"` + DeviceCode string `json:"device_code" validate:"required"` + DeviceType string `json:"device_type" validate:"required,oneof=OTB ODP"` + Longitude float64 `json:"longitude" validate:"required"` + Latitude float64 `json:"latitude" validate:"required"` + PortAmount int `json:"port_amount" validate:"required,min=1,max=100"` + Status string `json:"status" validate:"required,oneof=active inactive maintenance"` + Region *string `json:"region,omitempty" validate:"omitempty,min=3"` + Province *string `json:"province,omitempty" validate:"omitempty,min=3"` + City *string `json:"city,omitempty" validate:"omitempty,min=3"` + District *string `json:"district,omitempty" validate:"omitempty,min=3"` TowerID *uuid.UUID `json:"tower_id,omitempty"` - OLTID *uuid.UUID `json:"olt_id,omitempty"` + OLTID *uuid.UUID `json:"olt_id,omitempty"` } type UpdateDeviceDetailsDTO struct { - DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"` - DeviceType *string `json:"device_type,omitempty" validate:"omitempty,oneof=OTB ODP CLOSURE"` - Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` - Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` - PortAmount *int `json:"port_amount,omitempty" validate:"omitempty,min=0,max=100"` - Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"` - Region *string `json:"region,omitempty" validate:"omitempty,min=3"` - Province *string `json:"province,omitempty" validate:"omitempty,min=3"` - City *string `json:"city,omitempty" validate:"omitempty,min=3"` - District *string `json:"district,omitempty" validate:"omitempty,min=3"` - TowerID *uuid.UUID `json:"tower_id,omitempty"` - OLTID *uuid.UUID `json:"olt_id,omitempty"` + DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"` + DeviceType *string `json:"device_type,omitempty" validate:"omitempty,oneof=OTB ODP CLOSURE"` + Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` + Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` + PortAmount *int `json:"port_amount,omitempty" validate:"omitempty,min=0,max=100"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"` + Region *string `json:"region,omitempty" validate:"omitempty,min=3"` + Province *string `json:"province,omitempty" validate:"omitempty,min=3"` + City *string `json:"city,omitempty" validate:"omitempty,min=3"` + District *string `json:"district,omitempty" validate:"omitempty,min=3"` + TowerID *uuid.UUID `json:"tower_id,omitempty"` + OLTID *uuid.UUID `json:"olt_id,omitempty"` } type AssignMultipleCustomersDTO struct { CustomerName string `json:"customer_name" binding:"required"` PortNumber *int `json:"port_number,omitempty"` + IsOccupied *bool `json:"is_occupied,omitempty"` Status *entity.PortStatus `json:"status,omitempty"` // Add status field Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field } type UpdateCustomerByPortDTO struct { - PortNumber int `json:"port_number,omitempty"` - NewCustomerName *string `json:"new_customer_name,omitempty"` - Status *entity.PortStatus `json:"status,omitempty"` // Add status field - Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field + PortNumber int `json:"port_number,omitempty"` + NewCustomerName *string `json:"new_customer_name,omitempty"` + IsOccupied *bool `json:"is_occupied,omitempty"` + Status *entity.PortStatus `json:"status,omitempty"` // Add status field + Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field } type BulkUpdateCustomersByPortDTO struct { @@ -60,4 +62,4 @@ type RemoveCustomerByPortDTO struct { type DeleteImageDTO struct { Filename string `json:"filename" validate:"required"` -} \ No newline at end of file +} diff --git a/model/dto/req/devicePort_dto.go b/model/dto/req/devicePort_dto.go index edf0ada..5976d80 100644 --- a/model/dto/req/devicePort_dto.go +++ b/model/dto/req/devicePort_dto.go @@ -7,34 +7,34 @@ import ( ) type DevicePort struct { - DeviceID uuid.UUID `json:"device_id"` - PortNumber int `json:"port_number"` + DeviceID uuid.UUID `json:"device_id"` + PortNumber int `json:"port_number"` } type UpdateDevicePort struct { - DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"` - PortNumber *int `json:"port_number,omitempty" validate:"omitempty,min=1"` + 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 - Status *entity.PortStatus `json:"status,omitempty"` // Add status field - Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field + CustomerName string `json:"customer_name" binding:"required"` + PortNumber *int `json:"port_number,omitempty"` // Optional, will auto-assign if not provided + Status *entity.PortStatus `json:"status,omitempty"` // Add status field + Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field } type UpdatePortUsageDTO struct { - PortUsed int `json:"port_used" binding:"required,min=0"` + 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"` - Status *entity.PortStatus `json:"status,omitempty" validate:"omitempty,portstatus"` - Bandwidth *string `json:"bandwidth,omitempty" validate:"omitempty,min=1"` + PortNumber int `json:"port_number" binding:"required,min=1"` + CustomerName *string `json:"customer_name,omitempty"` + IsOccupied *bool `json:"is_occupied,omitempty"` + Status *entity.PortStatus `json:"status,omitempty" validate:"omitempty,portstatus"` + Bandwidth *string `json:"bandwidth,omitempty" validate:"omitempty,min=1"` } type UpdatePortAssignmentsDTO struct { - PortAssignments []PortAssignmentDTO `json:"port_assignments" binding:"required"` -} \ No newline at end of file + PortAssignments []PortAssignmentDTO `json:"port_assignments" binding:"required"` +} diff --git a/model/entity/device_port.go b/model/entity/device_port.go index 4aed908..34af463 100644 --- a/model/entity/device_port.go +++ b/model/entity/device_port.go @@ -13,287 +13,306 @@ import ( type PortStatus string const ( - PortStatusOn PortStatus = "on" - PortStatusOff PortStatus = "off" - PortStatusDyingGasp PortStatus = "dyingGasp" - PortStatusLOS PortStatus = "los" + 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) + 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) + 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 - Status PortStatus `json:"status"` // Add status field - Bandwidth *string `json:"bandwidth"` // Add bandwidth field (nullable) + 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 + 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) + 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) + 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"` + 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 + 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 + 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 port_used is 0, return empty array - if dp.Device.DeviceType == "OTB" { - return []PortAssignmentResponse{} - } + 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 - 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, - }) - } + if dp.PortUsed == 0 { + return []PortAssignmentResponse{} + } - 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 { - 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 + 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" -} \ No newline at end of file + return "device_ports" +} diff --git a/repository/device_details.go b/repository/device_details.go index b0c4f6c..7fb94cf 100644 --- a/repository/device_details.go +++ b/repository/device_details.go @@ -11,758 +11,805 @@ import ( ) type DeviceDetailsRepo interface { - Create(device entity.Device) error - GetAll() ([]entity.DeviceDetails, error) - GetByID(id uuid.UUID) (entity.DeviceDetails, error) - Update(id uuid.UUID, updates map[string]interface{}) error - Delete(id uuid.UUID) error - - // Port management - UpdateDevicePortUsage(deviceID uuid.UUID) error - // ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error - - // Connection management - GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) - GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) - GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) - - // Validation helpers - GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err 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 - MigrateCustomerNamesToPortAssignments(devicePort *entity.DevicePort, devicePortAmount int) error - AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error - 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) - ValidateTowerExists(towerID uuid.UUID) (bool, error) - SyncTowerLocationWithDevice(deviceID uuid.UUID) error + Create(device entity.Device) error + GetAll() ([]entity.DeviceDetails, error) + GetByID(id uuid.UUID) (entity.DeviceDetails, error) + Update(id uuid.UUID, updates map[string]interface{}) error + Delete(id uuid.UUID) error - + // Port management + UpdateDevicePortUsage(deviceID uuid.UUID) error + // ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error + + // Connection management + GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) + GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) + GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) + + // Validation helpers + GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err 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 + MigrateCustomerNamesToPortAssignments(devicePort *entity.DevicePort, devicePortAmount int) error + AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error + 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) + ValidateTowerExists(towerID uuid.UUID) (bool, error) + SyncTowerLocationWithDevice(deviceID uuid.UUID) error } type deviceDetailsRepo struct { - db *gorm.DB + db *gorm.DB } func NewDeviceDetailsRepo(db *gorm.DB) DeviceDetailsRepo { - return &deviceDetailsRepo{ - db: db, - } + return &deviceDetailsRepo{ + db: db, + } } func (r *deviceDetailsRepo) ValidateTowerExists(towerID uuid.UUID) (bool, error) { - var count int64 - err := r.db.Model(&entity.Tower{}).Where("id = ?", towerID).Count(&count).Error - return count > 0, err + var count int64 + err := r.db.Model(&entity.Tower{}).Where("id = ?", towerID).Count(&count).Error + return count > 0, err } func (r *deviceDetailsRepo) SyncTowerLocationWithDevice(deviceID uuid.UUID) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Get device details - var device entity.Device - if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil { - return fmt.Errorf("device not found: %w", err) - } + return r.db.Transaction(func(tx *gorm.DB) error { + // Get device details + var device entity.Device + if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil { + return fmt.Errorf("device not found: %w", err) + } - // Update tower location if device has a tower assigned - if device.TowerID != nil { - if err := tx.Model(&entity.Tower{}). - Where("id = ?", *device.TowerID). - Updates(map[string]interface{}{ - "longitude": device.Longitude, - "latitude": device.Latitude, - "updated_at": time.Now(), - }).Error; err != nil { - return fmt.Errorf("failed to sync tower location: %w", err) - } - } + // Update tower location if device has a tower assigned + if device.TowerID != nil { + if err := tx.Model(&entity.Tower{}). + Where("id = ?", *device.TowerID). + Updates(map[string]interface{}{ + "longitude": device.Longitude, + "latitude": device.Latitude, + "updated_at": time.Now(), + }).Error; err != nil { + return fmt.Errorf("failed to sync tower location: %w", err) + } + } - // Also update any towers that reference this device via dev_id - if err := tx.Model(&entity.Tower{}). - Where("dev_id = ?", deviceID). - Updates(map[string]interface{}{ - "longitude": device.Longitude, - "latitude": device.Latitude, - "updated_at": time.Now(), - }).Error; err != nil { - return fmt.Errorf("failed to sync related towers location: %w", err) - } + // Also update any towers that reference this device via dev_id + if err := tx.Model(&entity.Tower{}). + Where("dev_id = ?", deviceID). + Updates(map[string]interface{}{ + "longitude": device.Longitude, + "latitude": device.Latitude, + "updated_at": time.Now(), + }).Error; err != nil { + return fmt.Errorf("failed to sync related towers location: %w", err) + } - return nil - }) + return nil + }) } 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 + 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 - var device entity.Device - if err := tx.Set("gorm:query_option", "FOR UPDATE"). - Where("id = ?", deviceID).First(&device).Error; err != nil { - return err - } + 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 customer assignments - if device.DeviceType != "ODP" { - return fmt.Errorf("customer assignments can only be updated for ODP devices") - } + // Only ODP devices can have customer assignments + if device.DeviceType != "ODP" { + return fmt.Errorf("customer 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) - } + 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) + } - // 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, - Status: entity.PortStatusOff, - Bandwidth: nil, - } - } - } + // 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, + IsOccupied: false, + Status: entity.PortStatusOff, + Bandwidth: nil, + } + } + } - // Validate port number - if update.PortNumber > device.PortAmount { - return fmt.Errorf("port number %d exceeds device capacity (%d)", update.PortNumber, device.PortAmount) - } + // Validate port number + if update.PortNumber > device.PortAmount { + return fmt.Errorf("port number %d exceeds device capacity (%d)", update.PortNumber, device.PortAmount) + } - portIndex := update.PortNumber - 1 + portIndex := update.PortNumber - 1 - // If assigning a new customer name - if update.NewCustomerName != nil && *update.NewCustomerName != "" { - // Check if customer name already exists on another port - for i, assignment := range devicePort.PortAssignments { - if i != portIndex && assignment.CustomerName != nil && *assignment.CustomerName == *update.NewCustomerName { - return fmt.Errorf("customer %s is already assigned to port %d", *update.NewCustomerName, i+1) - } - } - } + // Determine if port should be occupied + isOccupied := false + if update.IsOccupied != nil { + isOccupied = *update.IsOccupied + } else if update.NewCustomerName != nil { + // If not specified, determine based on customer name + isOccupied = *update.NewCustomerName != "" + } - if update.NewCustomerName == nil { - // Remove customer from port - devicePort.PortAssignments[portIndex].CustomerName = nil - devicePort.PortAssignments[portIndex].Status = entity.PortStatusOff - devicePort.PortAssignments[portIndex].Bandwidth = nil - } else { - // Assign/update customer on port - devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName - - // Update status if provided, otherwise default to "on" - if update.Status != nil { - devicePort.PortAssignments[portIndex].Status = *update.Status - } else { - devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn - } - - // Update bandwidth if provided - if update.Bandwidth != nil { - devicePort.PortAssignments[portIndex].Bandwidth = update.Bandwidth - } - } + // If assigning a new customer name and the customer name is provided + if update.NewCustomerName != nil && *update.NewCustomerName != "" { + // Check if customer name already exists on another port + for i, assignment := range devicePort.PortAssignments { + if i != portIndex && assignment.CustomerName != nil && *assignment.CustomerName == *update.NewCustomerName { + return fmt.Errorf("customer %s is already assigned to port %d", *update.NewCustomerName, i+1) + } + } + } - // Update counters and backward compatibility fields - r.updateDevicePortCounters(&devicePort) - devicePort.UpdatedAt = time.Now() + // Update customer name + if update.NewCustomerName != nil { + devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName + } - return tx.Save(&devicePort).Error - }) + // Explicitly set the IsOccupied field + devicePort.PortAssignments[portIndex].IsOccupied = isOccupied + + // Update status (independent of occupation) + if update.Status != nil { + devicePort.PortAssignments[portIndex].Status = *update.Status + } else if isOccupied { + // Only set default status if not provided and port is occupied + devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn + } else { + devicePort.PortAssignments[portIndex].Status = entity.PortStatusOff + } + + // Update bandwidth + if update.Bandwidth != nil { + devicePort.PortAssignments[portIndex].Bandwidth = update.Bandwidth + } else if !isOccupied { + // Clear bandwidth when port is not occupied + devicePort.PortAssignments[portIndex].Bandwidth = nil + } + + // If explicitly marked as not occupied, clear customer name + if update.IsOccupied != nil && !*update.IsOccupied { + devicePort.PortAssignments[portIndex].CustomerName = nil + } + + // Update counters and backward compatibility fields + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) } func (r *deviceDetailsRepo) BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) 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 - } + 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 customer assignments - if device.DeviceType != "ODP" { - return fmt.Errorf("customer assignments can only be updated for ODP devices") - } + // Only ODP devices can have customer assignments + if device.DeviceType != "ODP" { + return fmt.Errorf("customer 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) - } + 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) + } - // 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, - Status: entity.PortStatusOff, - Bandwidth: nil, - } - } - } + // 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, + Status: entity.PortStatusOff, + Bandwidth: nil, + } + } + } - // Validate all port numbers first - for _, update := range updates { - if update.PortNumber > device.PortAmount { - return fmt.Errorf("port number %d exceeds device capacity (%d)", update.PortNumber, device.PortAmount) - } - } + // Validate all port numbers first + for _, update := range updates { + if update.PortNumber > device.PortAmount { + return fmt.Errorf("port number %d exceeds device capacity (%d)", update.PortNumber, device.PortAmount) + } + } - // Create a map of final assignments to validate for duplicates - finalAssignments := make(map[int]*string) - - // Start with current assignments - for i, assignment := range devicePort.PortAssignments { - finalAssignments[i+1] = assignment.CustomerName - } + // Create a map of final assignments to validate for duplicates + finalAssignments := make(map[int]*string) - // Apply updates - for _, update := range updates { - finalAssignments[update.PortNumber] = update.NewCustomerName - } + // Start with current assignments + for i, assignment := range devicePort.PortAssignments { + finalAssignments[i+1] = assignment.CustomerName + } - // Check for duplicate customer names - customerNames := make(map[string]int) // customer name -> port number - for portNum, customerName := range finalAssignments { - if customerName != nil && *customerName != "" { - if existingPort, exists := customerNames[*customerName]; exists { - return fmt.Errorf("customer %s would be assigned to both port %d and port %d", *customerName, existingPort, portNum) - } - customerNames[*customerName] = portNum - } - } + // Apply updates + for _, update := range updates { + finalAssignments[update.PortNumber] = update.NewCustomerName + } - // Apply all updates - FIX: Include Status and Bandwidth updates - for _, update := range updates { - portIndex := update.PortNumber - 1 - - // Update customer name - devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName - - // Update status if provided - if update.Status != nil { - devicePort.PortAssignments[portIndex].Status = *update.Status - } else { - // Set default status based on customer assignment - if update.NewCustomerName != nil && *update.NewCustomerName != "" { - devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn - } else { - devicePort.PortAssignments[portIndex].Status = entity.PortStatusOff - } - } - - // Update bandwidth if provided - if update.Bandwidth != nil { - devicePort.PortAssignments[portIndex].Bandwidth = update.Bandwidth - } else if update.NewCustomerName == nil || *update.NewCustomerName == "" { - // Clear bandwidth when removing customer - devicePort.PortAssignments[portIndex].Bandwidth = nil - } - } + // Check for duplicate customer names + customerNames := make(map[string]int) // customer name -> port number + for portNum, customerName := range finalAssignments { + if customerName != nil && *customerName != "" { + if existingPort, exists := customerNames[*customerName]; exists { + return fmt.Errorf("customer %s would be assigned to both port %d and port %d", *customerName, existingPort, portNum) + } + customerNames[*customerName] = portNum + } + } - // Recalculate port_used based on actual assignments - highestUsedPort := 0 - customerCount := 0 - - for _, assignment := range devicePort.PortAssignments { - if assignment.CustomerName != nil && *assignment.CustomerName != "" { - customerCount++ - if assignment.PortNumber > highestUsedPort { - highestUsedPort = assignment.PortNumber - } - } - } - - // Update port_used to reflect actual usage - if customerCount == 0 { - devicePort.PortUsed = 0 - } else { - // Set port_used to the highest port with a customer - // or keep current port_used if it's higher (for reserved ports) - if highestUsedPort > devicePort.PortUsed { - devicePort.PortUsed = highestUsedPort - } - } - - // Recalculate port_available - devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed + // Apply all updates - Include Status, Bandwidth and IsOccupied updates + highestOccupiedPort := 0 + for _, update := range updates { + portIndex := update.PortNumber - 1 - // Update counters and backward compatibility fields - r.updateDevicePortCounters(&devicePort) - devicePort.UpdatedAt = time.Now() + // Update customer name + devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName - return tx.Save(&devicePort).Error - }) + // Determine if port should be occupied + isOccupied := false + if update.IsOccupied != nil { + isOccupied = *update.IsOccupied + } else { + // If not specified, determine based on customer name + isOccupied = update.NewCustomerName != nil && *update.NewCustomerName != "" + } + + // Explicitly set the IsOccupied field + devicePort.PortAssignments[portIndex].IsOccupied = isOccupied + + // Update status (independent of occupation) + if update.Status != nil { + devicePort.PortAssignments[portIndex].Status = *update.Status + } else if isOccupied { + // Only set default status if not provided and port is occupied + devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn + } else { + devicePort.PortAssignments[portIndex].Status = entity.PortStatusOff + } + + // Update bandwidth if provided + if update.Bandwidth != nil { + devicePort.PortAssignments[portIndex].Bandwidth = update.Bandwidth + } else if !isOccupied { + // Clear bandwidth when port is not occupied + devicePort.PortAssignments[portIndex].Bandwidth = nil + } + + // Track highest occupied port + if isOccupied && update.PortNumber > highestOccupiedPort { + highestOccupiedPort = update.PortNumber + } + } + + // Recalculate port_used based on occupied ports (not just customer count) + if highestOccupiedPort == 0 { + // No explicitly occupied ports from updates, check existing assignments + for _, assignment := range devicePort.PortAssignments { + if assignment.IsOccupied { + if assignment.PortNumber > highestOccupiedPort { + highestOccupiedPort = assignment.PortNumber + } + } + } + } + + // Update port_used to reflect highest occupied port + devicePort.PortUsed = highestOccupiedPort + + // Recalculate port_available + devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed + + // Update counters and backward compatibility fields + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) } func (r *deviceDetailsRepo) RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error { - return r.db.Transaction(func(tx *gorm.DB) error { - var device entity.Device - if err := tx.Set("gorm:query_option", "FOR UPDATE"). - Where("id = ?", deviceID).First(&device).Error; err != nil { - return err - } + return r.db.Transaction(func(tx *gorm.DB) error { + var device entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", deviceID).First(&device).Error; err != nil { + return err + } - 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) - } + 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 number - if portNumber > device.PortAmount { - return fmt.Errorf("port number %d exceeds device capacity (%d)", portNumber, device.PortAmount) - } + // Validate port number + if portNumber > device.PortAmount { + return fmt.Errorf("port number %d exceeds device capacity (%d)", portNumber, device.PortAmount) + } - portIndex := portNumber - 1 + portIndex := portNumber - 1 - // Check if port has a customer - if len(devicePort.PortAssignments) <= portIndex || - devicePort.PortAssignments[portIndex].CustomerName == nil || - *devicePort.PortAssignments[portIndex].CustomerName == "" { - return fmt.Errorf("port %d does not have a customer assigned", portNumber) - } + // Check if port has a customer + if len(devicePort.PortAssignments) <= portIndex || + devicePort.PortAssignments[portIndex].CustomerName == nil || + *devicePort.PortAssignments[portIndex].CustomerName == "" { + return fmt.Errorf("port %d does not have a customer assigned", portNumber) + } - // Remove customer from port - devicePort.PortAssignments[portIndex].CustomerName = nil + // Remove customer from port + devicePort.PortAssignments[portIndex].CustomerName = nil - // Update counters and backward compatibility fields - r.updateDevicePortCounters(&devicePort) - devicePort.UpdatedAt = time.Now() + // Update counters and backward compatibility fields + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() - - return tx.Save(&devicePort).Error - }) + return tx.Save(&devicePort).Error + }) } 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 - } + 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") - } + // 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) - } + 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) - } - } + // 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, - Status: entity.PortStatusOff, - Bandwidth: nil, - } - } - } + // 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, + Status: entity.PortStatusOff, + Bandwidth: 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 - } - } + // 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 status if provided, otherwise default based on customer presence - if assignment.Status != nil { - devicePort.PortAssignments[portIndex].Status = *assignment.Status - } else if assignment.CustomerName != nil && *assignment.CustomerName != "" { - devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn - } else { - devicePort.PortAssignments[portIndex].Status = entity.PortStatusOff - } - - // Update bandwidth if provided - if assignment.Bandwidth != nil { - devicePort.PortAssignments[portIndex].Bandwidth = assignment.Bandwidth - } - } - } + // Track highest occupied port for calculating port_used + highestOccupiedPort := 0 - // 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() + // 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 - return tx.Save(&devicePort).Error - }) + // Handle is_occupied field + isOccupied := false + if assignment.IsOccupied != nil { + isOccupied = *assignment.IsOccupied + } else { + // If not specified, determine based on customer name + isOccupied = assignment.CustomerName != nil && *assignment.CustomerName != "" + } + + // Explicitly set the IsOccupied field + devicePort.PortAssignments[portIndex].IsOccupied = isOccupied + + // Update status (independent of occupation) + if assignment.Status != nil { + devicePort.PortAssignments[portIndex].Status = *assignment.Status + } else if isOccupied { + // Only set default status if not provided and port is occupied + devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn + } else { + devicePort.PortAssignments[portIndex].Status = entity.PortStatusOff + } + + // Update bandwidth if provided + if assignment.Bandwidth != nil { + devicePort.PortAssignments[portIndex].Bandwidth = assignment.Bandwidth + } + + // Track highest occupied port + if isOccupied && assignment.PortNumber > highestOccupiedPort { + highestOccupiedPort = assignment.PortNumber + } + } + } + + // Calculate port usage based on highest occupied port (not just customer count) + if highestOccupiedPort == 0 { + // No explicitly occupied ports, calculate from existing assignments + for _, assignment := range devicePort.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + if assignment.PortNumber > highestOccupiedPort { + highestOccupiedPort = assignment.PortNumber + } + } + } + } + + devicePort.PortUsed = highestOccupiedPort + devicePort.PortAvailable = device.PortAmount - highestOccupiedPort + + // Update counters and backward compatibility fields + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) } - func (r *deviceDetailsRepo) Create(device entity.Device) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Validate tower exists if TowerID is provided - if device.TowerID != nil { - var towerExists bool - var err error - if towerExists, err = r.ValidateTowerExists(*device.TowerID); err != nil { - return fmt.Errorf("failed to validate tower: %w", err) - } - if !towerExists { - return fmt.Errorf("tower with ID %s not found", device.TowerID.String()) - } - } + return r.db.Transaction(func(tx *gorm.DB) error { + // Validate tower exists if TowerID is provided + if device.TowerID != nil { + var towerExists bool + var err error + if towerExists, err = r.ValidateTowerExists(*device.TowerID); err != nil { + return fmt.Errorf("failed to validate tower: %w", err) + } + if !towerExists { + return fmt.Errorf("tower with ID %s not found", device.TowerID.String()) + } + } - // Create device - if err := tx.Create(&device).Error; err != nil { - return err - } - - // Create corresponding device port - devicePort := entity.DevicePort{ - ID: uuid.New(), - DeviceID: device.ID, - PortUsed: 0, - PortAvailable: device.PortAmount, - CreatedAt: device.CreatedAt, - UpdatedAt: device.UpdatedAt, - } - - if err := tx.Create(&devicePort).Error; err != nil { - return err - } + // Create device + if err := tx.Create(&device).Error; err != nil { + return err + } - // Sync tower location with device location - if device.TowerID != nil { - if err := tx.Model(&entity.Tower{}). - Where("id = ?", *device.TowerID). - Updates(map[string]interface{}{ - "longitude": device.Longitude, - "latitude": device.Latitude, - "updated_at": time.Now(), - }).Error; err != nil { - return fmt.Errorf("failed to sync tower location: %w", err) - } - } + // Create corresponding device port + devicePort := entity.DevicePort{ + ID: uuid.New(), + DeviceID: device.ID, + PortUsed: 0, + PortAvailable: device.PortAmount, + CreatedAt: device.CreatedAt, + UpdatedAt: device.UpdatedAt, + } - return nil - }) + if err := tx.Create(&devicePort).Error; err != nil { + return err + } + + // Sync tower location with device location + if device.TowerID != nil { + if err := tx.Model(&entity.Tower{}). + Where("id = ?", *device.TowerID). + Updates(map[string]interface{}{ + "longitude": device.Longitude, + "latitude": device.Latitude, + "updated_at": time.Now(), + }).Error; err != nil { + return fmt.Errorf("failed to sync tower location: %w", err) + } + } + + return nil + }) } func (r *deviceDetailsRepo) GetAll() ([]entity.DeviceDetails, error) { - var devices []entity.DeviceDetails - err := r.db. - Preload("DevicePort"). - Preload("BackbonesStart"). - Preload("BackbonesStart.DeviceStart"). - Preload("BackbonesStart.DeviceEnd"). - Preload("BackbonesEnd"). - Preload("BackbonesEnd.DeviceStart"). - Preload("BackbonesEnd.DeviceEnd"). - Preload("FishbonesStart"). - Preload("FishbonesStart.DeviceStart"). - Preload("FishbonesStart.DeviceEnd"). - Preload("FishbonesStart.Backbone"). - Preload("FishbonesEnd"). - Preload("FishbonesEnd.DeviceStart"). - Preload("FishbonesEnd.DeviceEnd"). - Preload("FishbonesEnd.Backbone"). - Preload("OLT"). - Preload("Tower"). // Add Tower preload - Preload("Towers"). - Preload("Towers.Device"). - Find(&devices).Error - return devices, err + var devices []entity.DeviceDetails + err := r.db. + Preload("DevicePort"). + Preload("BackbonesStart"). + Preload("BackbonesStart.DeviceStart"). + Preload("BackbonesStart.DeviceEnd"). + Preload("BackbonesEnd"). + Preload("BackbonesEnd.DeviceStart"). + Preload("BackbonesEnd.DeviceEnd"). + Preload("FishbonesStart"). + Preload("FishbonesStart.DeviceStart"). + Preload("FishbonesStart.DeviceEnd"). + Preload("FishbonesStart.Backbone"). + Preload("FishbonesEnd"). + Preload("FishbonesEnd.DeviceStart"). + Preload("FishbonesEnd.DeviceEnd"). + Preload("FishbonesEnd.Backbone"). + Preload("OLT"). + Preload("Tower"). // Add Tower preload + Preload("Towers"). + Preload("Towers.Device"). + Find(&devices).Error + return devices, err } func (r *deviceDetailsRepo) GetByID(id uuid.UUID) (entity.DeviceDetails, error) { - var device entity.DeviceDetails - err := r.db. - Preload("DevicePort"). - Preload("BackbonesStart"). - Preload("BackbonesStart.DeviceStart"). - Preload("BackbonesStart.DeviceEnd"). - Preload("BackbonesEnd"). - Preload("BackbonesEnd.DeviceStart"). - Preload("BackbonesEnd.DeviceEnd"). - Preload("FishbonesStart"). - Preload("FishbonesStart.DeviceStart"). - Preload("FishbonesStart.DeviceEnd"). - Preload("FishbonesStart.Backbone"). - Preload("FishbonesEnd"). - Preload("FishbonesEnd.DeviceStart"). - Preload("FishbonesEnd.DeviceEnd"). - Preload("FishbonesEnd.Backbone"). - Preload("OLT"). - Preload("Tower"). // Add Tower preload - Preload("Towers"). - Preload("Towers.Device"). - Where("id = ?", id). - First(&device).Error - return device, err + var device entity.DeviceDetails + err := r.db. + Preload("DevicePort"). + Preload("BackbonesStart"). + Preload("BackbonesStart.DeviceStart"). + Preload("BackbonesStart.DeviceEnd"). + Preload("BackbonesEnd"). + Preload("BackbonesEnd.DeviceStart"). + Preload("BackbonesEnd.DeviceEnd"). + Preload("FishbonesStart"). + Preload("FishbonesStart.DeviceStart"). + Preload("FishbonesStart.DeviceEnd"). + Preload("FishbonesStart.Backbone"). + Preload("FishbonesEnd"). + Preload("FishbonesEnd.DeviceStart"). + Preload("FishbonesEnd.DeviceEnd"). + Preload("FishbonesEnd.Backbone"). + Preload("OLT"). + Preload("Tower"). // Add Tower preload + Preload("Towers"). + Preload("Towers.Device"). + Where("id = ?", id). + First(&device).Error + return device, err } func (r *deviceDetailsRepo) Update(id uuid.UUID, updates map[string]interface{}) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Update device - if err := tx.Model(&entity.Device{}).Where("id = ?", id).Updates(updates).Error; err != nil { - return err - } - - // If TowerID is updated, sync locations - if towerID, exists := updates["tower_id"]; exists && towerID != nil { - towerUUID := towerID.(uuid.UUID) - // Validate tower exists - var towerExists bool - var err error - if towerExists, err = r.ValidateTowerExists(towerUUID); err != nil { - return fmt.Errorf("failed to validate tower: %w", err) - } - if !towerExists { - return fmt.Errorf("tower with ID %s not found", towerUUID.String()) - } - } - - // If port_amount is updated, update device_port - if portAmount, exists := updates["port_amount"]; exists { - if err := r.updatePortAmountCascade(tx, id, portAmount.(int)); err != nil { - return err - } - } + return r.db.Transaction(func(tx *gorm.DB) error { + // Update device + if err := tx.Model(&entity.Device{}).Where("id = ?", id).Updates(updates).Error; err != nil { + return err + } - // If location is updated, sync with tower - if _, hasLng := updates["longitude"]; hasLng { - if _, hasLat := updates["latitude"]; hasLat { - if err := r.SyncTowerLocationWithDevice(id); err != nil { - return err - } - } - } - - return nil - }) + // If TowerID is updated, sync locations + if towerID, exists := updates["tower_id"]; exists && towerID != nil { + towerUUID := towerID.(uuid.UUID) + // Validate tower exists + var towerExists bool + var err error + if towerExists, err = r.ValidateTowerExists(towerUUID); err != nil { + return fmt.Errorf("failed to validate tower: %w", err) + } + if !towerExists { + return fmt.Errorf("tower with ID %s not found", towerUUID.String()) + } + } + + // If port_amount is updated, update device_port + if portAmount, exists := updates["port_amount"]; exists { + if err := r.updatePortAmountCascade(tx, id, portAmount.(int)); err != nil { + return err + } + } + + // If location is updated, sync with tower + if _, hasLng := updates["longitude"]; hasLng { + if _, hasLat := updates["latitude"]; hasLat { + if err := r.SyncTowerLocationWithDevice(id); err != nil { + return err + } + } + } + + return nil + }) } func (r *deviceDetailsRepo) GetDevicesWithoutTowers(deviceTypes []string) ([]entity.DeviceDetails, error) { - var devices []entity.DeviceDetails - - query := r.db. - Preload("DevicePort"). - Preload("Tower"). // Add Tower preload - Where("device_type IN ?", deviceTypes). - Where("tower_id IS NULL"). // Check for NULL TowerID instead of dev_id - 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 + var devices []entity.DeviceDetails + + query := r.db. + Preload("DevicePort"). + Preload("Tower"). // Add Tower preload + Where("device_type IN ?", deviceTypes). + Where("tower_id IS NULL"). // Check for NULL TowerID instead of dev_id + 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 - if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { - return err - } - - // Check if new port amount is sufficient for current usage - if newPortAmount < devicePort.PortUsed { - return fmt.Errorf("cannot reduce port amount to %d, currently using %d ports", newPortAmount, devicePort.PortUsed) - } - - // Special case: if port_amount is set to 0, clear all assignments - if newPortAmount == 0 { - devicePort.PortUsed = 0 - devicePort.PortAvailable = 0 - devicePort.CustomerCount = 0 - devicePort.CustomerNames = []string{} - devicePort.PortAssignments = entity.PortAssignments{} - devicePort.UpdatedAt = time.Now() - } else { - // Calculate new port available - newPortAvailable := newPortAmount - devicePort.PortUsed - - // Update port assignments to match new port amount - if len(devicePort.PortAssignments) > 0 { - // Resize port assignments array - newPortAssignments := make(entity.PortAssignments, newPortAmount) - - // Copy existing assignments up to the new port amount - for i := 0; i < newPortAmount; i++ { - if i < len(devicePort.PortAssignments) { - newPortAssignments[i] = devicePort.PortAssignments[i] - } else { - newPortAssignments[i] = entity.PortAssignment{ - PortNumber: i + 1, - CustomerName: nil, - } - } - } - devicePort.PortAssignments = newPortAssignments - } - - devicePort.PortAvailable = newPortAvailable - devicePort.UpdatedAt = time.Now() - } - - return tx.Save(&devicePort).Error + // Get current port usage + var devicePort entity.DevicePort + if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { + return err + } + + // Check if new port amount is sufficient for current usage + if newPortAmount < devicePort.PortUsed { + return fmt.Errorf("cannot reduce port amount to %d, currently using %d ports", newPortAmount, devicePort.PortUsed) + } + + // Special case: if port_amount is set to 0, clear all assignments + if newPortAmount == 0 { + devicePort.PortUsed = 0 + devicePort.PortAvailable = 0 + devicePort.CustomerCount = 0 + devicePort.CustomerNames = []string{} + devicePort.PortAssignments = entity.PortAssignments{} + devicePort.UpdatedAt = time.Now() + } else { + // Calculate new port available + newPortAvailable := newPortAmount - devicePort.PortUsed + + // Update port assignments to match new port amount + if len(devicePort.PortAssignments) > 0 { + // Resize port assignments array + newPortAssignments := make(entity.PortAssignments, newPortAmount) + + // Copy existing assignments up to the new port amount + for i := 0; i < newPortAmount; i++ { + if i < len(devicePort.PortAssignments) { + newPortAssignments[i] = devicePort.PortAssignments[i] + } else { + newPortAssignments[i] = entity.PortAssignment{ + PortNumber: i + 1, + CustomerName: nil, + } + } + } + devicePort.PortAssignments = newPortAssignments + } + + devicePort.PortAvailable = newPortAvailable + devicePort.UpdatedAt = time.Now() + } + + return tx.Save(&devicePort).Error } func (r *deviceDetailsRepo) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error { - return r.db.Transaction(func(tx *gorm.DB) error { - var device entity.Device - if err := tx.Set("gorm:query_option", "FOR UPDATE"). - Where("id = ?", deviceID).First(&device).Error; err != nil { - return err - } + return r.db.Transaction(func(tx *gorm.DB) error { + var device entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", deviceID).First(&device).Error; err != nil { + return err + } - if portUsed > device.PortAmount { - return fmt.Errorf("port_used (%d) cannot exceed port_amount (%d)", portUsed, device.PortAmount) - } + if portUsed > device.PortAmount { + return fmt.Errorf("port_used (%d) cannot exceed port_amount (%d)", portUsed, device.PortAmount) + } - 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) - } + 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) + } - // Initialize port assignments if empty and we have customers or port_used > 0 - if len(devicePort.PortAssignments) == 0 && (len(devicePort.CustomerNames) > 0 || portUsed > 0) { - devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount) - for i := 0; i < device.PortAmount; i++ { - var customerName *string - // Map existing customer names to ports - if i < len(devicePort.CustomerNames) && devicePort.CustomerNames[i] != "" { - customerName = &devicePort.CustomerNames[i] - } - - devicePort.PortAssignments[i] = entity.PortAssignment{ - PortNumber: i + 1, - CustomerName: customerName, - } - } - } + // Initialize port assignments if empty and we have customers or port_used > 0 + if len(devicePort.PortAssignments) == 0 && (len(devicePort.CustomerNames) > 0 || portUsed > 0) { + devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount) + for i := 0; i < device.PortAmount; i++ { + var customerName *string + // Map existing customer names to ports + if i < len(devicePort.CustomerNames) && devicePort.CustomerNames[i] != "" { + customerName = &devicePort.CustomerNames[i] + } - // Update port assignments based on new port_used value - currentCustomerCount := 0 - for _, assignment := range devicePort.PortAssignments { - if assignment.CustomerName != nil && *assignment.CustomerName != "" { - currentCustomerCount++ - } - } + devicePort.PortAssignments[i] = entity.PortAssignment{ + PortNumber: i + 1, + CustomerName: customerName, + } + } + } - 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 port assignments based on new port_used value + currentCustomerCount := 0 + for _, assignment := range devicePort.PortAssignments { + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + currentCustomerCount++ + } + } - // Update counters - devicePort.PortUsed = portUsed - devicePort.PortAvailable = device.PortAmount - portUsed - r.updateDevicePortCounters(&devicePort) - devicePort.UpdatedAt = time.Now() + 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 + } + } + } + } - return tx.Save(&devicePort).Error - }) + // Update counters + devicePort.PortUsed = portUsed + devicePort.PortAvailable = device.PortAmount - portUsed + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) } func (r *deviceDetailsRepo) MigrateCustomerNamesToPortAssignments(devicePort *entity.DevicePort, devicePortAmount int) error { - // Only migrate if PortAssignments is empty but CustomerNames has data - if len(devicePort.PortAssignments) == 0 && len(devicePort.CustomerNames) > 0 { - devicePort.PortAssignments = make(entity.PortAssignments, devicePortAmount) - - for i := 0; i < devicePortAmount; i++ { - var customerName *string - if i < len(devicePort.CustomerNames) && devicePort.CustomerNames[i] != "" { - customerName = &devicePort.CustomerNames[i] - } - - devicePort.PortAssignments[i] = entity.PortAssignment{ - PortNumber: i + 1, - CustomerName: customerName, - } - } - } - return nil + // Only migrate if PortAssignments is empty but CustomerNames has data + if len(devicePort.PortAssignments) == 0 && len(devicePort.CustomerNames) > 0 { + devicePort.PortAssignments = make(entity.PortAssignments, devicePortAmount) + + for i := 0; i < devicePortAmount; i++ { + var customerName *string + if i < len(devicePort.CustomerNames) && devicePort.CustomerNames[i] != "" { + customerName = &devicePort.CustomerNames[i] + } + + devicePort.PortAssignments[i] = entity.PortAssignment{ + PortNumber: i + 1, + CustomerName: customerName, + } + } + } + return nil } // func (r *deviceDetailsRepo) ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error { @@ -770,355 +817,400 @@ func (r *deviceDetailsRepo) MigrateCustomerNamesToPortAssignments(devicePort *en // if err := r.db.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { // return err // } - + // if devicePort.Portvailable < requiredPorts { // return errors.New("insufficient available ports") // } - + // return nil // } func (r *deviceDetailsRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) { - var backbones []entity.Backbone - err := r.db.Preload("DeviceStart").Preload("DeviceEnd"). - Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). - Find(&backbones).Error - return backbones, err + var backbones []entity.Backbone + err := r.db.Preload("DeviceStart").Preload("DeviceEnd"). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Find(&backbones).Error + return backbones, err } func (r *deviceDetailsRepo) GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) { - var fishbones []entity.Fishbone - err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd"). - Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). - Find(&fishbones).Error - return fishbones, err + var fishbones []entity.Fishbone + err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd"). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Find(&fishbones).Error + return fishbones, err } func (r *deviceDetailsRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) { - var towers []entity.Tower - err := r.db.Where("dev_id = ?", deviceID).Find(&towers).Error - return towers, err + var towers []entity.Tower + err := r.db.Where("dev_id = ?", deviceID).Find(&towers).Error + return towers, err } - - func (r *deviceDetailsRepo) GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error) { - var devicePort entity.DevicePort - err = r.db.Where("device_id = ?", deviceID).First(&devicePort).Error - if err != nil { - return 0, 0, err - } - return devicePort.PortUsed, devicePort.PortAvailable, nil + var devicePort entity.DevicePort + err = r.db.Where("device_id = ?", deviceID).First(&devicePort).Error + if err != nil { + return 0, 0, err + } + return devicePort.PortUsed, devicePort.PortAvailable, nil } func (r *deviceDetailsRepo) Delete(id uuid.UUID) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Delete device port first - if err := tx.Where("device_id = ?", id).Delete(&entity.DevicePort{}).Error; err != nil { - return err - } - - // Delete device - return tx.Delete(&entity.Device{}, id).Error - }) + return r.db.Transaction(func(tx *gorm.DB) error { + // Delete device port first + if err := tx.Where("device_id = ?", id).Delete(&entity.DevicePort{}).Error; err != nil { + return err + } + + // Delete device + return tx.Delete(&entity.Device{}, id).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 - if err := tx.Set("gorm:query_option", "FOR UPDATE"). - Where("id = ?", deviceID).First(&device).Error; err != nil { - return err - } + 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 customers assigned - if device.DeviceType != "ODP" { - return fmt.Errorf("customers can only be assigned to ODP devices") - } + // Only ODP devices can have customers assigned + if device.DeviceType != "ODP" { + return fmt.Errorf("customers can only be assigned to 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) - } + 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) + } - // 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, - Status: entity.PortStatusOff, // Default status - Bandwidth: nil, - } - } - } + // 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, + Status: entity.PortStatusOff, // Default status + Bandwidth: nil, + } + } + } - // 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") - } - } + // 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 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) - } - } + // 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 - devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn // Default to "on" when assigning + // Assign customer to port + devicePort.PortAssignments[portIndex].CustomerName = &customerName + devicePort.PortAssignments[portIndex].Status = entity.PortStatusOn // Default to "on" when assigning - // Update both PortAssignments and CustomerNames for backward compatibility - r.updateDevicePortCounters(&devicePort) - devicePort.UpdatedAt = time.Now() + // Update both PortAssignments and CustomerNames for backward compatibility + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() - return tx.Save(&devicePort).Error - }) + return tx.Save(&devicePort).Error + }) } func (r *deviceDetailsRepo) RemoveCustomerFromPort(deviceID uuid.UUID, customerName string) error { - return r.db.Transaction(func(tx *gorm.DB) 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) - } + return r.db.Transaction(func(tx *gorm.DB) 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) + } - // Find and remove customer - newCustomerNames := make([]string, 0) - found := false - for _, existing := range devicePort.CustomerNames { - if existing != customerName { - newCustomerNames = append(newCustomerNames, existing) - } else { - found = true - } - } + // Find and remove customer + newCustomerNames := make([]string, 0) + found := false + for _, existing := range devicePort.CustomerNames { + if existing != customerName { + newCustomerNames = append(newCustomerNames, existing) + } else { + found = true + } + } - if !found { - return fmt.Errorf("customer %s is not assigned to this device", customerName) - } + if !found { + return fmt.Errorf("customer %s is not assigned to this device", customerName) + } - devicePort.CustomerNames = newCustomerNames - devicePort.CustomerCount = len(devicePort.CustomerNames) - devicePort.PortAvailable = devicePort.PortAvailable + 1 - devicePort.UpdatedAt = time.Now() + devicePort.CustomerNames = newCustomerNames + devicePort.CustomerCount = len(devicePort.CustomerNames) + devicePort.PortAvailable = devicePort.PortAvailable + 1 + devicePort.UpdatedAt = time.Now() - return tx.Save(&devicePort).Error - }) + return tx.Save(&devicePort).Error + }) } 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 - }) + // 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) { - // Get device port amount (need to load device if not loaded) - var device entity.Device - if devicePort.Device.ID == uuid.Nil { - r.db.Where("id = ?", devicePort.DeviceID).First(&device) - devicePort.Device = device - } else { - device = devicePort.Device - } - - // Count actual customers and find highest used port - customerCount := 0 - customerNames := make([]string, 0) - highestCustomerPort := 0 - - for _, assignment := range devicePort.PortAssignments { - if assignment.CustomerName != nil && *assignment.CustomerName != "" { - customerCount++ - customerNames = append(customerNames, *assignment.CustomerName) // Just customer names, not with port numbers - if assignment.PortNumber > highestCustomerPort { - highestCustomerPort = assignment.PortNumber - } - } - } - - // Set port_used to the highest port number that has a customer - // This ensures we don't have gaps in port usage - if customerCount == 0 { - devicePort.PortUsed = 0 - } else { - devicePort.PortUsed = highestCustomerPort - } - - // Calculate port_available - devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed - - // Update customer fields - devicePort.CustomerCount = customerCount - devicePort.CustomerNames = customerNames + // Get device port amount (need to load device if not loaded) + var device entity.Device + if devicePort.Device.ID == uuid.Nil { + r.db.Where("id = ?", devicePort.DeviceID).First(&device) + devicePort.Device = device + } else { + device = devicePort.Device + } + + // Count actual customers and find highest occupied port + customerCount := 0 + customerNames := make([]string, 0) + highestOccupiedPort := 0 + + // Get ports with customers and check IsOccupied field + portsWithCustomers := make([]int, 0) + for _, assignment := range devicePort.PortAssignments { + // Count customers + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + customerCount++ + customerNames = append(customerNames, *assignment.CustomerName) + portsWithCustomers = append(portsWithCustomers, assignment.PortNumber) + } + + // Check occupation (either by IsOccupied field or customer presence) + if assignment.IsOccupied || (assignment.CustomerName != nil && *assignment.CustomerName != "") { + if assignment.PortNumber > highestOccupiedPort { + highestOccupiedPort = assignment.PortNumber + } + } + } + + // For ODP devices, we need to maintain the concept of "used ports" even without customer names + // This handles the case where port_used was set manually or ports are occupied without customer names + if device.DeviceType == "ODP" { + // If current port_used is higher than what we calculated from customer names, + // it means there are occupied ports without customer names + if devicePort.PortUsed > highestOccupiedPort { + highestOccupiedPort = devicePort.PortUsed + } + + // Ensure we have assignments for all ports up to the highest occupied port + if len(devicePort.PortAssignments) == 0 || len(devicePort.PortAssignments) < device.PortAmount { + devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount) + for i := 0; i < device.PortAmount; i++ { + devicePort.PortAssignments[i] = entity.PortAssignment{ + PortNumber: i + 1, + CustomerName: nil, + IsOccupied: false, + Status: entity.PortStatusOff, + Bandwidth: nil, + } + } + + // Restore customer assignments + for _, portNum := range portsWithCustomers { + portIndex := portNum - 1 + for _, assignment := range devicePort.PortAssignments { + if assignment.PortNumber == portNum && assignment.CustomerName != nil { + devicePort.PortAssignments[portIndex].CustomerName = assignment.CustomerName + devicePort.PortAssignments[portIndex].IsOccupied = assignment.IsOccupied + devicePort.PortAssignments[portIndex].Status = assignment.Status + devicePort.PortAssignments[portIndex].Bandwidth = assignment.Bandwidth + break + } + } + } + } + } + + // Set port_used to the highest occupied port (with or without customer) + devicePort.PortUsed = highestOccupiedPort + + // Calculate port_available + devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed + + // Update customer fields + devicePort.CustomerCount = customerCount + devicePort.CustomerNames = customerNames } func (r *deviceDetailsRepo) AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) 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 - } + 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 customers assigned - if device.DeviceType != "ODP" { - return fmt.Errorf("customers can only be assigned to ODP devices") - } + // Only ODP devices can have customers assigned + if device.DeviceType != "ODP" { + return fmt.Errorf("customers can only be assigned to 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) - } + 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) + } - // 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, - } - } - } + // 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, + } + } + } - // Check if there are enough available ports - availablePorts := 0 - for _, assignment := range devicePort.PortAssignments { - if assignment.CustomerName == nil || *assignment.CustomerName == "" { - availablePorts++ - } - } + // Check if there are enough available ports + availablePorts := 0 + for _, assignment := range devicePort.PortAssignments { + if assignment.CustomerName == nil || *assignment.CustomerName == "" { + availablePorts++ + } + } - portsNeeded := len(assignments) - if portsNeeded > availablePorts { - return fmt.Errorf("not enough available ports: need %d, available %d", portsNeeded, availablePorts) - } + portsNeeded := len(assignments) + if portsNeeded > availablePorts { + return fmt.Errorf("not enough available ports: need %d, available %d", portsNeeded, availablePorts) + } - // Process each assignment - for _, assignment := range assignments { - var targetPortNumber int - - if assignment.PortNumber != nil { - targetPortNumber = *assignment.PortNumber - if targetPortNumber < 1 || targetPortNumber > device.PortAmount { - return fmt.Errorf("port number %d must be between 1 and %d", targetPortNumber, device.PortAmount) - } - } else { - // Auto-assign to first available port - targetPortNumber = -1 - for i, portAssignment := range devicePort.PortAssignments { - if portAssignment.CustomerName == nil || *portAssignment.CustomerName == "" { - targetPortNumber = i + 1 - break - } - } - if targetPortNumber == -1 { - return fmt.Errorf("no available ports for customer assignment") - } - } + // Process each assignment + for _, assignment := range assignments { + var targetPortNumber int - // 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) - } + if assignment.PortNumber != nil { + targetPortNumber = *assignment.PortNumber + if targetPortNumber < 1 || targetPortNumber > device.PortAmount { + return fmt.Errorf("port number %d must be between 1 and %d", targetPortNumber, device.PortAmount) + } + } else { + // Auto-assign to first available port + targetPortNumber = -1 + for i, portAssignment := range devicePort.PortAssignments { + if portAssignment.CustomerName == nil || *portAssignment.CustomerName == "" { + targetPortNumber = i + 1 + break + } + } + if targetPortNumber == -1 { + return fmt.Errorf("no available ports for customer assignment") + } + } - // Check if customer is already assigned to another port - for i, portAssignment := range devicePort.PortAssignments { - if portAssignment.CustomerName != nil && *portAssignment.CustomerName == assignment.CustomerName { - return fmt.Errorf("customer %s is already assigned to port %d", assignment.CustomerName, i+1) - } - } + // 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) + } - // Assign customer to port - devicePort.PortAssignments[portIndex].CustomerName = &assignment.CustomerName - } + // Check if customer is already assigned to another port + for i, portAssignment := range devicePort.PortAssignments { + if portAssignment.CustomerName != nil && *portAssignment.CustomerName == assignment.CustomerName { + return fmt.Errorf("customer %s is already assigned to port %d", assignment.CustomerName, i+1) + } + } - // Update counters and backward compatibility fields - r.updateDevicePortCounters(&devicePort) - devicePort.UpdatedAt = time.Now() + // Assign customer to port + devicePort.PortAssignments[portIndex].CustomerName = &assignment.CustomerName + } - return tx.Save(&devicePort).Error - }) + // Update counters and backward compatibility fields + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) } func (r *deviceDetailsRepo) RecalculatePortUsageAfterBulkUpdate(deviceID uuid.UUID) error { - return r.db.Transaction(func(tx *gorm.DB) error { - var device entity.Device - if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil { - return err - } + return r.db.Transaction(func(tx *gorm.DB) error { + var device entity.Device + if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil { + return err + } - var devicePort entity.DevicePort - if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { - return err - } + var devicePort entity.DevicePort + if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { + return err + } - // Find the highest port number with a customer - highestUsedPort := 0 - customerCount := 0 - - for _, assignment := range devicePort.PortAssignments { - if assignment.CustomerName != nil && *assignment.CustomerName != "" { - customerCount++ - if assignment.PortNumber > highestUsedPort { - highestUsedPort = assignment.PortNumber - } - } - } - - // Update port usage - devicePort.PortUsed = highestUsedPort - devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed - - // Update other counters - r.updateDevicePortCounters(&devicePort) - devicePort.UpdatedAt = time.Now() - - return tx.Save(&devicePort).Error - }) -} \ No newline at end of file + // Find the highest port number that is occupied (using IsOccupied field) + highestOccupiedPort := 0 + customerCount := 0 + + // Check all assignments for occupation and customers + for _, assignment := range devicePort.PortAssignments { + // Count customers + if assignment.CustomerName != nil && *assignment.CustomerName != "" { + customerCount++ + } + + // Check occupation using IsOccupied field or customer presence + if assignment.IsOccupied || (assignment.CustomerName != nil && *assignment.CustomerName != "") { + if assignment.PortNumber > highestOccupiedPort { + highestOccupiedPort = assignment.PortNumber + } + } + } + + // Update port usage - this represents the highest occupied port number + devicePort.PortUsed = highestOccupiedPort + devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed + + // Update other counters using the updated logic + r.updateDevicePortCounters(&devicePort) + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) +}