fix: port assignment bulk update

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

View File

@ -64,8 +64,6 @@ func (s *Server) setupController() {
publicRegistration.POST("/register", controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).RegisterUser)
}
// Add user registration controller (public endpoints)
// controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).Route()
@ -96,8 +94,6 @@ func (s *Server) setupController() {
}
func (s *Server) Run() {
s.setupController()
if err := s.engine.Run(s.host); err != nil {

View File

@ -22,7 +22,11 @@ type infraManager struct {
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{})
// 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
}
@ -41,13 +45,13 @@ func (im *infraManager) autoMigrate(db *gorm.DB) error {
&entity.Role{},
&entity.User{},
&entity.Device{},
&entity.Tower{},
&entity.OLT{},
&entity.DevicePort{},
&entity.Backbone{},
&entity.Fishbone{},
&entity.Tower{},
&entity.DevicePort{},
&entity.CountAssets{},
&entity.ActivityLog{},
&entity.OLT{},
)
}

View File

@ -3,6 +3,7 @@ package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
@ -14,13 +15,13 @@ func CORSMiddleware() gin.HandlerFunc {
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:80", // Add production URL
"http://103.110.8.103",
"http://nam.winteraccess.id",
"https://nam.winteraccess.id",
}
// Check if origin is in allowed list

View File

