adding more response for port assignments

This commit is contained in:
areeqakbr 2025-06-14 15:30:04 +07:00
parent 567f286d21
commit ece05cee18
16 changed files with 860 additions and 175 deletions

View File

@ -32,9 +32,40 @@ func (c *DeviceDetailsController) Route() {
deviceDetails.PUT("/:id", c.updateDeviceDetails)
deviceDetails.DELETE("/:id", c.deleteDeviceDetails)
deviceDetails.POST("/:id/recalculate-ports", c.recalculatePortUsage)
deviceDetails.POST("/:id/assign-customer", c.assignCustomer)
deviceDetails.PUT("/:id/port-usage", c.updatePortUsage) // New endpoint
deviceDetails.PUT("/:id/port-assignments", c.updatePortAssignments) // New endpoint
}
}
func (c *DeviceDetailsController) assignCustomer(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request struct {
CustomerName string `json:"customer_name"`
PortNumber *int `json:"port_number"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.AssignCustomerToPort(deviceID, request.CustomerName, request.PortNumber)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Customer assigned successfully", nil)
}
func (c *DeviceDetailsController) getAllDeviceDetails(ctx *gin.Context) {
devices, err := c.deviceDetailsUC.GetAllDeviceDetails()
if err != nil {
@ -133,4 +164,50 @@ func (c *DeviceDetailsController) recalculatePortUsage(ctx *gin.Context) {
}
common.SingleResponses(ctx, "Port usage recalculated successfully", nil)
}
func (c *DeviceDetailsController) updatePortUsage(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.UpdatePortUsageDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.UpdatePortUsage(deviceID, request.PortUsed)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Port usage updated successfully", nil)
}
func (c *DeviceDetailsController) updatePortAssignments(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.UpdatePortAssignmentsDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.UpdatePortAssignments(deviceID, request.PortAssignments)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Port assignments updated successfully", nil)
}

View File

@ -26,6 +26,7 @@ func (fc *FishboneController) Route() {
rg.POST("", fc.CreateFishbone())
rg.GET("/:uuid", fc.GetFishboneByID())
rg.PUT("/:uuid", fc.UpdateFishbone())
rg.GET("/backbone/:backbone_id", fc.GetFishboneByBackboneID())
}
}
@ -36,6 +37,30 @@ func NewFishboneController(fu usecase.FishboneUseCase, rg *gin.RouterGroup) *Fis
}
}
func (fc *FishboneController) GetFishboneByBackboneID() gin.HandlerFunc {
return func(c *gin.Context) {
backboneIDStr := c.Param("backbone_id")
backboneID, err := uuid.Parse(backboneIDStr)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid backbone ID format")
return
}
fishbones, err := fc.fu.GetByBackboneID(backboneID)
if err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
return
}
if len(fishbones) == 0 {
common.SingleResponses(c, "No fishbones found for this backbone", []interface{}{})
return
}
common.SingleResponses(c, "Success", fishbones)
}
}
func (fc *FishboneController) GetFishbone() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println("Fetching all fishbones")

View File

@ -3,6 +3,7 @@ package controller
import (
"net/http"
"strconv"
"strings"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -51,6 +52,27 @@ func (tc *TowerController) GetTower() gin.HandlerFunc {
func (tc *TowerController) CreateTower() gin.HandlerFunc {
return func(c *gin.Context) {
contentType := c.GetHeader("Content-Type")
// Handle JSON request
if strings.Contains(contentType, "application/json") {
var towerDTO req.TowerDTO
if err := c.ShouldBindJSON(&towerDTO); err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid JSON format: "+err.Error())
return
}
// No image file for JSON requests
err := tc.tu.Post(towerDTO, nil)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Tower has been created", nil)
return
}
// Parse multipart form
err := c.Request.ParseMultipartForm(10 << 20) // 10MB max
if err != nil {
@ -58,6 +80,8 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
return
}
// Extract form data
deviceIDStr := c.PostForm("dev_id")
towerCode := c.PostForm("tower_code")
@ -148,14 +172,35 @@ func (tc *TowerController) GetTowerByID() gin.HandlerFunc {
func (tc *TowerController) UpdateTower() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("uuid")
tower_uuid, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
// Parse multipart form
contentType := c.GetHeader("Content-Type")
// Handle JSON request
if strings.Contains(contentType, "application/json") {
var towerUpdateDTO req.UpdateTowerDTO
if err := c.ShouldBindJSON(&towerUpdateDTO); err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid JSON format: "+err.Error())
return
}
// No image file for JSON requests
err := tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, nil)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Tower has been updated", nil)
return
}
// Handle multipart form request (existing logic)
err = c.Request.ParseMultipartForm(10 << 20) // 10MB max
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
@ -165,10 +210,13 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
// Create update DTO
towerUpdateDTO := req.UpdateTowerDTO{}
// Optional fields
// Optional fields - handle form data
if deviceIDStr := c.PostForm("device_id"); deviceIDStr != "" {
if deviceID, err := uuid.Parse(deviceIDStr); err == nil {
towerUpdateDTO.DeviceID = &deviceID
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID format")
return
}
}
@ -179,15 +227,36 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
if longitudeStr := c.PostForm("longitude"); longitudeStr != "" {
if longitude, err := strconv.ParseFloat(longitudeStr, 64); err == nil {
towerUpdateDTO.Longitude = &longitude
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid longitude format")
return
}
}
if latitudeStr := c.PostForm("latitude"); latitudeStr != "" {
if latitude, err := strconv.ParseFloat(latitudeStr, 64); err == nil {
towerUpdateDTO.Latitude = &latitude
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid latitude format")
return
}
}
// Handle external_tower field
if externalTowerStr := c.PostForm("external_tower"); externalTowerStr != "" {
if externalTower, err := strconv.ParseBool(externalTowerStr); err == nil {
towerUpdateDTO.ExternalTower = &externalTower
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid external_tower value")
return
}
}
// Handle image_url field (for updating image URL directly)
if imageURL := c.PostForm("image_url"); imageURL != "" {
towerUpdateDTO.ImageURL = &imageURL
}
// Get image file (optional)
imageFile, _ := c.FormFile("image")

View File

@ -51,7 +51,6 @@ func (s *Server) setupController() {
controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route()
rg.Use(middleware.AuthMiddleware(s.ucManager.NewUserUsecase()))
rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase()))
rg.Use(middleware.RateLimitMiddleware())
{
controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route()
controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), rg).Route()

View File

@ -10,4 +10,23 @@ type DevicePort struct {
type UpdateDevicePort struct {
DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"`
PortNumber *int `json:"port_number,omitempty" validate:"omitempty,min=1"`
}
type AssignCustomerToPortDTO struct {
CustomerName string `json:"customer_name" binding:"required"`
PortNumber *int `json:"port_number,omitempty"` // Optional, will auto-assign if not provided
}
type UpdatePortUsageDTO struct {
PortUsed int `json:"port_used" binding:"required,min=0"`
}
type PortAssignmentDTO struct {
PortNumber int `json:"port_number" binding:"required,min=1"`
CustomerName *string `json:"customer_name,omitempty"`
}
type UpdatePortAssignmentsDTO struct {
PortAssignments []PortAssignmentDTO `json:"port_assignments" binding:"required"`
}

View File

@ -6,29 +6,31 @@ import (
)
type DeviceDetailsResponse struct {
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType string `json:"device_type"`
Address string `json:"address"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Status string `json:"status"`
PortAmount int `json:"port_amount"`
PortUsed int `json:"port_used"`
PortAvailable int `json:"port_available"`
Region *string `json:"region,omitempty"`
Province *string `json:"province,omitempty"`
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
ImageURL *string `json:"image_url,omitempty"`
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType string `json:"device_type"`
Address string `json:"address"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Status string `json:"status"`
PortAmount int `json:"port_amount"`
PortUsed int `json:"port_used"`
PortAvailable int `json:"port_available"`
CustomerNames []string `json:"customer_names"` // Just customer names
PortAssignments []PortAssignmentResponse `json:"port_assignments"` // Port details with customers
Region *string `json:"region,omitempty"`
Province *string `json:"province,omitempty"`
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
ImageURL *string `json:"image_url,omitempty"`
// Connection details
Backbones []BackboneConnectionInfo `json:"backbones"`
Fishbones []FishboneConnectionInfo `json:"fishbones"`
Towers []TowerConnectionDetail `json:"towers"`
Backbones []BackboneConnectionInfo `json:"backbones"`
Fishbones []FishboneConnectionInfo `json:"fishbones"`
Towers []TowerConnectionDetail `json:"towers"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type TowerConnectionDetail struct {
@ -37,4 +39,10 @@ type TowerConnectionDetail struct {
Distance float64 `json:"distance_km"` // Distance from tower to device
ExternalTower *bool `json:"external_tower"` // Indicates if this is an external tower
ImageURL *string `json:"image_url,omitempty"`
}
type PortAssignmentResponse struct {
PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"`
IsOccupied bool `json:"is_occupied"`
}

View File

@ -15,6 +15,7 @@ type DeviceResponse struct {
Address string `json:"address"`
PortAmount int `json:"port_amount"`
Status string `json:"status"` // Always include, even if null
CustomerNames []string `json:"customer_names"` // Always include, even if empty
Province *string `json:"province"` // Always include, even if null
City *string `json:"city"` // Always include, even if null
District *string `json:"district"` // Always include, even if null

View File

@ -13,7 +13,7 @@ type TowerResponse struct {
Latitude float64 `json:"latitude"`
Address string `json:"address"`
ImageURL string `json:"image_url"`
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
ExternalTower *bool `json:"external_tower"` // Make nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -1,24 +1,267 @@
package entity
import (
"time"
"github.com/google/uuid"
"database/sql/driver"
"encoding/json"
"fmt"
"sort"
"time"
"github.com/google/uuid"
)
type StringSlice []string
func (s StringSlice) Value() (driver.Value, error) {
if len(s) == 0 {
return "[]", nil
}
return json.Marshal(s)
}
func (s *StringSlice) Scan(value interface{}) error {
if value == nil {
*s = StringSlice{}
return nil
}
var bytes []byte
switch v := value.(type) {
case []byte: // []byte and []uint8 are the same type in Go
bytes = v
case string:
bytes = []byte(v)
default:
return fmt.Errorf("cannot scan %T into StringSlice", value)
}
// Handle empty string case
if len(bytes) == 0 {
*s = StringSlice{}
return nil
}
return json.Unmarshal(bytes, s)
}
type PortAssignment struct {
PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"` // Nullable for empty ports
}
type PortAssignmentResponse struct {
PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"`
IsOccupied bool `json:"is_occupied"`
}
type PortAssignments []PortAssignment
func (p PortAssignments) Value() (driver.Value, error) {
if len(p) == 0 {
return "[]", nil
}
return json.Marshal(p)
}
func (p *PortAssignments) Scan(value interface{}) error {
if value == nil {
*p = PortAssignments{}
return nil
}
var bytes []byte
switch v := value.(type) {
case []byte: // []byte and []uint8 are the same type in Go
bytes = v
case string:
bytes = []byte(v)
default:
return fmt.Errorf("cannot scan %T into PortAssignments", value)
}
if len(bytes) == 0 {
*p = PortAssignments{}
return nil
}
return json.Unmarshal(bytes, p)
}
type DevicePort struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"`
PortUsed int `json:"port_used"`
PortAvailable int `json:"port_available"`
CustomerCount int `json:"customer_count"` // Add this field
CustomerNames []string `json:"customer_names" gorm:"type:json"` // Store customer names as JSON array
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"`
PortUsed int `json:"port_used"`
PortAvailable int `json:"port_available"`
CustomerCount int `json:"customer_count"`
CustomerNames StringSlice `json:"customer_names" gorm:"type:jsonb"`
PortAssignments PortAssignments `json:"port_assignments" gorm:"type:jsonb"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relationships
Device Device `json:"device" gorm:"foreignKey:DeviceID"`
}
// Helper method to get customer names with port numbers
func (dp *DevicePort) GetCustomerNamesWithPorts() []string {
var result []string
for _, assignment := range dp.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
result = append(result, fmt.Sprintf("%d: %s", assignment.PortNumber, *assignment.CustomerName))
}
}
// If no port assignments, fall back to customer names
if len(result) == 0 {
for i, name := range dp.CustomerNames {
if name != "" {
result = append(result, fmt.Sprintf("%d: %s", i+1, name))
}
}
}
return result
}
// Helper method to get only customer names
func (dp *DevicePort) GetCustomerNames() []string {
var result []string
for _, assignment := range dp.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
result = append(result, *assignment.CustomerName)
}
}
// If no port assignments, fall back to customer names
if len(result) == 0 {
for _, name := range dp.CustomerNames {
if name != "" {
result = append(result, name)
}
}
}
return result
}
func (dp *DevicePort) GetPortAssignmentsWithDetails() []PortAssignmentResponse {
// If both port_used and port_available are 0, return empty array
if dp.PortUsed == 0 && dp.PortAvailable == 0 {
return []PortAssignmentResponse{}
}
// If port_used is 0 but port_available > 0, still return empty array
// This means the device has capacity but no ports are currently used
if dp.PortUsed == 0 {
return []PortAssignmentResponse{}
}
var result []PortAssignmentResponse
// Step 1: Create a map of all port assignments
portMap := make(map[int]*string)
maxPort := 0
if len(dp.PortAssignments) > 0 {
for _, assignment := range dp.PortAssignments {
portMap[assignment.PortNumber] = assignment.CustomerName
if assignment.PortNumber > maxPort {
maxPort = assignment.PortNumber
}
}
} else {
// Fallback: use CustomerNames array
for i, name := range dp.CustomerNames {
if name != "" {
portMap[i+1] = &name
}
if i+1 > maxPort {
maxPort = i + 1
}
}
}
// If no ports have assignments and port_used > 0, create empty assignments up to port_used
if maxPort == 0 && dp.PortUsed > 0 {
maxPort = dp.PortUsed
}
// Step 2: Determine which ports should be occupied
// First, collect all ports that have customers
portsWithCustomers := make([]int, 0)
for portNum, customerName := range portMap {
if customerName != nil && *customerName != "" {
portsWithCustomers = append(portsWithCustomers, portNum)
}
}
// Sort to get them in order
sort.Ints(portsWithCustomers)
// Step 3: Determine occupied ports
occupiedPorts := make(map[int]bool)
// All ports with customers are occupied
for _, portNum := range portsWithCustomers {
occupiedPorts[portNum] = true
}
// Fill remaining slots to reach port_used
occupiedCount := len(portsWithCustomers)
if occupiedCount < dp.PortUsed {
// Need to occupy more ports (ports without customer names but still occupied)
for i := 1; i <= maxPort && occupiedCount < dp.PortUsed; i++ {
if !occupiedPorts[i] {
occupiedPorts[i] = true
occupiedCount++
}
}
// If we still haven't reached port_used, extend maxPort
for i := maxPort + 1; occupiedCount < dp.PortUsed; i++ {
occupiedPorts[i] = true
occupiedCount++
maxPort = i
}
}
// Step 4: Create the result only for ports that are either occupied or have customers
for i := 1; i <= maxPort; i++ {
// Only include ports that are occupied or have customers
if occupiedPorts[i] || (portMap[i] != nil && *portMap[i] != "") {
var customerName *string
if portMap[i] != nil {
customerName = portMap[i]
}
result = append(result, PortAssignmentResponse{
PortNumber: i,
CustomerName: customerName,
IsOccupied: occupiedPorts[i],
})
}
}
return result
}
func (dp *DevicePort) GetCustomerNamesOnly() []string {
var result []string
// If we have PortAssignments data, use it
if len(dp.PortAssignments) > 0 {
for _, assignment := range dp.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
result = append(result, *assignment.CustomerName)
}
}
} else {
// Fallback: use CustomerNames directly
for _, name := range dp.CustomerNames {
if name != "" {
result = append(result, name)
}
}
}
return result
}
func (DevicePort) TableName() string {
return "device_ports"
}

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/entity"
"github.com/google/uuid"
@ -28,8 +29,11 @@ type DeviceDetailsRepo interface {
// Validation helpers
GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error)
AssignCustomerToPort(deviceID uuid.UUID, customerName string) error
AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error
RemoveCustomerFromPort(deviceID uuid.UUID, customerName string) error
UpdatePortUsage(deviceID uuid.UUID, portUsed int) error
UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error
}
type deviceDetailsRepo struct {
@ -42,6 +46,83 @@ func NewDeviceDetailsRepo(db *gorm.DB) DeviceDetailsRepo {
}
}
func (r *deviceDetailsRepo) UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock both device and device_port records
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
// Only ODP devices can have port assignments
if device.DeviceType != "ODP" {
return fmt.Errorf("port assignments can only be updated for ODP devices")
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Validate port numbers are within device capacity
for _, assignment := range assignments {
if assignment.PortNumber < 1 || assignment.PortNumber > device.PortAmount {
return fmt.Errorf("port number %d is out of range (1-%d)", assignment.PortNumber, device.PortAmount)
}
}
// Initialize port assignments array if needed
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
// Check for duplicate customer names (if not null)
customerNames := make(map[string]int) // map customer name to port number
for _, assignment := range assignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
if existingPort, exists := customerNames[*assignment.CustomerName]; exists {
return fmt.Errorf("customer %s is assigned to multiple ports (%d and %d)",
*assignment.CustomerName, existingPort, assignment.PortNumber)
}
customerNames[*assignment.CustomerName] = assignment.PortNumber
}
}
// Update port assignments
for _, assignment := range assignments {
portIndex := assignment.PortNumber - 1
if portIndex < len(devicePort.PortAssignments) {
devicePort.PortAssignments[portIndex].PortNumber = assignment.PortNumber
devicePort.PortAssignments[portIndex].CustomerName = assignment.CustomerName
}
}
// Update counters and backward compatibility fields
r.updateDevicePortCounters(&devicePort)
// Calculate port usage based on assignments
portUsed := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
portUsed++
}
}
devicePort.PortUsed = portUsed
devicePort.PortAvailable = device.PortAmount - portUsed
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) Create(device entity.Device) error {
@ -154,111 +235,64 @@ func (r *deviceDetailsRepo) updatePortAmountCascade(tx *gorm.DB, deviceID uuid.U
}).Error
}
func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error {
func (r *deviceDetailsRepo) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock the device record to prevent race conditions
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
var portUsed int
var customerCount int
var customerNames []string
switch device.DeviceType {
case "OTB":
// For OTB: count backbones (each backbone uses 1 port)
var backboneCount int64
if err := tx.Model(&entity.Backbone{}).
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
Count(&backboneCount).Error; err != nil {
return err
}
portUsed = int(backboneCount)
customerCount = 0 // OTB doesn't serve customers directly
case "closure":
// For closure: count fishbones where this device is the start device
var fishboneCount int64
if err := tx.Model(&entity.Fishbone{}).
Where("dev_start_id = ?", deviceID).
Count(&fishboneCount).Error; err != nil {
return err
}
portUsed = int(fishboneCount)
customerCount = 0 // Closure doesn't serve customers directly
case "ODP":
// For ODP: sum fishbone core amounts where this device is the end device
var totalCores int64
if err := tx.Model(&entity.Fishbone{}).
Where("dev_end_id = ?", deviceID).
Select("COALESCE(SUM(core_amount), 0)").
Scan(&totalCores).Error; err != nil {
return err
}
portUsed = int(totalCores)
// For ODP: customer count should equal port_used
// Get existing customer assignments
var existingDevicePort entity.DevicePort
if err := tx.Where("device_id = ?", deviceID).First(&existingDevicePort).Error; err == nil {
customerNames = existingDevicePort.CustomerNames
}
// Ensure customer count matches port_used for ODP
customerCount = portUsed
// If we have more customers than ports used, trim the list
if len(customerNames) > portUsed {
customerNames = customerNames[:portUsed]
}
default:
portUsed = 0
customerCount = 0
}
// Calculate port available
portAvailable := device.PortAmount - portUsed
if portAvailable < 0 {
portAvailable = 0
if portUsed > device.PortAmount {
return fmt.Errorf("port_used (%d) cannot exceed port_amount (%d)", portUsed, device.PortAmount)
}
// Update or create DevicePort record with locking
result := tx.Set("gorm:query_option", "FOR UPDATE").
Model(&entity.DevicePort{}).
Where("device_id = ?", deviceID).
Updates(map[string]interface{}{
"port_used": portUsed,
"port_available": portAvailable,
"customer_count": customerCount,
"customer_names": customerNames,
"updated_at": gorm.Expr("NOW()"),
})
if result.Error != nil {
return result.Error
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// If no record was updated, create a new one
if result.RowsAffected == 0 {
devicePort := entity.DevicePort{
ID: uuid.New(),
DeviceID: deviceID,
PortUsed: portUsed,
PortAvailable: portAvailable,
CustomerCount: customerCount,
CustomerNames: customerNames,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
// Initialize port assignments if empty
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
return tx.Create(&devicePort).Error
}
return nil
// Update port assignments based on new port_used value
currentCustomerCount := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
currentCustomerCount++
}
}
if portUsed < currentCustomerCount {
// Need to remove some customers (keep first N customers)
customersKept := 0
for i := range devicePort.PortAssignments {
if devicePort.PortAssignments[i].CustomerName != nil && *devicePort.PortAssignments[i].CustomerName != "" {
if customersKept < portUsed {
customersKept++
} else {
devicePort.PortAssignments[i].CustomerName = nil
}
}
}
}
// Update counters
devicePort.PortUsed = portUsed
devicePort.PortAvailable = device.PortAmount - portUsed
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
@ -320,7 +354,7 @@ func (r *deviceDetailsRepo) Delete(id uuid.UUID) error {
})
}
func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerName string) error {
func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock both device and device_port records
var device entity.Device
@ -340,22 +374,57 @@ func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerNam
return fmt.Errorf("device port record not found: %w", err)
}
// Check if there are available ports
if devicePort.PortAvailable <= 0 {
return fmt.Errorf("no available ports for customer assignment")
}
// Check if customer is already assigned
for _, existing := range devicePort.CustomerNames {
if existing == customerName {
return fmt.Errorf("customer %s is already assigned to this device", customerName)
// Initialize port assignments if empty
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
// Add customer to the list
devicePort.CustomerNames = append(devicePort.CustomerNames, customerName)
devicePort.CustomerCount = len(devicePort.CustomerNames)
devicePort.PortAvailable = devicePort.PortAvailable - 1
// Determine port number to use
var targetPortNumber int
if portNumber != nil {
targetPortNumber = *portNumber
if targetPortNumber < 1 || targetPortNumber > device.PortAmount {
return fmt.Errorf("port number must be between 1 and %d", device.PortAmount)
}
} else {
// Auto-assign to first available port
targetPortNumber = -1
for i, assignment := range devicePort.PortAssignments {
if assignment.CustomerName == nil || *assignment.CustomerName == "" {
targetPortNumber = i + 1
break
}
}
if targetPortNumber == -1 {
return fmt.Errorf("no available ports for customer assignment")
}
}
// Check if the specified port is already occupied
portIndex := targetPortNumber - 1
if devicePort.PortAssignments[portIndex].CustomerName != nil &&
*devicePort.PortAssignments[portIndex].CustomerName != "" {
return fmt.Errorf("port %d is already occupied by %s", targetPortNumber, *devicePort.PortAssignments[portIndex].CustomerName)
}
// Check if customer is already assigned to another port
for i, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName == customerName {
return fmt.Errorf("customer %s is already assigned to port %d", customerName, i+1)
}
}
// Assign customer to port
devicePort.PortAssignments[portIndex].CustomerName = &customerName
// Update both PortAssignments and CustomerNames for backward compatibility
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
@ -392,4 +461,37 @@ func (r *deviceDetailsRepo) RemoveCustomerFromPort(deviceID uuid.UUID, customerN
return tx.Save(&devicePort).Error
})
}
}
func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error {
// This method is required by the DeviceDetailsRepo interface
return r.db.Transaction(func(tx *gorm.DB) error {
var devicePort entity.DevicePort
if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Update based on port assignments
r.updateDevicePortCounters(&devicePort)
devicePort.PortUsed = devicePort.CustomerCount
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) updateDevicePortCounters(devicePort *entity.DevicePort) {
// Count actual customers and create customer names list
customerCount := 0
customerNames := make([]string, 0)
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
customerCount++
customerNames = append(customerNames, *assignment.CustomerName)
}
}
devicePort.CustomerCount = customerCount
devicePort.CustomerNames = customerNames
}

View File

@ -24,6 +24,7 @@ type FishboneRepo interface {
CheckDeviceExists(id uuid.UUID) (bool, error)
Delete(id uuid.UUID) error
WithTransaction(fn func(*gorm.DB) error) error
GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error)
}
type fishboneRepo struct {
@ -44,6 +45,16 @@ func (r *fishboneRepo) Post(fishbone entity.Fishbone) error {
return nil
}
func (r *fishboneRepo) GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error) {
var fishbones []entity.Fishbone
err := r.db.Where("bb_id = ?", backboneID).
Preload("DeviceStart").
Preload("DeviceEnd").
Preload("Backbone").
Find(&fishbones).Error
return fishbones, err
}
func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) {
log.Print("Fetching all fishbones with relations")
var fishbones []entity.Fishbone

View File

@ -158,7 +158,7 @@ func (u *backboneUseCase) GetByID(id uuid.UUID) (res.BackboneResponse, error) {
}
return backboneResp, nil
}
}
func (u *backboneUseCase) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackboneDTO) error {
err := u.validate.Struct(backbone)

View File

@ -1,18 +1,18 @@
package usecase
import (
"errors"
"fmt"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils/helper"
"users_management/m/utils/service"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"errors"
"fmt"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils/helper"
"users_management/m/utils/service"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
type DeviceDetailsUseCase interface {
@ -25,6 +25,9 @@ type DeviceDetailsUseCase interface {
// Port management
// ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error
RecalculatePortUsage(deviceID uuid.UUID) error
AssignCustomerToPort(deviceID uuid.UUID, customerName string,portNumber *int) error
UpdatePortUsage(deviceID uuid.UUID, portUsed int) error
UpdatePortAssignments(deviceID uuid.UUID, portAssignments []req.PortAssignmentDTO) error
}
type deviceDetailsUseCase struct {
@ -41,6 +44,32 @@ func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, geo
}
}
func (u *deviceDetailsUseCase) UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error {
// Validate the assignments
if len(assignments) == 0 {
return errors.New("port assignments cannot be empty")
}
// Check for duplicate port numbers
portNumbers := make(map[int]bool)
for _, assignment := range assignments {
if portNumbers[assignment.PortNumber] {
return fmt.Errorf("duplicate port number: %d", assignment.PortNumber)
}
portNumbers[assignment.PortNumber] = true
}
return u.deviceDetailsRepo.UpdatePortAssignments(deviceID, assignments)
}
func (u *deviceDetailsUseCase) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error {
return u.deviceDetailsRepo.UpdatePortUsage(deviceID, portUsed)
}
func (u *deviceDetailsUseCase) CreateDeviceDetails(deviceDTO req.DeviceDetailsDTO) error {
err := u.validate.Struct(deviceDTO)
if err != nil {
@ -173,4 +202,8 @@ func (u *deviceDetailsUseCase) DeleteDeviceDetails(id uuid.UUID) error {
func (u *deviceDetailsUseCase) RecalculatePortUsage(deviceID uuid.UUID) error {
return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID)
}
func (u *deviceDetailsUseCase) AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error {
return u.deviceDetailsRepo.AssignCustomerToPort(deviceID, customerName, portNumber)
}

View File

@ -22,6 +22,7 @@ type FishboneUseCase interface {
UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error
DeleteFishbone(id uuid.UUID) error
GetFishboneStats() (map[string]interface{}, error)
GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error)
}
type fishboneUseCase struct {
@ -40,6 +41,20 @@ func NewFishboneUseCase(fishboneRepo repository.FishboneRepo, backboneRepo repos
}
}
func (u *fishboneUseCase) GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error) {
fishbones, err := u.fishboneRepo.GetByBackboneID(backboneID)
if err != nil {
return nil, err
}
fishboneResp, err := helper.ConvertToFishboneResponses(fishbones, nil)
if err != nil {
return nil, err
}
return fishboneResp, nil
}
func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error {

View File

@ -127,6 +127,9 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, image
if tower.Latitude != nil {
updates["Latitude"] = *tower.Latitude
}
if tower.ExternalTower != nil {
updates["ExternalTower"] = *tower.ExternalTower
}
// Handle image upload
if imageFile != nil {

View File

@ -108,28 +108,108 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
}
towerInfos = append(towerInfos, info)
}
// Convert entity.PortAssignmentResponse to res.PortAssignmentResponse
entityPortAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
resPortAssignments := make([]res.PortAssignmentResponse, len(entityPortAssignments))
for i, pa := range entityPortAssignments {
resPortAssignments[i] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
}
}
portAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
// Ensure we show all ports up to PortAmount
portAssignmentMap := make(map[int]res.PortAssignmentResponse)
for _, pa := range portAssignments {
portAssignmentMap[pa.PortNumber] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
}
}
// Fill in missing ports
finalPortAssignments := make([]res.PortAssignmentResponse, 0)
for i := 1; i <= device.PortAmount; i++ {
if pa, exists := portAssignmentMap[i]; exists {
finalPortAssignments = append(finalPortAssignments, pa)
} else {
// Determine if this port should be occupied
isOccupied := false
// Count occupied ports before this one
occupiedBefore := 0
for j := 1; j < i; j++ {
if existingPA, exists := portAssignmentMap[j]; exists && existingPA.IsOccupied {
occupiedBefore++
}
}
// Count total ports with customers
customersCount := 0
for _, pa := range portAssignments {
if pa.CustomerName != nil && *pa.CustomerName != "" {
customersCount++
}
}
// This port should be occupied if we haven't reached port_used yet
if occupiedBefore + customersCount < device.DevicePort.PortUsed {
isOccupied = true
}
finalPortAssignments = append(finalPortAssignments, res.PortAssignmentResponse{
PortNumber: i,
CustomerName: nil,
IsOccupied: isOccupied,
})
}
}
// If port_used is 0, return empty port assignments
if device.DevicePort.PortUsed == 0 {
finalPortAssignments = []res.PortAssignmentResponse{}
} else {
// Get port assignments with details
portAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
// Convert to response format
finalPortAssignments = make([]res.PortAssignmentResponse, len(portAssignments))
for i, pa := range portAssignments {
finalPortAssignments[i] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
}
}
}
response := res.DeviceDetailsResponse{
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Address: address,
Longitude: device.Longitude,
Latitude: device.Latitude,
Status: string(device.Status),
PortAmount: device.PortAmount,
PortUsed: device.DevicePort.PortUsed,
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Address: address,
Longitude: device.Longitude,
Latitude: device.Latitude,
Status: string(device.Status),
PortAmount: device.PortAmount,
PortUsed: device.DevicePort.PortUsed,
PortAvailable: device.DevicePort.PortAvailable,
Region: device.Region,
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
Backbones: backboneInfos,
Fishbones: fishboneInfos,
Towers: towerInfos,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
CustomerNames: device.DevicePort.GetCustomerNamesOnly(), // Just customer names
PortAssignments: finalPortAssignments, // Use finalPortAssignments instead of resPortAssignments
Region: device.Region,
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
Backbones: backboneInfos,
Fishbones: fishboneInfos,
Towers: towerInfos,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
return response, nil