@ -39,6 +39,7 @@ type UpdateDeviceDetailsDTO struct {
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
}
@ -46,6 +47,7 @@ type AssignMultipleCustomersDTO struct {
type UpdateCustomerByPortDTO struct {
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
}

View File

@ -16,7 +16,6 @@ type UpdateDevicePort struct {
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
@ -31,6 +30,7 @@ type UpdatePortUsageDTO struct {
type PortAssignmentDTO struct {
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"`
}

View File

@ -56,6 +56,7 @@ func (s *StringSlice) Scan(value interface{}) error {
type PortAssignment struct {
PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"` // Nullable for empty ports
IsOccupied bool `json:"is_occupied"` // Explicitly store occupation status
Status PortStatus `json:"status"` // Add status field
Bandwidth *string `json:"bandwidth"` // Add bandwidth field (nullable)
}
@ -222,23 +223,39 @@ func (dp *DevicePort) GetPortAssignmentsWithDetails() []PortAssignmentResponse {
// Sort to get them in order
sort.Ints(portsWithCustomers)
// Step 3: Determine occupied ports
// Step 3: Determine occupied ports - USE EXPLICIT IsOccupied FIELD
occupiedPorts := make(map[int]bool)
// All ports with customers are occupied
for _, portNum := range portsWithCustomers {
occupiedPorts[portNum] = true
// 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
}
// 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++ {
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
occupiedCount++
currentOccupiedCount++
}
}
}
}
@ -253,8 +270,10 @@ func (dp *DevicePort) GetPortAssignmentsWithDetails() []PortAssignmentResponse {
customerName = assignment.CustomerName
status = assignment.Status
bandwidth = assignment.Bandwidth
} else if occupiedPorts[i] {
// Port is occupied but no assignment data - use defaults
}
// Override status for legacy occupied ports (those without explicit assignment)
if occupiedPorts[i] && portMap[i] == nil {
status = PortStatusOn
}

View File

@ -41,8 +41,6 @@ type DeviceDetailsRepo interface {
GetDevicesWithoutConnections(deviceTypes []string) ([]entity.DeviceDetails, error)
ValidateTowerExists(towerID uuid.UUID) (bool, error)
SyncTowerLocationWithDevice(deviceID uuid.UUID) error
}
type deviceDetailsRepo struct {
@ -152,6 +150,7 @@ func (r *deviceDetailsRepo) UpdateCustomerByPort(deviceID uuid.UUID, update req.
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
IsOccupied: false,
Status: entity.PortStatusOff,
Bandwidth: nil,
}
@ -165,7 +164,16 @@ func (r *deviceDetailsRepo) UpdateCustomerByPort(deviceID uuid.UUID, update req.
portIndex := update.PortNumber - 1
// If assigning a new customer name
// 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 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 {
@ -175,26 +183,35 @@ func (r *deviceDetailsRepo) UpdateCustomerByPort(deviceID uuid.UUID, update req.
}
}
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
// Update customer name
if update.NewCustomerName != nil {
devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName
}
// Update status if provided, otherwise default to "on"
// 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 {
} 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
// 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
@ -269,57 +286,64 @@ func (r *deviceDetailsRepo) BulkUpdateCustomersByPort(deviceID uuid.UUID, update
}
}
// Apply all updates - FIX: Include Status and Bandwidth updates
// Apply all updates - Include Status, Bandwidth and IsOccupied updates
highestOccupiedPort := 0
for _, update := range updates {
portIndex := update.PortNumber - 1
// Update customer name
devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName
// Update status if provided
// 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 {
// Set default status based on customer assignment
if update.NewCustomerName != nil && *update.NewCustomerName != "" {
} 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 update.NewCustomerName == nil || *update.NewCustomerName == "" {
// Clear bandwidth when removing customer
} 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 actual assignments
highestUsedPort := 0
customerCount := 0
// 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.CustomerName != nil && *assignment.CustomerName != "" {
customerCount++
if assignment.PortNumber > highestUsedPort {
highestUsedPort = assignment.PortNumber
if assignment.IsOccupied {
if assignment.PortNumber > highestOccupiedPort {
highestOccupiedPort = 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
}
}
// Update port_used to reflect highest occupied port
devicePort.PortUsed = highestOccupiedPort
// Recalculate port_available
devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed
@ -367,7 +391,6 @@ func (r *deviceDetailsRepo) RemoveCustomerByPort(deviceID uuid.UUID, portNumber
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
@ -424,6 +447,9 @@ func (r *deviceDetailsRepo) UpdatePortAssignments(deviceID uuid.UUID, assignment
}
}
// Track highest occupied port for calculating port_used
highestOccupiedPort := 0
// Update port assignments
for _, assignment := range assignments {
portIndex := assignment.PortNumber - 1
@ -431,10 +457,23 @@ func (r *deviceDetailsRepo) UpdatePortAssignments(deviceID uuid.UUID, assignment
devicePort.PortAssignments[portIndex].PortNumber = assignment.PortNumber
devicePort.PortAssignments[portIndex].CustomerName = assignment.CustomerName
// Update status if provided, otherwise default based on customer presence
// 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 assignment.CustomerName != nil && *assignment.CustomerName != "" {
} 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
@ -444,29 +483,37 @@ func (r *deviceDetailsRepo) UpdatePortAssignments(deviceID uuid.UUID, assignment
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)
// Calculate port usage based on assignments
portUsed := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
portUsed++
}
}
devicePort.PortUsed = portUsed
devicePort.PortAvailable = device.PortAmount - portUsed
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) Create(device entity.Device) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Validate tower exists if TowerID is provided
@ -800,8 +847,6 @@ func (r *deviceDetailsRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.To
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
@ -962,29 +1007,70 @@ func (r *deviceDetailsRepo) updateDevicePortCounters(devicePort *entity.DevicePo
device = devicePort.Device
}
// Count actual customers and find highest used port
// Count actual customers and find highest occupied port
customerCount := 0
customerNames := make([]string, 0)
highestCustomerPort := 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) // Just customer names, not with port numbers
if assignment.PortNumber > highestCustomerPort {
highestCustomerPort = assignment.PortNumber
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
}
}
}
// 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
// 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
@ -1098,24 +1184,30 @@ func (r *deviceDetailsRepo) RecalculatePortUsageAfterBulkUpdate(deviceID uuid.UU
return err
}
// Find the highest port number with a customer
highestUsedPort := 0
// 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++
if assignment.PortNumber > highestUsedPort {
highestUsedPort = assignment.PortNumber
}
// 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
devicePort.PortUsed = highestUsedPort
// Update port usage - this represents the highest occupied port number
devicePort.PortUsed = highestOccupiedPort
devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed
// Update other counters
// Update other counters using the updated logic
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()