diff --git a/.gitignore b/.gitignore index 9f57f91..98b2774 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ node_modules/ .env # Coverage files -coverage.out \ No newline at end of file +coverage.out +uploads/towers \ No newline at end of file diff --git a/config/config.go b/config/config.go index 0ef42ce..14b022d 100644 --- a/config/config.go +++ b/config/config.go @@ -2,8 +2,10 @@ package config import ( "errors" - "github.com/joho/godotenv" "os" + "strconv" + + "github.com/joho/godotenv" ) type DbConfig struct { @@ -17,6 +19,7 @@ type LoginConfig struct { LoginAPI string AuthMeAPI string LogoutAPI string + SkipSSLVerification bool } type TokenConfig struct { @@ -67,10 +70,17 @@ func (c *Config) readConfig() error { TokenApiKey: os.Getenv("TOKEN_API_KEY"), } + skipSSL, err := strconv.ParseBool(os.Getenv("SKIP_SSL_VERIFICATION")) + if err != nil { + return errors.New("failed to read SKIP_SSL_VERIFICATION environment variable") + } + + c.LoginConfig = LoginConfig{ LoginAPI: os.Getenv("API_LOGIN_URL"), AuthMeAPI: os.Getenv("API_ME_URL"), LogoutAPI: os.Getenv("API_LOGOUT_URL"), + SkipSSLVerification: skipSSL, } if c.ApiConfig.ApiPort == "" { diff --git a/delivery/controller/activity_logs_controller.go b/delivery/controller/activity_logs_controller.go new file mode 100644 index 0000000..e99df34 --- /dev/null +++ b/delivery/controller/activity_logs_controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "net/http" + "strconv" + "users_management/m/middleware" + "users_management/m/usecase" + "users_management/m/utils/common" + "users_management/m/utils/helper" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type ActivityLogController struct { + activityLogUC usecase.ActivityLogUseCase + rg *gin.RouterGroup +} + +func NewActivityLogController(activityLogUC usecase.ActivityLogUseCase, rg *gin.RouterGroup) *ActivityLogController { + return &ActivityLogController{ + activityLogUC: activityLogUC, + rg: rg, + } +} + +func (c *ActivityLogController) Route() { + logs := c.rg.Group("/logs") + { + // Users can see their own logs (simplified response) + logs.GET("/my-logs", c.getMyLogs) + + // Only admins and superadmins can see all logs (detailed response) + logs.GET("/all", middleware.RequireAdminRole(), c.getAllLogs) + + // Admins and superadmins can see teknisi logs specifically (detailed response) + logs.GET("/teknisi", middleware.RequireAdminRole(), c.getTeknisinLogs) + } +} + +func (c *ActivityLogController) getMyLogs(ctx *gin.Context) { + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + uid, ok := userID.(uuid.UUID) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + logs, total, err := c.activityLogUC.GetUserLogs(uid, page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + // Convert to simplified response using helper + simplifiedLogs := helper.ConvertToActivityLogResponses(logs) + + response := gin.H{ + "logs": simplifiedLogs, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "User logs retrieved successfully", response) +} + +func (c *ActivityLogController) getAllLogs(ctx *gin.Context) { + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + logs, total, err := c.activityLogUC.GetAllLogs(page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + // Convert to detailed response for admins using helper + detailedLogs := helper.ConvertToActivityLogDetailResponses(logs) + + response := gin.H{ + "logs": detailedLogs, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "All logs retrieved successfully", response) +} + +func (c *ActivityLogController) getTeknisinLogs(ctx *gin.Context) { + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + logs, total, err := c.activityLogUC.GetTeknisinLogs(page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + // Convert to detailed response for admins using helper + detailedLogs := helper.ConvertToActivityLogDetailResponses(logs) + + response := gin.H{ + "logs": detailedLogs, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "Teknisi logs retrieved successfully", response) +} \ No newline at end of file diff --git a/delivery/controller/backbone_controller.go b/delivery/controller/backbone_controller.go index f0b7344..833ee1e 100644 --- a/delivery/controller/backbone_controller.go +++ b/delivery/controller/backbone_controller.go @@ -1,6 +1,7 @@ package controller import ( + "log" "net/http" "users_management/m/middleware" "users_management/m/model/dto/req" @@ -18,15 +19,13 @@ type BackboneController struct { func (bc *BackboneController) Route() { rg := bc.rg.Group("/backbone") - - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.CORSMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) { rg.GET("", bc.GetBackbone()) - rg.POST("", bc.CreateBackbone()) rg.GET("/:uuid", bc.GetBackboneByID()) - rg.PUT("/:uuid", bc.UpdateBackbone()) + + rg.POST("", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), bc.CreateBackbone()) + rg.PUT("/:uuid", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), bc.UpdateBackbone()) + } } @@ -56,6 +55,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc { err := c.ShouldBindJSON(&backboneDTO) if err != nil { + log.Println("Error binding JSON:", err) common.ErrorResponses(c, http.StatusBadRequest, err.Error()) return } @@ -63,6 +63,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc { err = bc.bu.CreateBackbone(backboneDTO) if err != nil { + log.Println("Error creating backbone:", err) common.ErrorResponses(c, http.StatusBadRequest, err.Error()) return } diff --git a/delivery/controller/count_assets_controller.go b/delivery/controller/count_assets_controller.go new file mode 100644 index 0000000..27d8bed --- /dev/null +++ b/delivery/controller/count_assets_controller.go @@ -0,0 +1,35 @@ +package controller + +import ( + "net/http" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" +) + +type CountAssetsController struct { + countAssetsUC usecase.CountAssetsUseCase + rg *gin.RouterGroup +} + +func NewCountAssetsController(countAssetsUC usecase.CountAssetsUseCase, rg *gin.RouterGroup) *CountAssetsController { + return &CountAssetsController{ + countAssetsUC: countAssetsUC, + rg: rg, + } +} + +func (c *CountAssetsController) Route() { + c.rg.GET("/count-assets", c.getCounts) +} + +func (c *CountAssetsController) getCounts(ctx *gin.Context) { + counts, err := c.countAssetsUC.GetCounts() + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "Count assets retrieved successfully", counts) +} \ No newline at end of file diff --git a/delivery/controller/devicePort_controller.go b/delivery/controller/devicePort_controller.go index 568c707..3e615a6 100644 --- a/delivery/controller/devicePort_controller.go +++ b/delivery/controller/devicePort_controller.go @@ -2,7 +2,6 @@ package controller import ( "net/http" - "users_management/m/middleware" "users_management/m/model/dto/req" "users_management/m/usecase" "users_management/m/utils/common" @@ -18,9 +17,6 @@ type DevicePortController struct { func (dc *DevicePortController) Route() { rg := dc.rg.Group("/device-port") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) { rg.GET("", dc.GetDevicePort()) rg.POST("", dc.CreateDevicePort()) diff --git a/delivery/controller/device_details.go b/delivery/controller/device_details.go new file mode 100644 index 0000000..9962cb5 --- /dev/null +++ b/delivery/controller/device_details.go @@ -0,0 +1,331 @@ +package controller + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "users_management/m/middleware" + "users_management/m/model/dto/req" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type DeviceDetailsController struct { + deviceDetailsUC usecase.DeviceDetailsUseCase + rg *gin.RouterGroup +} + +func NewDeviceDetailsController(deviceDetailsUC usecase.DeviceDetailsUseCase, rg *gin.RouterGroup) *DeviceDetailsController { + return &DeviceDetailsController{ + deviceDetailsUC: deviceDetailsUC, + rg: rg, + } +} + +func (c *DeviceDetailsController) Route() { + deviceDetails := c.rg.Group("/device-details") + deviceDetails.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) + { + deviceDetails.GET("", c.getAllDeviceDetails) + deviceDetails.POST("", c.createDeviceDetails) + deviceDetails.GET("/:id", c.getDeviceDetailsByID) + 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 + + deviceDetails.PUT("/:id/update-customer-by-port", c.updateCustomerByPort) // Updated endpoint name + deviceDetails.PUT("/:id/bulk-update-customers-by-port", c.bulkUpdateCustomersByPort) // Updated endpoint name + deviceDetails.DELETE("/:id/remove-customer-by-port", c.removeCustomerByPort) + + } +} + +func (c *DeviceDetailsController) updateCustomerByPort(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.UpdateCustomerByPortDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.deviceDetailsUC.UpdateCustomerByPort(deviceID, request) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + if request.NewCustomerName == nil { + common.SingleResponses(ctx, fmt.Sprintf("Customer removed from port %d successfully", request.PortNumber), nil) + } else { + common.SingleResponses(ctx, fmt.Sprintf("Customer assigned to port %d successfully", request.PortNumber), nil) + } +} + +func (c *DeviceDetailsController) bulkUpdateCustomersByPort(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.BulkUpdateCustomersByPortDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.deviceDetailsUC.BulkUpdateCustomersByPort(deviceID, request.Updates) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, fmt.Sprintf("%d port assignments updated successfully", len(request.Updates)), nil) +} + +func (c *DeviceDetailsController) removeCustomerByPort(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.RemoveCustomerByPortDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.deviceDetailsUC.RemoveCustomerByPort(deviceID, request.PortNumber) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, fmt.Sprintf("Customer removed from port %d successfully", request.PortNumber), nil) +} + +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 + } + + // Try to parse as array first + var arrayRequest []req.AssignMultipleCustomersDTO + + // Try to parse as single object + var singleRequest struct { + CustomerName string `json:"customer_name" binding:"required"` + PortNumber *int `json:"port_number,omitempty"` + } + + // Get raw JSON to determine the structure + rawData, err := ctx.GetRawData() + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid JSON data") + return + } + + // Reset the request body for subsequent parsing + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(rawData)) + + // Check if it's an array by looking at the first character + trimmed := bytes.TrimSpace(rawData) + if len(trimmed) > 0 && trimmed[0] == '[' { + // It's an array + if err := json.Unmarshal(rawData, &arrayRequest); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid array format: "+err.Error()) + return + } + + // Process array of assignments + err = c.deviceDetailsUC.AssignMultipleCustomersToPort(deviceID, arrayRequest) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, fmt.Sprintf("%d customers assigned successfully", len(arrayRequest)), nil) + } else { + // It's a single object + if err := json.Unmarshal(rawData, &singleRequest); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid object format: "+err.Error()) + return + } + + // Process single assignment + err = c.deviceDetailsUC.AssignCustomerToPort(deviceID, singleRequest.CustomerName, singleRequest.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 { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "Device details retrieved successfully", devices) +} + +func (c *DeviceDetailsController) createDeviceDetails(ctx *gin.Context) { + var request req.DeviceDetailsDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err := c.deviceDetailsUC.CreateDeviceDetails(request) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Device details created successfully", nil) +} + +func (c *DeviceDetailsController) getDeviceDetailsByID(ctx *gin.Context) { + id := ctx.Param("id") + deviceID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID") + return + } + + device, err := c.deviceDetailsUC.GetDeviceDetailsByID(deviceID) + if err != nil { + common.ErrorResponses(ctx, http.StatusNotFound, err.Error()) + return + } + + common.SingleResponses(ctx, "Device details retrieved successfully", device) +} + +func (c *DeviceDetailsController) updateDeviceDetails(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.UpdateDeviceDetailsDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.deviceDetailsUC.UpdateDeviceDetails(deviceID, request) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Device details updated successfully", nil) +} + +func (c *DeviceDetailsController) deleteDeviceDetails(ctx *gin.Context) { + id := ctx.Param("id") + deviceID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID") + return + } + + err = c.deviceDetailsUC.DeleteDeviceDetails(deviceID) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Device details deleted successfully", nil) +} + +func (c *DeviceDetailsController) recalculatePortUsage(ctx *gin.Context) { + id := ctx.Param("id") + deviceID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID") + return + } + + err = c.deviceDetailsUC.RecalculatePortUsage(deviceID) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + 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) +} \ No newline at end of file diff --git a/delivery/controller/device_inspections_controller.go b/delivery/controller/device_inspections_controller.go new file mode 100644 index 0000000..36a98eb --- /dev/null +++ b/delivery/controller/device_inspections_controller.go @@ -0,0 +1,255 @@ +package controller + +import ( + "net/http" + "strconv" + "users_management/m/middleware" + "users_management/m/model/dto/req" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type DeviceInspectionController struct { + inspectionUC usecase.DeviceInspectionUseCase + rg *gin.RouterGroup +} + +func NewDeviceInspectionController(inspectionUC usecase.DeviceInspectionUseCase, rg *gin.RouterGroup) *DeviceInspectionController { + return &DeviceInspectionController{ + inspectionUC: inspectionUC, + rg: rg, + } +} + +func (c *DeviceInspectionController) Route() { + inspections := c.rg.Group("/device-inspections") + { + // Teknisi can create and read their own inspections + inspections.POST("", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), c.createInspection) + inspections.GET("/my-inspections", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), c.getMyInspections) + + // Admin and Superadmin can see all inspections + inspections.GET("", middleware.RequireAdminRole(), c.getAllInspections) + inspections.GET("/:id", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), c.getInspectionByID) + + // Update inspections - teknisi can update their own, admin can update any + inspections.PUT("/:id", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), c.updateInspection) + + // Only admin can approve inspections + inspections.PATCH("/:id/approve", middleware.RequireAdminRole(), c.approveInspection) + } +} + +func (c *DeviceInspectionController) createInspection(ctx *gin.Context) { + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + uid, ok := userID.(uuid.UUID) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + var inspectionDTO req.DeviceInspectionDTO + if err := ctx.ShouldBindJSON(&inspectionDTO); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err := c.inspectionUC.CreateInspection(uid, inspectionDTO) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Device inspection created successfully", nil) +} + +func (c *DeviceInspectionController) getAllInspections(ctx *gin.Context) { + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + inspections, total, err := c.inspectionUC.GetAllInspections(page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + response := gin.H{ + "inspections": inspections, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "All inspections retrieved successfully", response) +} + +func (c *DeviceInspectionController) getMyInspections(ctx *gin.Context) { + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + uid, ok := userID.(uuid.UUID) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 10 + } + + inspections, total, err := c.inspectionUC.GetUserInspections(uid, page, limit) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + response := gin.H{ + "inspections": inspections, + "total": total, + "page": page, + "limit": limit, + "total_pages": (total + int64(limit) - 1) / int64(limit), + } + + common.SingleResponses(ctx, "User inspections retrieved successfully", response) +} + +func (c *DeviceInspectionController) getInspectionByID(ctx *gin.Context) { + id := ctx.Param("id") + inspectionID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid inspection ID") + return + } + + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + uid, ok := userID.(uuid.UUID) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + userRole, exists := ctx.Get("userRole") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User role not found") + return + } + + role, ok := userRole.(string) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user role") + return + } + + // Get the inspection first + inspection, err := c.inspectionUC.GetInspectionByID(inspectionID) + if err != nil { + common.ErrorResponses(ctx, http.StatusNotFound, err.Error()) + return + } + + // For teknisi, check if they own this inspection + if role == "Teknisi" { + isOwner, err := c.inspectionUC.CheckInspectionOwnership(inspectionID, uid) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, "Error checking inspection ownership") + return + } + if !isOwner { + common.ErrorResponses(ctx, http.StatusForbidden, "Unauthorized: you can only view your own inspections") + return + } + } + + common.SingleResponses(ctx, "Inspection retrieved successfully", inspection) +} +func (c *DeviceInspectionController) updateInspection(ctx *gin.Context) { + id := ctx.Param("id") + inspectionID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid inspection ID") + return + } + + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + uid, ok := userID.(uuid.UUID) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + userRole, _ := ctx.Get("userRole") + role, _ := userRole.(string) + + var inspectionDTO req.UpdateDeviceInspectionDTO + if err := ctx.ShouldBindJSON(&inspectionDTO); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.inspectionUC.UpdateInspection(inspectionID, uid, inspectionDTO, role) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Inspection updated successfully", nil) +} + +func (c *DeviceInspectionController) approveInspection(ctx *gin.Context) { + id := ctx.Param("id") + inspectionID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid inspection ID") + return + } + + var approvalDTO req.ApproveInspectionDTO + if err := ctx.ShouldBindJSON(&approvalDTO); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err = c.inspectionUC.ApproveInspection(inspectionID, approvalDTO) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(ctx, "Inspection approval updated successfully", nil) +} \ No newline at end of file diff --git a/delivery/controller/devices_controller.go b/delivery/controller/devices_controller.go index 4736c49..8d4860a 100644 --- a/delivery/controller/devices_controller.go +++ b/delivery/controller/devices_controller.go @@ -18,9 +18,7 @@ type DeviceController struct { func (dc *DeviceController) Route() { rg := dc.rg.Group("/devices") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) + rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) { rg.POST("", dc.CreateDevice()) rg.GET("", dc.GetAllDevices()) diff --git a/delivery/controller/fishbone_controller.go b/delivery/controller/fishbone_controller.go index 1bc0b44..5d6f732 100644 --- a/delivery/controller/fishbone_controller.go +++ b/delivery/controller/fishbone_controller.go @@ -1,6 +1,7 @@ package controller import ( + "log" "net/http" "users_management/m/middleware" "users_management/m/model/dto/req" @@ -18,14 +19,14 @@ type FishboneController struct { func (fc *FishboneController) Route() { rg := fc.rg.Group("/fishbone") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) + rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) + // Apply middleware to all routes { rg.GET("", fc.GetFishbone()) rg.POST("", fc.CreateFishbone()) rg.GET("/:uuid", fc.GetFishboneByID()) rg.PUT("/:uuid", fc.UpdateFishbone()) + rg.GET("/backbone/:backbone_id", fc.GetFishboneByBackboneID()) } } @@ -36,83 +37,151 @@ 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) { - fishbones, err := fc.fu.GetAllFishbone() + return func(c *gin.Context) { + log.Println("Fetching all fishbones") + fishbones, err := fc.fu.GetAllFishbone() - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - common.SingleResponses(c, "Success", fishbones) - } + common.SingleResponses(c, "Success", fishbones) + } } func (fc *FishboneController) CreateFishbone() gin.HandlerFunc { - return func(c *gin.Context) { - var fishboneDTO req.FishboneDTO - err := c.ShouldBindJSON(&fishboneDTO) + return func(c *gin.Context) { + var fishboneDTO req.FishboneDTO + err := c.ShouldBindJSON(&fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - err = fc.fu.CreateFishbone(fishboneDTO) + err = fc.fu.CreateFishbone(fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - common.SingleResponses(c, "Fishbone has been created", nil) - } + common.SingleResponses(c, "Fishbone has been created successfully", nil) + } } func (fc *FishboneController) GetFishboneByID() gin.HandlerFunc { - return func(c *gin.Context) { - id := c.Param("uuid") - uuid, err := uuid.Parse(id) - if err != nil{ - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } - fishbone, err := fc.fu.GetByID(uuid) + return func(c *gin.Context) { + id := c.Param("uuid") + fishboneUUID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format") + return + } - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + fishbone, err := fc.fu.GetByID(fishboneUUID) + if err != nil { + if err.Error() == "fishbone not found" { + common.ErrorResponses(c, http.StatusNotFound, err.Error()) + return + } + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + return + } - common.SingleResponses(c, "Success", fishbone) - } + common.SingleResponses(c, "Success", fishbone) + } } func (fc *FishboneController) UpdateFishbone() gin.HandlerFunc { - return func(c *gin.Context) { - id := c.Param("uuid") - uuid, err := uuid.Parse(id) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + return func(c *gin.Context) { + id := c.Param("uuid") + fishboneUUID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format") + return + } - var fishboneDTO req.UpdateFishboneDTO - err = c.ShouldBindJSON(&fishboneDTO) + var fishboneDTO req.UpdateFishboneDTO + err = c.ShouldBindJSON(&fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - err = fc.fu.UpdateFishbone(uuid, fishboneDTO) + err = fc.fu.UpdateFishbone(fishboneUUID, fishboneDTO) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + if err != nil { + if err.Error() == "fishbone not found" { + common.ErrorResponses(c, http.StatusNotFound, err.Error()) + return + } + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - common.SingleResponses(c, "Fishbone has been updated", nil) - } + common.SingleResponses(c, "Fishbone has been updated successfully", nil) + } +} + +func (fc *FishboneController) DeleteFishbone() gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("uuid") + fishboneUUID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format") + return + } + + err = fc.fu.DeleteFishbone(fishboneUUID) + if err != nil { + if err.Error() == "fishbone not found" { + common.ErrorResponses(c, http.StatusNotFound, err.Error()) + return + } + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(c, "Fishbone has been deleted successfully", nil) + } +} + +func (fc *FishboneController) GetFishboneStats() gin.HandlerFunc { + return func(c *gin.Context) { + stats, err := fc.fu.GetFishboneStats() + if err != nil { + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(c, "Success", stats) + } } \ No newline at end of file diff --git a/delivery/controller/nearest_device_controller.go b/delivery/controller/nearest_device_controller.go new file mode 100644 index 0000000..a5e2024 --- /dev/null +++ b/delivery/controller/nearest_device_controller.go @@ -0,0 +1,101 @@ +package controller + +import ( + "net/http" + "strconv" + "users_management/m/middleware" + "users_management/m/model/dto/req" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type NearestDeviceController struct { + nearestDeviceUC usecase.NearestDeviceUseCase + rg *gin.RouterGroup +} + +func NewNearestDeviceController(nearestDeviceUC usecase.NearestDeviceUseCase, rg *gin.RouterGroup) *NearestDeviceController { + return &NearestDeviceController{ + nearestDeviceUC: nearestDeviceUC, + rg: rg, + } +} + +func (c *NearestDeviceController) Route() { + nearestDevices := c.rg.Group("/nearest-devices") + nearestDevices.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) + { + nearestDevices.POST("/search", c.getNearestDevices) + nearestDevices.GET("/:id", c.getNearestDeviceByID) + } +} + +func (c *NearestDeviceController) getNearestDevices(ctx *gin.Context) { + var request req.NearestDeviceDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + devices, err := c.nearestDeviceUC.GetNearestDevices(request) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + response := gin.H{ + "devices": devices, + "total": len(devices), + "search_params": gin.H{ + "latitude": request.Latitude, + "longitude": request.Longitude, + "radius": request.Radius, + "province": request.Province, + "city": request.City, + "district": request.District, + }, + } + + common.SingleResponses(ctx, "Nearest devices retrieved successfully", response) +} + +func (c *NearestDeviceController) getNearestDeviceByID(ctx *gin.Context) { + id := ctx.Param("id") + deviceID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID") + return + } + + // Get user coordinates from query params + latStr := ctx.Query("lat") + lngStr := ctx.Query("lng") + + if latStr == "" || lngStr == "" { + common.ErrorResponses(ctx, http.StatusBadRequest, "User coordinates (lat, lng) are required") + return + } + + userLat, err := strconv.ParseFloat(latStr, 64) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude") + return + } + + userLng, err := strconv.ParseFloat(lngStr, 64) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude") + return + } + + device, err := c.nearestDeviceUC.GetNearestDeviceByID(deviceID, userLat, userLng) + if err != nil { + common.ErrorResponses(ctx, http.StatusNotFound, err.Error()) + return + } + + common.SingleResponses(ctx, "Device details retrieved successfully", device) +} \ No newline at end of file diff --git a/delivery/controller/tower_controller.go b/delivery/controller/tower_controller.go index c8b6332..b787093 100644 --- a/delivery/controller/tower_controller.go +++ b/delivery/controller/tower_controller.go @@ -2,6 +2,8 @@ package controller import ( "net/http" + "strconv" + "strings" "users_management/m/middleware" "users_management/m/model/dto/req" "users_management/m/usecase" @@ -18,12 +20,11 @@ type TowerController struct { func (tc *TowerController) Route() { rg := tc.rg.Group("/tower") - rg.Use(middleware.AuthMiddleware()) - rg.Use(middleware.RateLimitMiddleware()) - rg.Use(middleware.CORSMiddleware()) + rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) // Apply middleware to all routes { rg.GET("", tc.GetTower()) rg.POST("", tc.CreateTower()) + rg.GET("/:uuid", tc.GetTowerByID()) rg.PUT("/:uuid", tc.UpdateTower()) } @@ -50,24 +51,103 @@ func (tc *TowerController) GetTower() gin.HandlerFunc { } func (tc *TowerController) CreateTower() gin.HandlerFunc { - return func(c *gin.Context) { - var towerDTO req.TowerDTO - err := c.ShouldBindJSON(&towerDTO) + return func(c *gin.Context) { - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + 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 + } - err = tc.tu.Post(towerDTO) + // No image file for JSON requests + err := tc.tu.Post(towerDTO, nil) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } - 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 { + common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form") + return + } - common.SingleResponses(c, "Tower has been created", nil) - } + + + // Extract form data + deviceIDStr := c.PostForm("dev_id") + towerCode := c.PostForm("tower_code") + longitudeStr := c.PostForm("longitude") + latitudeStr := c.PostForm("latitude") + externalTowerStr := c.PostForm("external_tower") + + // Validate required fields + if towerCode == "" || longitudeStr == "" || latitudeStr == "" { + common.ErrorResponses(c, http.StatusBadRequest, "Missing required fields") + return + } + + var deviceID *uuid.UUID + if deviceIDStr != "" { + parsedDeviceID, err := uuid.Parse(deviceIDStr) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID") + return + } + deviceID = &parsedDeviceID + } + + // Parse coordinates + longitude, err := strconv.ParseFloat(longitudeStr, 64) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid longitude") + return + } + + latitude, err := strconv.ParseFloat(latitudeStr, 64) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid latitude") + return + } + + var externalTower *bool + if externalTowerStr != "" { + parsedExternalTower, err := strconv.ParseBool(externalTowerStr) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid external_tower value") + return + } + externalTower = &parsedExternalTower + } + + // Create DTO + towerDTO := req.TowerDTO{ + DeviceID: deviceID, + TowerCode: towerCode, + Longitude: longitude, + Latitude: latitude, + ExternalTower: externalTower, + } + + // Get image file (optional) + imageFile, _ := c.FormFile("image") + + err = tc.tu.Post(towerDTO, imageFile) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(c, "Tower has been created", nil) + } } func (tc *TowerController) GetTowerByID() gin.HandlerFunc { @@ -90,27 +170,102 @@ func (tc *TowerController) GetTowerByID() gin.HandlerFunc { } func (tc *TowerController) UpdateTower() gin.HandlerFunc { - return func(c *gin.Context) { - id := c.Param("uuid") - uuid, err := uuid.Parse(id) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - } - var towerUpdateDTO req.UpdateTowerDTO - err = c.ShouldBindJSON(&towerUpdateDTO) + 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 + } - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + 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 + } - err = tc.tu.UpdateTower(uuid, towerUpdateDTO) + // 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 + } - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, err.Error()) - return - } + common.SingleResponses(c, "Tower has been updated", nil) + return + } - common.SingleResponses(c, "Tower has been updated", nil) - } + // 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") + return + } + + // Create update DTO + towerUpdateDTO := req.UpdateTowerDTO{} + + // 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 + } + } + + if towerCode := c.PostForm("tower_code"); towerCode != "" { + towerUpdateDTO.TowerCode = &towerCode + } + + 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") + + err = tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, imageFile) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, err.Error()) + return + } + + common.SingleResponses(c, "Tower has been updated", nil) + } } \ No newline at end of file diff --git a/delivery/controller/user_management_controller.go b/delivery/controller/user_management_controller.go new file mode 100644 index 0000000..209915d --- /dev/null +++ b/delivery/controller/user_management_controller.go @@ -0,0 +1,89 @@ +package controller + +import ( + "net/http" + "users_management/m/middleware" + "users_management/m/usecase" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" +) + +type UserManagementController struct { + userUC usecase.UsersUsecase + rg *gin.RouterGroup +} + +func NewUserManagementController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *UserManagementController { + return &UserManagementController{ + userUC: userUC, + rg: rg, + } +} + +func (c *UserManagementController) Route() { + users := c.rg.Group("/user-management") + { + // Only superadmin can manage user roles + users.PUT("/role", middleware.RequireSuperAdminRole(), c.updateUserRole) + + // Admins and superadmins can view all users + users.GET("/users", middleware.RequireAdminRole(), c.getAllUsers) + + // Users can view their own profile + users.GET("/profile", c.getMyProfile) + } +} + +type UpdateRoleRequest struct { + NomorInduk string `json:"nomor_induk" binding:"required"` + RoleName string `json:"role_name" binding:"required"` +} + +func (c *UserManagementController) updateUserRole(ctx *gin.Context) { + var req UpdateRoleRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + err := c.userUC.UpdateUserRole(req.NomorInduk, req.RoleName) + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "User role updated successfully", nil) +} + +func (c *UserManagementController) getAllUsers(ctx *gin.Context) { + users, err := c.userUC.GetAllUsers() + if err != nil { + common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error()) + return + } + + common.SingleResponses(ctx, "Users retrieved successfully", users) +} + +func (c *UserManagementController) getMyProfile(ctx *gin.Context) { + userID, exists := ctx.Get("userID") + if !exists { + common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found") + return + } + + nomorInduk, ok := userID.(string) + if !ok { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID") + return + } + + user, err := c.userUC.GetUserByNomorInduk(nomorInduk) + if err != nil { + common.ErrorResponses(ctx, http.StatusNotFound, "User not found") + return + } + + common.SingleResponses(ctx, "User profile retrieved successfully", user) +} \ No newline at end of file diff --git a/delivery/server.go b/delivery/server.go index b6b0afa..129258d 100644 --- a/delivery/server.go +++ b/delivery/server.go @@ -42,18 +42,31 @@ func NewServer() *Server { } func (s *Server) setupController() { + + s.engine.Static("/uploads", "./uploads") + s.engine.Use(middleware.CORSMiddleware()) + + rg := s.engine.Group("/api/v1") controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route() - rg.Use(middleware.AuthMiddleware()) + rg.Use(middleware.AuthMiddleware(s.ucManager.NewUserUsecase())) + rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase())) { controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route() controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), rg).Route() controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), rg).Route() controller.NewTowerController(s.ucManager.NewTowerUsecase(), rg).Route() controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), rg).Route() + controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route() + controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), rg).Route() + controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), rg).Route() + controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), rg).Route() + controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), rg).Route() } } + + func (s *Server) Run() { s.setupController() if err := s.engine.Run(s.host); err != nil { diff --git a/go.mod b/go.mod index c9385cf..319d1d7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/go-playground/validator/v10 v10.24.0 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 + golang.org/x/crypto v0.33.0 + golang.org/x/time v0.10.0 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 ) @@ -15,7 +17,6 @@ require ( github.com/bytedance/sonic v1.12.8 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.0.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -39,12 +40,10 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.14.0 // indirect - golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.10.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b5f466a..ceb4f59 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= diff --git a/manager/infra_manager.go b/manager/infra_manager.go index 6ac151a..4b6b794 100644 --- a/manager/infra_manager.go +++ b/manager/infra_manager.go @@ -3,6 +3,8 @@ package manager import ( "fmt" "users_management/m/config" + "users_management/m/model/entity" + "gorm.io/driver/postgres" "gorm.io/gorm" @@ -24,10 +26,30 @@ func (im *infraManager) openConn() error { 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( + &entity.Role{}, + &entity.User{}, + &entity.Device{}, + &entity.Backbone{}, + &entity.Fishbone{}, + &entity.Tower{}, + &entity.DevicePort{}, + &entity.CountAssets{}, + &entity.ActivityLog{}, + ) +} + func NewInfraManager(cfg *config.Config) (InfraManager, error) { im := &infraManager{ cfg: cfg, diff --git a/manager/repository_manager.go b/manager/repository_manager.go index 283e91e..084fd2f 100644 --- a/manager/repository_manager.go +++ b/manager/repository_manager.go @@ -10,6 +10,13 @@ type RepositoryManager interface { NewTowerRepository() repository.TowerRepo NewDevicePortRepository() repository.DevicePortRepo + NewCountAssetsRepository() repository.CountAssetsRepo + NewActivityLogRepository() repository.ActivityLogRepo + NewDeviceInspectionRepository() repository.DeviceInspectionRepo + NewNearestDeviceRepository() repository.NearestDeviceRepo + + NewDeviceDetailsRepository() repository.DeviceDetailsRepo + } type repositoryManager struct { @@ -22,6 +29,13 @@ func NewRepositoryManager(infra InfraManager) RepositoryManager { } } +func (rm *repositoryManager) NewDeviceDetailsRepository() repository.DeviceDetailsRepo { + return repository.NewDeviceDetailsRepo(rm.infra.Conn()) +} +func (rm *repositoryManager) NewNearestDeviceRepository() repository.NearestDeviceRepo { + return repository.NewNearestDeviceRepo(rm.infra.Conn()) +} + func (rm *repositoryManager) NewUserRepository() repository.UsersRepo { return repository.NewUsersRepo(rm.infra.Conn()) } @@ -44,4 +58,16 @@ func (rm *repositoryManager) NewTowerRepository() repository.TowerRepo { func (rm *repositoryManager) NewDevicePortRepository() repository.DevicePortRepo { return repository.NewDevicePortRepo(rm.infra.Conn()) +} + +func (rm *repositoryManager) NewCountAssetsRepository() repository.CountAssetsRepo { + return repository.NewCountAssetsRepo(rm.infra.Conn()) +} + +func (rm *repositoryManager) NewActivityLogRepository() repository.ActivityLogRepo { + return repository.NewActivityLogRepo(rm.infra.Conn()) +} + +func (rm *repositoryManager) NewDeviceInspectionRepository() repository.DeviceInspectionRepo { + return repository.NewDeviceInspectionRepo(rm.infra.Conn()) } \ No newline at end of file diff --git a/manager/usecase_manager.go b/manager/usecase_manager.go index 62583c9..635ed77 100644 --- a/manager/usecase_manager.go +++ b/manager/usecase_manager.go @@ -16,6 +16,16 @@ type UsecaseManager interface { NewTowerUsecase() usecase.TowerUseCase NewDevicePortUsecase() usecase.DevicePortUseCase + NewCountAssetsUsecase() usecase.CountAssetsUseCase + + NewActivityLogUsecase() usecase.ActivityLogUseCase + + NewDeviceInspectionUsecase() usecase.DeviceInspectionUseCase + + NewNearestDeviceUsecase() usecase.NearestDeviceUseCase + + NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase + } type usecaseManager struct { @@ -30,6 +40,20 @@ func NewUsecaseManager(repo RepositoryManager, cfg *config.Config) UsecaseManage return &usecaseManager{repo: repo,cfg: cfg , geocoder: cachedGeocoder} } +func (um *usecaseManager) NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase { + return usecase.NewDeviceDetailsUseCase( + um.repo.NewDeviceDetailsRepository(), + um.geocoder, + ) +} + +func (um *usecaseManager) NewNearestDeviceUsecase() usecase.NearestDeviceUseCase { + return usecase.NewNearestDeviceUseCase( + um.repo.NewNearestDeviceRepository(), + um.geocoder, + ) +} + func (um *usecaseManager) NewUserUsecase() usecase.UsersUsecase { return usecase.NewUsersUsecase(um.repo.NewUserRepository()) } @@ -43,11 +67,20 @@ func (um *usecaseManager) NewDeviceUsecase() usecase.DeviceUseCase { } func (um *usecaseManager) NewBackboneUsecase() usecase.BackboneUseCase { - return usecase.NewBackboneUseCase(um.repo.NewBackboneRepository(),um.repo.NewFishboneRepository()) + return usecase.NewBackboneUseCase( + um.repo.NewBackboneRepository(), + um.repo.NewFishboneRepository(), + um.repo.NewDeviceDetailsRepository(), // Add this + ) } + func (um *usecaseManager) NewFishboneUsecase() usecase.FishboneUseCase { - return usecase.NewFishboneUseCase(um.repo.NewFishboneRepository()) + return usecase.NewFishboneUseCase( + um.repo.NewFishboneRepository(), + um.repo.NewBackboneRepository(), + um.repo.NewDeviceDetailsRepository(), // Add this line + ) } func (um *usecaseManager) NewTowerUsecase() usecase.TowerUseCase { @@ -56,4 +89,21 @@ func (um *usecaseManager) NewTowerUsecase() usecase.TowerUseCase { func (um *usecaseManager) NewDevicePortUsecase() usecase.DevicePortUseCase { return usecase.NewDevicePortUseCase(um.repo.NewDevicePortRepository()) +} + + +func (um *usecaseManager) NewCountAssetsUsecase() usecase.CountAssetsUseCase { + return usecase.NewCountAssetsUseCase(um.repo.NewCountAssetsRepository()) +} + +func (um *usecaseManager) NewActivityLogUsecase() usecase.ActivityLogUseCase { + return usecase.NewActivityLogUseCase(um.repo.NewActivityLogRepository()) +} + +func (um *usecaseManager) NewDeviceInspectionUsecase() usecase.DeviceInspectionUseCase { + return usecase.NewDeviceInspectionUseCase( + um.repo.NewDeviceInspectionRepository(), + um.NewActivityLogUsecase(), + service.NewGeocodingService(), // Add the geocoding service + ) } \ No newline at end of file diff --git a/middleware/activity_logs_middleware.go b/middleware/activity_logs_middleware.go new file mode 100644 index 0000000..2531ed9 --- /dev/null +++ b/middleware/activity_logs_middleware.go @@ -0,0 +1,141 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "users_management/m/usecase" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type responseWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (r responseWriter) Write(b []byte) (int, error) { + r.body.Write(b) + return r.ResponseWriter.Write(b) +} + +func ActivityLoggingMiddleware(activityLogUC usecase.ActivityLogUseCase) gin.HandlerFunc { + return func(c *gin.Context) { + // Skip logging for certain endpoints + if shouldSkipLogging(c.Request.URL.Path) { + c.Next() + return + } + + // Get user info from context (set by auth middleware) + userID, userExists := c.Get("userID") + if !userExists { + c.Next() + return + } + + // Convert userID to UUID + uid, ok := userID.(uuid.UUID) + if !ok { + c.Next() + return + } + + // Read request body + var requestBody []byte + if c.Request.Body != nil { + requestBody, _ = io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + } + + // Wrap response writer to capture response + w := &responseWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer} + c.Writer = w + + // Process request + c.Next() + + // Log the activity in background + go func() { + action := getActionFromMethod(c.Request.Method) + resource := getResourceFromPath(c.Request.URL.Path) + resourceID := getResourceIDFromPath(c.Request.URL.Path) + + var oldData, newData interface{} + + // For updates, you might want to fetch old data before the operation + // This is a simplified version + if len(requestBody) > 0 { + json.Unmarshal(requestBody, &newData) + } + + details := "" + if c.Writer.Status() >= 400 { + details = fmt.Sprintf("Request failed with status: %d", c.Writer.Status()) + } + + activityLogUC.LogActivity( + uid, + action, + resource, + resourceID, + oldData, + newData, + c.ClientIP(), + c.Request.UserAgent(), + details, + ) + }() + } +} + +func shouldSkipLogging(path string) bool { + skipPaths := []string{ + "/api/v1/logs", + "/api/v1/users/login", + "/api/v1/users/logout", + "/uploads", + } + + for _, skipPath := range skipPaths { + if strings.HasPrefix(path, skipPath) { + return true + } + } + return false +} + +func getActionFromMethod(method string) string { + switch method { + case "POST": + return "CREATE" + case "PUT", "PATCH": + return "UPDATE" + case "DELETE": + return "DELETE" + case "GET": + return "READ" + default: + return method + } +} + +func getResourceFromPath(path string) string { + parts := strings.Split(path, "/") + if len(parts) >= 4 { + return parts[3] // /api/v1/devices -> devices + } + return "unknown" +} + +func getResourceIDFromPath(path string) *string { + parts := strings.Split(path, "/") + if len(parts) >= 5 { + id := parts[4] + return &id + } + return nil +} \ No newline at end of file diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index beaa0dd..f22032c 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -1,67 +1,96 @@ package middleware import ( + "crypto/tls" "encoding/json" + "log" "net/http" "strings" + "time" "users_management/m/model/dto/res" + "users_management/m/model/entity" + "users_management/m/usecase" "users_management/m/utils/common" "github.com/gin-gonic/gin" ) -func AuthMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - token := c.GetHeader("Authorization") +func AuthMiddleware(userUC usecase.UsersUsecase) gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") - if token == "" { - common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required") - c.Abort() - return - } + if token == "" { + common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required") + c.Abort() + return + } - token = strings.TrimPrefix(token, "Bearer ") + token = strings.TrimPrefix(token, "Bearer ") - c.Set("token", token) + c.Set("token", token) - req, err := http.NewRequest("POST", "https://demo.api-hrm.winteraccess.id/api/v2/auth/me", nil) + req, err := http.NewRequest("POST", "https://demo.api-hrm.winteraccess.id/api/v2/auth/me", nil) - if err != nil { - common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) - c.Abort() - return - } - - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Accept", "application/json") - - client := &http.Client{} - - resp, err := client.Do(req) - if err != nil { - common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) - c.Abort() - return - } - - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - common.ErrorResponses(c, http.StatusUnauthorized, "Unauthroized") - c.Abort() - return - } - - var authResponse res.AuthMeResponse - if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil { + if err != nil { common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) c.Abort() return } - c.Set("userID", authResponse.Data.NomorInduk) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/json") + - c.Next() + var client *http.Client + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client = &http.Client{ + Transport: tr, + Timeout: 30 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + c.Abort() + return + } - } -} + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + common.ErrorResponses(c, http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + + var authResponse res.AuthMeResponse + if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil { + common.ErrorResponses(c, http.StatusInternalServerError, err.Error()) + c.Abort() + return + } + + // Set basic user info from external API + var user entity.User + + + // Check if user exists in local database + user, err = userUC.GetUserByUsername(strings.ToLower(authResponse.Data.Username)) + c.Set("userID", user.ID) + c.Set("userName", user.Username) + log.Println("User data from local DB:", user.ID) + if err != nil { + + defaultRole := "Teknisi" + c.Set("userRole", defaultRole) + } else { + // User exists in local DB, use their assigned role + c.Set("userRole", user.Role.Name) + } + + c.Next() + } +} \ No newline at end of file diff --git a/middleware/cors_middleware.go b/middleware/cors_middleware.go index a1ba6dc..9995708 100644 --- a/middleware/cors_middleware.go +++ b/middleware/cors_middleware.go @@ -1,23 +1,61 @@ package middleware import ( - "net/http" - - "github.com/gin-gonic/gin" + "net/http" + "strings" + "github.com/gin-gonic/gin" ) func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // Change to specific domains if needed - c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + 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", // Add production URL + } + + // 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 + } - // Allow OPTIONS method to pass through - if c.Request.Method == http.MethodOptions { - c.AbortWithStatus(http.StatusNoContent) - return - } - - c.Next() - } + c.Next() + } } \ No newline at end of file diff --git a/middleware/rate_limitter.go b/middleware/rate_limitter.go index 880a306..21219d5 100644 --- a/middleware/rate_limitter.go +++ b/middleware/rate_limitter.go @@ -1,18 +1,20 @@ package middleware import ( - "net/http" - "sync" - "time" - "users_management/m/utils/common" + "log" + "net/http" + "sync" + "time" + "users_management/m/utils/common" - "github.com/gin-gonic/gin" - "golang.org/x/time/rate" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "golang.org/x/time/rate" ) var ( - rateLimiters = make(map[string]*rate.Limiter) - mu sync.Mutex + rateLimiters = make(map[string]*rate.Limiter) + mu sync.Mutex ) func getRateLimiter(userID string) *rate.Limiter { @@ -21,7 +23,7 @@ func getRateLimiter(userID string) *rate.Limiter { limiter, exists := rateLimiters[userID] if !exists { - limiter = rate.NewLimiter(rate.Every(1*time.Minute), 50) // 1 request per second with a burst of 2 requests + limiter = rate.NewLimiter(rate.Every(1*time.Minute), 50) // 50 requests per minute rateLimiters[userID] = limiter } @@ -29,32 +31,49 @@ func getRateLimiter(userID string) *rate.Limiter { } func getLoginLimiter() *rate.Limiter { - mu.Lock() - defer mu.Unlock() + mu.Lock() + defer mu.Unlock() - limiter, exists := rateLimiters["login"] - if !exists { - limiter = rate.NewLimiter(rate.Every(1*time.Minute), 10) // 5 request per second with a burst of 10 requests - rateLimiters["login"] = limiter - } + limiter, exists := rateLimiters["login"] + if !exists { + limiter = rate.NewLimiter(rate.Every(1*time.Minute), 10) // 10 requests per minute for login + rateLimiters["login"] = limiter + } - return limiter + return limiter } func RateLimitMiddleware() gin.HandlerFunc{ - return func(c *gin.Context) { - if c.Request.Method == http.MethodOptions { - c.Next() - return - } - userID, exists := c.Get("userID") + return func(c *gin.Context) { + if c.Request.Method == http.MethodOptions { + c.Next() + return + } + + userID, exists := c.Get("userID") if !exists { common.ErrorResponses(c, http.StatusUnauthorized, "Unauthorized: No user ID found") c.Abort() return } - limiter := getRateLimiter(userID.(string)) + // Convert UUID to string for rate limiting + var userIDStr string + switch v := userID.(type) { + case uuid.UUID: + userIDStr = v.String() + case string: + userIDStr = v + default: + log.Printf("Unexpected userID type: %T", userID) + common.ErrorResponses(c, http.StatusInternalServerError, "Invalid user ID type") + c.Abort() + return + } + + log.Printf("User ID for rate limiting: %s", userIDStr) + + limiter := getRateLimiter(userIDStr) if !limiter.Allow() { common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests") @@ -62,24 +81,23 @@ func RateLimitMiddleware() gin.HandlerFunc{ return } - c.Next() - } + c.Next() + } } func RateLoginMiddleware() gin.HandlerFunc{ - return func(c *gin.Context) { - if c.Request.Method == http.MethodOptions { - c.Next() - return - } - limiter := getLoginLimiter() - - if !limiter.Allow() { - common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests") - c.Abort() - return - } - c.Next() - } -} + return func(c *gin.Context) { + if c.Request.Method == http.MethodOptions { + c.Next() + return + } + limiter := getLoginLimiter() + if !limiter.Allow() { + common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests") + c.Abort() + return + } + c.Next() + } +} \ No newline at end of file diff --git a/middleware/rbac.go b/middleware/rbac.go new file mode 100644 index 0000000..48cfc46 --- /dev/null +++ b/middleware/rbac.go @@ -0,0 +1,161 @@ +package middleware + +import ( + "net/http" + "users_management/m/utils/common" + + "github.com/gin-gonic/gin" +) + +// RequireRole middleware to check if user has required role +func RequireRole(allowedRoles ...string) gin.HandlerFunc { + return func(c *gin.Context) { + userRole, exists := c.Get("userRole") + if !exists { + common.ErrorResponses(c, http.StatusUnauthorized, "User role not found") + c.Abort() + return + } + + role, ok := userRole.(string) + if !ok { + common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role") + c.Abort() + return + } + + // Check if user role is in allowed roles + for _, allowedRole := range allowedRoles { + if role == allowedRole { + c.Next() + return + } + } + + common.ErrorResponses(c, http.StatusForbidden, "Insufficient permissions") + c.Abort() + } +} + +// RequireAnyRole middleware - user needs at least one of the specified roles +func RequireAnyRole(roles ...string) gin.HandlerFunc { + return RequireRole(roles...) +} + +// RequireAdminRole middleware for admin-only access +func RequireAdminRole() gin.HandlerFunc { + return RequireRole("Admin", "Superadmin") +} + +// RequireSuperAdminRole middleware for superadmin-only access +func RequireSuperAdminRole() gin.HandlerFunc { + return RequireRole("Superadmin") +} + +// RequireTeknisiRole middleware for teknisi access +func RequireTeknisiRole() gin.HandlerFunc { + return RequireRole("Teknisi") +} + +// RequireNonTeknisiRole middleware to block teknisi users +func RequireNonTeknisiRole() gin.HandlerFunc { + return func(c *gin.Context) { + userRole, exists := c.Get("userRole") + if !exists { + common.ErrorResponses(c, http.StatusUnauthorized, "User role not found") + c.Abort() + return + } + + role, ok := userRole.(string) + if !ok { + common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role") + c.Abort() + return + } + + if role == "Teknisi" { + common.ErrorResponses(c, http.StatusForbidden, "Teknisi users are not allowed to access this resource") + c.Abort() + return + } + + c.Next() + } +} + +// PermissionMiddleware for more granular permissions +func PermissionMiddleware(resource, action string) gin.HandlerFunc { + return func(c *gin.Context) { + userRole, exists := c.Get("userRole") + if !exists { + common.ErrorResponses(c, http.StatusUnauthorized, "User role not found") + c.Abort() + return + } + + role, ok := userRole.(string) + if !ok { + common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role") + c.Abort() + return + } + + if !hasPermission(role, resource, action) { + common.ErrorResponses(c, http.StatusForbidden, "Insufficient permissions for this action") + c.Abort() + return + } + + c.Next() + } +} + +// Permission checker function +func hasPermission(role, resource, action string) bool { + permissions := getPermissions() + + rolePermissions, exists := permissions[role] + if !exists { + return false + } + + resourcePermissions, exists := rolePermissions[resource] + if !exists { + return false + } + + for _, allowedAction := range resourcePermissions { + if allowedAction == action || allowedAction == "*" { + return true + } + } + + return false +} + +// Define permissions for each role +func getPermissions() map[string]map[string][]string { + return map[string]map[string][]string{ + "Superadmin": { + "*": {"*"}, // Full access to everything + }, + "Admin": { + "backbone": {"CREATE", "READ", "UPDATE", "DELETE"}, + "fishbone": {"CREATE", "READ", "UPDATE", "DELETE"}, + "devices": {"CREATE", "READ", "UPDATE", "DELETE"}, + "towers": {"CREATE", "READ", "UPDATE", "DELETE"}, + "ports": {"CREATE", "READ", "UPDATE", "DELETE"}, + "logs": {"READ"}, + "users": {"READ"}, + }, + "Teknisi": { + "backbone": {"CREATE", "READ", "UPDATE"}, + "fishbone": {"CREATE", "READ", "UPDATE"}, + "devices": {"CREATE", "READ", "UPDATE"}, + "towers": {"CREATE", "READ", "UPDATE"}, + "ports": {"CREATE", "READ", "UPDATE"}, + "logs": {"READ"}, // Only own logs + }, + } +} \ No newline at end of file diff --git a/model/dto/req/backbone_dto.go b/model/dto/req/backbone_dto.go index c293f41..e88d3f0 100644 --- a/model/dto/req/backbone_dto.go +++ b/model/dto/req/backbone_dto.go @@ -3,6 +3,7 @@ package req import "github.com/google/uuid" type BackboneDTO struct { + BackboneCode string `json:"backbone_code" validate:"required,min=3"` DeviceStartID uuid.UUID `json:"dev_start_id"` DeviceEndID uuid.UUID `json:"dev_end_id"` CoreAmount int `json:"core_amount"` diff --git a/model/dto/req/deviceDetails.go b/model/dto/req/deviceDetails.go new file mode 100644 index 0000000..ee30eec --- /dev/null +++ b/model/dto/req/deviceDetails.go @@ -0,0 +1,46 @@ +package req + + +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"` +} + +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"` +} + +type AssignMultipleCustomersDTO struct { + CustomerName string `json:"customer_name" binding:"required"` + PortNumber *int `json:"port_number,omitempty"` +} + +type UpdateCustomerByPortDTO struct { + PortNumber int `json:"port_number" binding:"required,min=1"` + NewCustomerName *string `json:"new_customer_name,omitempty"` // null to remove, string to update/assign +} + +type BulkUpdateCustomersByPortDTO struct { + Updates []UpdateCustomerByPortDTO `json:"updates" binding:"required"` +} + +type RemoveCustomerByPortDTO struct { + PortNumber int `json:"port_number" binding:"required,min=1"` +} \ No newline at end of file diff --git a/model/dto/req/devicePort_dto.go b/model/dto/req/devicePort_dto.go index b4c0ec5..d13801f 100644 --- a/model/dto/req/devicePort_dto.go +++ b/model/dto/req/devicePort_dto.go @@ -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"` } \ No newline at end of file diff --git a/model/dto/req/device_dto.go b/model/dto/req/device_dto.go index 1259c07..cc35165 100644 --- a/model/dto/req/device_dto.go +++ b/model/dto/req/device_dto.go @@ -1,20 +1,25 @@ package req - type DeviceDTO struct { - DeviceCode string `json:"device_code" validate:"required"` - DeviceType string `json:"device_type" validate:"required"` - Longitude float64 `json:"longitude" validate:"required"` - Latitude float64 `json:"latitude" validate:"required"` - PortAmount int `json:"port_amount" validate:"required"` - Status string `json:"status" validate:"required,oneof=active inactive maintenance"` + DeviceCode string `json:"device_code" validate:"required"` + DeviceType string `json:"device_type" validate:"required"` + Longitude float64 `json:"longitude" validate:"required"` + Latitude float64 `json:"latitude" validate:"required"` + PortAmount int `json:"port_amount"` + Status string `json:"status" validate:"required,oneof=active inactive maintenance"` + 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"` } type UpdateDeviceDTO struct { - DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"` - DeviceType *string `json:"device_type,omitempty" validate:"omitempty,min=3"` - 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=1"` - Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"` + DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"` + DeviceType *string `json:"device_type,omitempty" validate:"omitempty,min=3"` + Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` + Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` + PortAmount *int `json:"port_amount,omitempty" validate:"omitempty"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"` + 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"` } \ No newline at end of file diff --git a/model/dto/req/device_inspections_dto.go b/model/dto/req/device_inspections_dto.go new file mode 100644 index 0000000..8e5de21 --- /dev/null +++ b/model/dto/req/device_inspections_dto.go @@ -0,0 +1,38 @@ +package req + +import "github.com/google/uuid" + +type DeviceInspectionDTO struct { + DeviceID uuid.UUID `json:"device_id" validate:"required"` + BackboneID *uuid.UUID `json:"backbone_id,omitempty"` + FishboneID *uuid.UUID `json:"fishbone_id,omitempty"` + TowerID *uuid.UUID `json:"tower_id,omitempty"` + Status string `json:"status" validate:"required,oneof=pending in_progress completed maintenance"` + PortUsed string `json:"port_used" validate:"required"` + PortAvailable string `json:"port_available"` + Description string `json:"description"` + ImageURL *string `json:"image_url,omitempty"` + InspectionPlacement struct { + Longitude float64 `json:"longitude" validate:"required"` + Latitude float64 `json:"latitude" validate:"required"` + } `json:"inspection_placement" validate:"required"` +} + +type UpdateDeviceInspectionDTO struct { + BackboneID *uuid.UUID `json:"backbone_id,omitempty"` + FishboneID *uuid.UUID `json:"fishbone_id,omitempty"` + TowerID *uuid.UUID `json:"tower_id,omitempty"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending in_progress completed maintenance"` + PortUsed *string `json:"port_used,omitempty"` + Description *string `json:"description,omitempty"` + ImageURL *string `json:"image_url,omitempty"` + InspectionPlacement *struct { + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + } `json:"inspection_placement,omitempty"` +} + +type ApproveInspectionDTO struct { + InspectionApproval string `json:"inspection_approval" validate:"required,oneof=approved rejected"` + Notes string `json:"notes,omitempty"` +} \ No newline at end of file diff --git a/model/dto/req/fishbone_dto.go b/model/dto/req/fishbone_dto.go index 0c64f75..1a113dc 100644 --- a/model/dto/req/fishbone_dto.go +++ b/model/dto/req/fishbone_dto.go @@ -3,15 +3,17 @@ package req import "github.com/google/uuid" type FishboneDTO struct { - BackboneID uuid.UUID `json:"bb_id"` - DeviceStartID uuid.UUID `json:"dev_start_id"` - DeviceEndID uuid.UUID `json:"dev_end_id"` - CoreAmount int `json:"core_amount"` + FishboneCode string `json:"fishbone_code" validate:"required,min=3"` + BackboneID uuid.UUID `json:"bb_id" validate:"required"` + DeviceStartID uuid.UUID `json:"dev_start_id" validate:"required"` + DeviceEndID uuid.UUID `json:"dev_end_id" validate:"required"` + CoreAmount int `json:"core_amount" validate:"required,min=1"` } type UpdateFishboneDTO struct { - BackboneID *uuid.UUID `json:"bb_id,omitempty" validate:"omitempty,min=3"` - DeviceStartID *uuid.UUID `json:"dev_start_id,omitempty" validate:"omitempty,min=3"` - DeviceEndID *uuid.UUID `json:"dev_end_id,omitempty" validate:"omitempty,min=3"` - CoreAmount *int `json:"core_amount,omitempty" validate:"omitempty,min=1"` + FishboneCode *string `json:"fishbone_code,omitempty" validate:"omitempty,min=3"` + BackboneID *uuid.UUID `json:"bb_id,omitempty"` + DeviceStartID *uuid.UUID `json:"dev_start_id,omitempty"` + DeviceEndID *uuid.UUID `json:"dev_end_id,omitempty"` + CoreAmount *int `json:"core_amount,omitempty" validate:"omitempty,min=1"` } \ No newline at end of file diff --git a/model/dto/req/nearest_devices.go b/model/dto/req/nearest_devices.go new file mode 100644 index 0000000..1313e82 --- /dev/null +++ b/model/dto/req/nearest_devices.go @@ -0,0 +1,11 @@ +package req + +type NearestDeviceDTO struct { + Longitude float64 `json:"longitude" validate:"required"` + Latitude float64 `json:"latitude" validate:"required"` + Radius float64 `json:"radius" validate:"omitempty,min=0.1,max=50"` // Default 5km, max 50km + Limit int `json:"limit" validate:"omitempty,min=1,max=100"` // Default 10, max 100 + Province *string `json:"province,omitempty"` + City *string `json:"city,omitempty"` + District *string `json:"district,omitempty"` +} \ No newline at end of file diff --git a/model/dto/req/tower_dto.go b/model/dto/req/tower_dto.go index a91ddc2..8dab41c 100644 --- a/model/dto/req/tower_dto.go +++ b/model/dto/req/tower_dto.go @@ -3,15 +3,20 @@ package req import "github.com/google/uuid" type TowerDTO struct { - DeviceID uuid.UUID `json:"dev_id"` - TowerCode string `json:"tower_code"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` + DeviceID *uuid.UUID `json:"dev_id,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + TowerCode string `json:"tower_code" validate:"required"` + Longitude float64 `json:"longitude" validate:"required"` + Latitude float64 `json:"latitude" validate:"required"` + ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable } type UpdateTowerDTO struct { - DeviceID *string `json:"device_id,omitempty" validate:"omitempty,min=3"` - TowerCode *string `json:"tower_code,omitempty" validate:"omitempty"` - Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` - Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` + DeviceID *uuid.UUID `json:"device_id,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + TowerCode *string `json:"tower_code,omitempty"` + Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` + Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` + ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable + ImageURL *string `json:"image_url,omitempty"` } \ No newline at end of file diff --git a/model/dto/res/activity_logs_res.go b/model/dto/res/activity_logs_res.go new file mode 100644 index 0000000..08f3993 --- /dev/null +++ b/model/dto/res/activity_logs_res.go @@ -0,0 +1,34 @@ +package res + +import ( + "time" + "github.com/google/uuid" +) + +// ActivityLogResponse - simplified response for users +type ActivityLogResponse struct { + ID uuid.UUID `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + Action string `json:"action"` + Resource string `json:"resource"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Timestamp time.Time `json:"timestamp"` +} + +// ActivityLogDetailResponse - detailed response for admins (if needed) +type ActivityLogDetailResponse struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + Action string `json:"action"` + Resource string `json:"resource"` + ResourceID *string `json:"resource_id,omitempty"` + OldData *string `json:"old_data,omitempty"` + NewData *string `json:"new_data,omitempty"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Timestamp time.Time `json:"timestamp"` +} \ No newline at end of file diff --git a/model/dto/res/backbone_res.go b/model/dto/res/backbone_res.go index f239768..f4e7fa5 100644 --- a/model/dto/res/backbone_res.go +++ b/model/dto/res/backbone_res.go @@ -8,6 +8,7 @@ import ( type BackboneResponse struct { ID uuid.UUID `json:"id"` + BackboneCode string `json:"backbone_code"` DevStart string `json:"dev_start"` DevEnd string `json:"dev_end"` CoreAmount int `json:"core_amount"` diff --git a/model/dto/res/device_details.go b/model/dto/res/device_details.go new file mode 100644 index 0000000..12de463 --- /dev/null +++ b/model/dto/res/device_details.go @@ -0,0 +1,48 @@ +package res + +import ( + "time" + "github.com/google/uuid" +) + +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"` + 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"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TowerConnectionDetail struct { + ID uuid.UUID `json:"id"` + TowerCode string `json:"tower_code"` + 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"` +} \ No newline at end of file diff --git a/model/dto/res/device_inspections_res.go b/model/dto/res/device_inspections_res.go new file mode 100644 index 0000000..c7aced3 --- /dev/null +++ b/model/dto/res/device_inspections_res.go @@ -0,0 +1,72 @@ +package res + +import ( + "time" + "github.com/google/uuid" +) + +type DeviceInspectionResponse struct { + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType string `json:"device_type"` + UserName string `json:"user_name"` + BackboneCode *string `json:"backbone_code,omitempty"` + FishboneCode *string `json:"fishbone_code,omitempty"` + TowerCode *string `json:"tower_code,omitempty"` + Status string `json:"status"` + PortUsed string `json:"port_used"` + PortAvailable string `json:"port_available"` + CableAmount int `json:"cable_amount"` + Description string `json:"description"` + ImageURL *string `json:"image_url,omitempty"` + InspectionPlacement string `json:"inspection_placement"` // Full address + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + InspectionApproval string `json:"inspection_approval"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type DeviceInspectionDetailResponse struct { + ID uuid.UUID `json:"id"` + Device DeviceInfo `json:"device"` + User UserInfo `json:"user"` + Backbone *BackboneInfo `json:"backbone,omitempty"` + Fishbone *FishboneInfo `json:"fishbone,omitempty"` + Tower *TowerInfo `json:"tower,omitempty"` + Status string `json:"status"` + PortUsed string `json:"port_used"` + PortAvailable string `json:"port_available"` + CableAmount int `json:"cable_amount"` + Description string `json:"description"` + ImageURL *string `json:"image_url,omitempty"` + InspectionPlacement string `json:"inspection_placement"` // Full address + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + InspectionApproval string `json:"inspection_approval"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + + +type DeviceInfo struct { + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType string `json:"device_type"` +} + +type UserInfo struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Username string `json:"username"` +} + +type TowerInfo struct { + ID uuid.UUID `json:"id"` + TowerCode string `json:"tower_code"` +} + +type FishboneInfo struct { + ID uuid.UUID `json:"id"` + FishboneCode string `json:"fishbone_code"` +} \ No newline at end of file diff --git a/model/dto/res/device_res.go b/model/dto/res/device_res.go index 4ff9af5..d7ff642 100644 --- a/model/dto/res/device_res.go +++ b/model/dto/res/device_res.go @@ -7,14 +7,18 @@ import ( ) type DeviceResponse struct { - ID uuid.UUID `json:"id"` - DeviceCode string `json:"device_code"` - DeviceType string `json:"device_type"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - Address string `json:"address"` - PortAmount int `json:"port_amount"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} \ No newline at end of file + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType string `json:"device_type"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + 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 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/model/dto/res/fishbone_res.go b/model/dto/res/fishbone_res.go index ac39d51..fa96a6f 100644 --- a/model/dto/res/fishbone_res.go +++ b/model/dto/res/fishbone_res.go @@ -1,17 +1,32 @@ package res import ( - "time" + "time" - "github.com/google/uuid" + "github.com/google/uuid" ) type FishboneResponse struct { - ID uuid.UUID `json:"id"` - DeviceName string `json:"device_name"` - TowerCode string `json:"tower_code"` - Longtitude float64 `json:"longtitude"` - Latitude float64 `json:"latitude"` - Address string `json:"address"` - CreatedAt time.Time `json:"created_at"` -} \ No newline at end of file + ID uuid.UUID `json:"id"` + FishboneCode string `json:"fishbone_code"` + BackboneCode string `json:"backbone_code"` + DeviceStart string `json:"device_start"` + DeviceEnd string `json:"device_end"` + CoreAmount int `json:"core_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type FishboneDetailResponse struct { + ID uuid.UUID `json:"id"` + FishboneCode string `json:"fishbone_code"` + Backbone BackboneInfo `json:"backbone"` + CoreAmount int `json:"core_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type BackboneInfo struct { + ID uuid.UUID `json:"id"` + BackboneCode string `json:"backbone_code"` +} diff --git a/model/dto/res/nearest_devices.go b/model/dto/res/nearest_devices.go new file mode 100644 index 0000000..7302b5c --- /dev/null +++ b/model/dto/res/nearest_devices.go @@ -0,0 +1,80 @@ +package res + +import ( + "time" + "github.com/google/uuid" +) + +// Simplified response for list +type NearestDeviceResponse struct { + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType string `json:"device_type"` + Distance float64 `json:"distance_km"` + Address string `json:"address"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Status string `json:"status"` + PortAmount int `json:"port_amount"` + Region *string `json:"region,omitempty"` + Province *string `json:"province,omitempty"` + City *string `json:"city,omitempty"` + District *string `json:"district,omitempty"` + + // Connection counts + BackboneCount int `json:"backbone_count"` + FishboneCount int `json:"fishbone_count"` + TowerCount int `json:"tower_count"` + + CreatedAt time.Time `json:"created_at"` +} + +// Detailed response for single device +type NearestDeviceDetailResponse struct { + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType string `json:"device_type"` + Distance float64 `json:"distance_km"` + Address string `json:"address"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Status string `json:"status"` + PortAmount int `json:"port_amount"` + 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"` + + // Detailed connection information + Backbones []BackboneConnectionInfo `json:"backbones"` + Fishbones []FishboneConnectionInfo `json:"fishbones"` + Towers []TowerConnectionInfo `json:"towers"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type BackboneConnectionInfo struct { + ID uuid.UUID `json:"id"` + BackboneCode string `json:"backbone_code"` + CoreAmount int `json:"core_amount"` + IsStartDevice bool `json:"is_start_device"` + ConnectedTo string `json:"connected_to"` // Device code of the other end +} + +type FishboneConnectionInfo struct { + ID uuid.UUID `json:"id"` + FishboneCode string `json:"fishbone_code"` + CoreAmount int `json:"core_amount"` + BackboneCode string `json:"backbone_code"` + IsStartDevice bool `json:"is_start_device"` + ConnectedTo string `json:"connected_to"` // Device code of the other end +} + +type TowerConnectionInfo struct { + ID uuid.UUID `json:"id"` + TowerCode string `json:"tower_code"` + Distance float64 `json:"distance_km"` // Distance from tower to device + ImageURL *string `json:"image_url,omitempty"` +} \ No newline at end of file diff --git a/model/dto/res/tower_res.go b/model/dto/res/tower_res.go index 3447ea7..61aeff6 100644 --- a/model/dto/res/tower_res.go +++ b/model/dto/res/tower_res.go @@ -1,17 +1,19 @@ package res import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type TowerResponse struct { - ID uuid.UUID `json:"id"` - DeviceCode string `json:"device_code"` - TowerCode string `json:"tower_code"` - Longitude float64 `json:"longtitude"` - Latitude float64 `json:"latitude"` - Address string `json:"address"` - CreatedAt time.Time `json:"created_at"` -} + ID uuid.UUID `json:"id"` + DeviceCode *string `json:"device_code,omitempty"` // Make nullable + TowerCode *string `json:"tower_code"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Address string `json:"address"` + ImageURL string `json:"image_url"` + ExternalTower *bool `json:"external_tower"` // Make nullable + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} \ No newline at end of file diff --git a/model/entity/activity_logs.go b/model/entity/activity_logs.go new file mode 100644 index 0000000..e133898 --- /dev/null +++ b/model/entity/activity_logs.go @@ -0,0 +1,25 @@ +package entity + +import ( + "time" + "github.com/google/uuid" +) + +type ActivityLog struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + UserID uuid.UUID `json:"user_id" gorm:"type:uuid;not null"` + User User `json:"user" gorm:"foreignKey:UserID"` + Action string `json:"action" gorm:"not null"` // CREATE, UPDATE, DELETE, LOGIN, LOGOUT + Resource string `json:"resource" gorm:"not null"` // devices, backbone, fishbone, towers, etc. + ResourceID *string `json:"resource_id,omitempty"` // ID of the affected resource + OldData *string `json:"old_data,omitempty" gorm:"type:text"` // JSON of old data for updates + NewData *string `json:"new_data,omitempty" gorm:"type:text"` // JSON of new data + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Timestamp time.Time `json:"timestamp" gorm:"autoCreateTime"` + Details *string `json:"details,omitempty"` // Additional details +} + +func (ActivityLog) TableName() string { + return "activity_logs" +} \ No newline at end of file diff --git a/model/entity/backbone.go b/model/entity/backbone.go index 9bfc603..d042a33 100644 --- a/model/entity/backbone.go +++ b/model/entity/backbone.go @@ -1,21 +1,21 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type Backbone struct { - ID uuid.UUID `json:"id"` - DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"column:dev_start_id"` - DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"column:dev_end_id"` - CoreAmount int `json:"core_amount"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + BackboneCode string `json:"backbone_code" gorm:"unique"` + DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"` + DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"` + CoreAmount int `json:"core_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - DeviceStart Device `gorm:"foreignKey:DeviceStartID"` - DeviceEnd Device `gorm:"foreignKey:DeviceEndID"` + DeviceStart Device `gorm:"foreignKey:DeviceStartID"` + DeviceEnd Device `gorm:"foreignKey:DeviceEndID"` } func (Backbone) TableName() string { diff --git a/model/entity/count_assets.go b/model/entity/count_assets.go new file mode 100644 index 0000000..0b0e06f --- /dev/null +++ b/model/entity/count_assets.go @@ -0,0 +1,20 @@ +package entity + +import ( + "time" + "github.com/google/uuid" +) + +type CountAssets struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + FishboneAmount int `json:"fishbone_amount" gorm:"column:fishbone_amount"` + BackboneAmount int `json:"backbone_amount" gorm:"column:backbone_amount"` + DeviceAmount int `json:"device_amount" gorm:"column:device_amount"` + TowerAmount int `json:"tower_amount" gorm:"column:tower_amount"` + CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` +} + +func (CountAssets) TableName() string { + return "count_assets" +} \ No newline at end of file diff --git a/model/entity/device_details.go b/model/entity/device_details.go new file mode 100644 index 0000000..c5e6e3d --- /dev/null +++ b/model/entity/device_details.go @@ -0,0 +1,51 @@ +package entity + +import ( + "time" + "github.com/google/uuid" +) + +type DeviceDetails struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceCode string `json:"device_code" gorm:"unique"` + DeviceType DeviceType `json:"device_type"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + PortAmount int `json:"port_amount"` + Status DeviceStatus `json:"status"` + Region *string `json:"region,omitempty" gorm:"type:varchar(255)"` + Province *string `json:"province,omitempty" gorm:"type:varchar(255)"` + City *string `json:"city,omitempty" gorm:"type:varchar(255)"` + District *string `json:"district,omitempty" gorm:"type:varchar(255)"` + ImageURL *string `json:"image_url,omitempty" gorm:"type:text"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + // Fixed Relationships - Use direct foreign keys + DevicePort DevicePort `json:"device_port" gorm:"foreignKey:DeviceID;references:ID"` + BackbonesStart []Backbone `json:"backbones_start" gorm:"foreignKey:DeviceStartID;references:ID"` + BackbonesEnd []Backbone `json:"backbones_end" gorm:"foreignKey:DeviceEndID;references:ID"` + FishbonesStart []Fishbone `json:"fishbones_start" gorm:"foreignKey:DeviceStartID;references:ID"` + FishbonesEnd []Fishbone `json:"fishbones_end" gorm:"foreignKey:DeviceEndID;references:ID"` + Towers []Tower `json:"towers" gorm:"foreignKey:DeviceID;references:ID"` +} + +func (DeviceDetails) TableName() string { + return "devices" // Use same table as Device entity +} + +// Helper method to get all backbones connected to this device +func (d *DeviceDetails) GetAllBackbones() []Backbone { + allBackbones := make([]Backbone, 0) + allBackbones = append(allBackbones, d.BackbonesStart...) + allBackbones = append(allBackbones, d.BackbonesEnd...) + return allBackbones +} + +// Helper method to get all fishbones connected to this device +func (d *DeviceDetails) GetAllFishbones() []Fishbone { + allFishbones := make([]Fishbone, 0) + allFishbones = append(allFishbones, d.FishbonesStart...) + allFishbones = append(allFishbones, d.FishbonesEnd...) + return allFishbones +} \ No newline at end of file diff --git a/model/entity/device_inspection.go b/model/entity/device_inspection.go new file mode 100644 index 0000000..2b83280 --- /dev/null +++ b/model/entity/device_inspection.go @@ -0,0 +1,38 @@ +package entity + +import ( + "time" + "github.com/google/uuid" +) + +type DeviceInspection struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;column:device_id"` + BackboneID *uuid.UUID `json:"backbone_id,omitempty" gorm:"type:uuid;column:backbone_id"` + FishboneID *uuid.UUID `json:"fishbone_id,omitempty" gorm:"type:uuid;column:fishbone_id"` + TowerID *uuid.UUID `json:"tower_id,omitempty" gorm:"type:uuid;column:tower_id"` + UserID uuid.UUID `json:"user_id" gorm:"type:uuid;column:user_id"` + Status string `json:"status" gorm:"type:varchar(50);column:status"` + PortUsed string `json:"port_used" gorm:"type:varchar(50);column:port_used"` + PortAvailable string `json:"port_available" gorm:"type:varchar(50);column:port_available"` + CableAmount int `json:"cable_amount" gorm:"type:int;column:cable_amount"` + Description string `json:"description" gorm:"type:text;column:description"` + ImageURL *string `json:"image_url,omitempty" gorm:"type:text;column:image_url"` + InspectionPlacement string `json:"inspection_placement" gorm:"type:text;column:inspection_placement"` // Store the full address from OpenStreetMap + Longitude float64 `json:"longitude" gorm:"type:float;column:longitude"` + Latitude float64 `json:"latitude" gorm:"type:float;column:latitude"` + InspectionApproval string `json:"inspection_approval" gorm:"type:varchar(50);column:inspection_approval"` + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;column:updated_at"` + + // Relationships + Device Device `json:"device" gorm:"foreignKey:DeviceID"` + Backbone *Backbone `json:"backbone,omitempty" gorm:"foreignKey:BackboneID"` + Fishbone *Fishbone `json:"fishbone,omitempty" gorm:"foreignKey:FishboneID"` + User User `json:"user" gorm:"foreignKey:UserID"` + Tower *Tower `json:"tower,omitempty" gorm:"foreignKey:TowerID"` +} + +func (DeviceInspection) TableName() string { + return "device_inspection" +} \ No newline at end of file diff --git a/model/entity/device_port.go b/model/entity/device_port.go index 6e4e8a5..0e99684 100644 --- a/model/entity/device_port.go +++ b/model/entity/device_port.go @@ -1,21 +1,252 @@ package entity import ( + "database/sql/driver" + "encoding/json" + "fmt" + "sort" "time" "github.com/google/uuid" ) -type DevicePort struct { - ID uuid.UUID `json:"id"` - DeviceID uuid.UUID `json:"device_id" gorm:"column:device_id"` - PortNumber int `json:"port_number"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type StringSlice []string - Device Device `gorm:"foreignKey:DeviceID"` +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"` + 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 port_used is 0, return empty array + 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 if PortAssignments is empty + for i, name := range dp.CustomerNames { + portNum := i + 1 + if name != "" { + portMap[portNum] = &name + 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 + // 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++ + } + } + } + + // Step 4: Create the result for ALL ports up to maxPort + for i := 1; i <= maxPort; 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" + return "device_ports" } \ No newline at end of file diff --git a/model/entity/device_with_distance.go b/model/entity/device_with_distance.go new file mode 100644 index 0000000..52751df --- /dev/null +++ b/model/entity/device_with_distance.go @@ -0,0 +1,25 @@ +package entity + +import ( + "time" + "github.com/google/uuid" +) + +// DeviceWithDistance represents a device with calculated distance +type DeviceWithDistance struct { + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType DeviceType `json:"device_type"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + PortAmount int `json:"port_amount"` + Status DeviceStatus `json:"status"` + 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"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Distance float64 `json:"distance"` // Calculated distance +} \ No newline at end of file diff --git a/model/entity/devices.go b/model/entity/devices.go index 1e36fff..8c45be8 100644 --- a/model/entity/devices.go +++ b/model/entity/devices.go @@ -1,29 +1,40 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type DeviceType string type DeviceStatus string const ( - Odp DeviceType = "ODP" - activeDev DeviceStatus = "active" - inactiveDev DeviceStatus = "inactive" - maintenanceDev DeviceStatus = "maintenance" + ODP DeviceType = "ODP" + OTB DeviceType = "OTB" + + Closure DeviceType = "closure" + + ActiveDev DeviceStatus = "active" + InactiveDev DeviceStatus = "inactive" + MaintenanceDev DeviceStatus = "maintenance" ) type Device struct { - ID uuid.UUID `json:"id"` - DeviceCode string `json:"device_code"` - DeviceType DeviceType `json:"device_type"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - PortAmount int `json:"port_amount"` - Status DeviceStatus `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceCode string `json:"device_code" gorm:"unique"` + DeviceType DeviceType `json:"device_type"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + PortAmount int `json:"port_amount"` + Status DeviceStatus `json:"status"` + Province *string `json:"province,omitempty" gorm:"type:varchar(255)"` + City *string `json:"city,omitempty" gorm:"type:varchar(255)"` + District *string `json:"district,omitempty" gorm:"type:varchar(255)"` + ImageURL *string `json:"image_url,omitempty" gorm:"type:text"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (Device) TableName() string { + return "devices" } \ No newline at end of file diff --git a/model/entity/fishbone.go b/model/entity/fishbone.go index 59a3194..788d7aa 100644 --- a/model/entity/fishbone.go +++ b/model/entity/fishbone.go @@ -1,27 +1,26 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type Fishbone struct { - ID uuid.UUID `json:"id"` - BackboneID uuid.UUID `json:"backbone_id" gorm:"column:bb_id"` - DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"column:dev_start_id"` - DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"column:dev_end_id"` - CoreAmount int `json:"core_amount"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + FishboneCode string `json:"fishbone_code" gorm:"unique"` + BackboneID uuid.UUID `json:"bb_id" gorm:"type:uuid;column:bb_id"` + DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"` + DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"` + CoreAmount int `json:"core_amount" gorm:"column:core_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - DeviceStart Device `gorm:"foreignKey:DeviceStartID"` - DeviceEnd Device `gorm:"foreignKey:DeviceEndID"` - - Backbone Backbone `gorm:"foreignKey:BackboneID"` - + // Relationships + Backbone Backbone `json:"backbone" gorm:"foreignKey:BackboneID"` + DeviceStart Device `json:"device_start" gorm:"foreignKey:DeviceStartID"` + DeviceEnd Device `json:"device_end" gorm:"foreignKey:DeviceEndID"` } func (Fishbone) TableName() string { - return "fishbone" + return "fishbone" } \ No newline at end of file diff --git a/model/entity/roles.go b/model/entity/roles.go index ee6089f..f5264df 100644 --- a/model/entity/roles.go +++ b/model/entity/roles.go @@ -1,10 +1,18 @@ package entity -import "github.com/google/uuid" +import ( + "time" + + "github.com/google/uuid" +) type Role struct { - Id uuid.UUID `json:"id"` - Name string `json:"name"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + Id uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + Name string `json:"name" gorm:"type:varchar(100);not null"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (Role) TableName() string { + return "roles" } \ No newline at end of file diff --git a/model/entity/tower.go b/model/entity/tower.go index 69a0315..9a59dc5 100644 --- a/model/entity/tower.go +++ b/model/entity/tower.go @@ -1,21 +1,22 @@ package entity import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type Tower struct { - ID uuid.UUID `json:"id"` - DeviceID uuid.UUID`json:"dev_id" gorm:"column:dev_id"` - TowerCode string `json:"tower_code"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - 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:"dev_id,omitempty" gorm:"type:uuid;column:dev_id;null"` // Make nullable + TowerCode string `json:"tower_code" gorm:"unique"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ImageURL string `json:"image_url" gorm:"column:image_url"` + ExternalTower *bool `json:"external_tower,omitempty" gorm:"column:external_tower;null"` // Make nullable and fix typo + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - Device Device `gorm:"foreignKey:DeviceID"` + Device *Device `json:"device,omitempty" gorm:"foreignKey:DeviceID"` // Make nullable } func (Tower) TableName() string { diff --git a/model/entity/tower_inspection.go b/model/entity/tower_inspection.go new file mode 100644 index 0000000..86f827f --- /dev/null +++ b/model/entity/tower_inspection.go @@ -0,0 +1,13 @@ +package entity + +import "github.com/google/uuid" + +type TowerInspection struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + TowerID uuid.UUID `json:"tower_id" gorm:"type:uuid;column:tower_id"` + UserID uuid.UUID `json:"user_id" gorm:"type:uuid;column:user_id"` + Description string `json:"description" gorm:"type:text;column:description"` + ImageURL string `json:"image_url" gorm:"type:text;column:image_url"` + CreatedAt string `json:"created_at" gorm:"type:timestamp;column:created_at"` + UpdatedAt string `json:"updated_at" gorm:"type:timestamp;column:updated_at"` +} \ No newline at end of file diff --git a/model/entity/users.go b/model/entity/users.go index 9a962cb..29db6f3 100644 --- a/model/entity/users.go +++ b/model/entity/users.go @@ -2,17 +2,21 @@ package entity import ( "time" - "github.com/google/uuid" ) type User struct { - ID uuid.UUID `json:"id"` - RoleID uuid.UUID `json:"role_id"` - Role Role `json:"role"` - Name string `json:"name"` - Username string `json:"username"` - Password string `json:"password"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + NomorInduk *string `json:"nomor_induk,omitempty" gorm:"unique"` // Add this field + RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"` + Role Role `json:"role" gorm:"foreignKey:RoleID"` + Name string `json:"name"` + Username string `json:"username" gorm:"unique"` + Password string `json:"password"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (User) TableName() string { + return "users" } \ No newline at end of file diff --git a/repository/activity_logs_repo.go b/repository/activity_logs_repo.go new file mode 100644 index 0000000..4f51bde --- /dev/null +++ b/repository/activity_logs_repo.go @@ -0,0 +1,90 @@ +package repository + +import ( + "users_management/m/model/entity" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ActivityLogRepo interface { + Create(log entity.ActivityLog) error + GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.ActivityLog, error) + GetAllForAdmins(limit, offset int) ([]entity.ActivityLog, error) + GetByUserRole(role string, limit, offset int) ([]entity.ActivityLog, error) + CountByUserID(userID uuid.UUID) (int64, error) + CountAll() (int64, error) + CountByUserRole(role string) (int64, error) +} + +type activityLogRepo struct { + db *gorm.DB +} + +func NewActivityLogRepo(db *gorm.DB) ActivityLogRepo { + return &activityLogRepo{ + db: db, + } +} + +func (r *activityLogRepo) Create(log entity.ActivityLog) error { + return r.db.Create(&log).Error +} + +func (r *activityLogRepo) GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.ActivityLog, error) { + var logs []entity.ActivityLog + err := r.db.Where("user_id = ?", userID). + Preload("User"). + Preload("User.Role"). + Order("timestamp DESC"). + Limit(limit). + Offset(offset). + Find(&logs).Error + return logs, err +} + +func (r *activityLogRepo) GetAllForAdmins(limit, offset int) ([]entity.ActivityLog, error) { + var logs []entity.ActivityLog + err := r.db.Preload("User"). + Preload("User.Role"). + Order("timestamp DESC"). + Limit(limit). + Offset(offset). + Find(&logs).Error + return logs, err +} + +func (r *activityLogRepo) GetByUserRole(role string, limit, offset int) ([]entity.ActivityLog, error) { + var logs []entity.ActivityLog + err := r.db.Joins("JOIN users ON users.id = activity_logs.user_id"). + Joins("JOIN roles ON roles.id = users.role_id"). + Where("roles.name = ?", role). + Preload("User"). + Preload("User.Role"). + Order("activity_logs.timestamp DESC"). + Limit(limit). + Offset(offset). + Find(&logs).Error + return logs, err +} + +func (r *activityLogRepo) CountByUserID(userID uuid.UUID) (int64, error) { + var count int64 + err := r.db.Model(&entity.ActivityLog{}).Where("user_id = ?", userID).Count(&count).Error + return count, err +} + +func (r *activityLogRepo) CountAll() (int64, error) { + var count int64 + err := r.db.Model(&entity.ActivityLog{}).Count(&count).Error + return count, err +} + +func (r *activityLogRepo) CountByUserRole(role string) (int64, error) { + var count int64 + err := r.db.Model(&entity.ActivityLog{}). + Joins("JOIN users ON users.id = activity_logs.user_id"). + Joins("JOIN roles ON roles.id = users.role_id"). + Where("roles.name = ?", role). + Count(&count).Error + return count, err +} \ No newline at end of file diff --git a/repository/backbone_repo.go b/repository/backbone_repo.go index 7d98d63..2bcdf3e 100644 --- a/repository/backbone_repo.go +++ b/repository/backbone_repo.go @@ -12,6 +12,7 @@ type BackboneRepo interface { GetAll() ([]entity.Backbone, error) Update(id uuid.UUID, updates map[string]interface{}) error GetByID(id uuid.UUID) (entity.Backbone, error) + WithTransaction(fn func(*gorm.DB) error) error } type backboneRepo struct { @@ -57,4 +58,8 @@ func (r *backboneRepo) GetByID(id uuid.UUID) (entity.Backbone, error) { return backbone, err } return backbone, nil +} + +func (r *backboneRepo) WithTransaction(fn func(*gorm.DB) error) error { + return r.db.Transaction(fn) } \ No newline at end of file diff --git a/repository/count_assets.go b/repository/count_assets.go new file mode 100644 index 0000000..5b0165b --- /dev/null +++ b/repository/count_assets.go @@ -0,0 +1,136 @@ +package repository + +import ( + "users_management/m/model/entity" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CountAssetsRepo interface { + Get() (entity.CountAssets, error) + UpdateCounts() error +} + + +type countAssetsRepo struct { + db *gorm.DB +} + + +func NewCountAssetsRepo(db *gorm.DB) CountAssetsRepo { + return &countAssetsRepo{ + db: db, + } +} + +func (r *countAssetsRepo) Get() (entity.CountAssets, error) { + var countAssets entity.CountAssets + + // Try to get existing record, if not found create a new one + err := r.db.First(&countAssets).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // Create new record with current counts + return r.createNewCountRecord() + } + return countAssets, err + } + + return countAssets, nil +} + +func (r *countAssetsRepo) UpdateCounts() error { + var countAssets entity.CountAssets + + // Get current counts from each table + fishboneCount, err := r.countFishbones() + if err != nil { + return err + } + + backboneCount, err := r.countBackbones() + if err != nil { + return err + } + + deviceCount, err := r.countDevices() + if err != nil { + return err + } + + towerCount, err := r.countTowers() + if err != nil { + return err + } + + // Check if record exists + err = r.db.First(&countAssets).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // Create new record + countAssets = entity.CountAssets{ + ID: uuid.New(), + FishboneAmount: fishboneCount, + BackboneAmount: backboneCount, + DeviceAmount: deviceCount, + TowerAmount: towerCount, + } + return r.db.Create(&countAssets).Error + } + return err + } + + // Update existing record + updates := map[string]interface{}{ + "fishbone_amount": fishboneCount, + "backbone_amount": backboneCount, + "device_amount": deviceCount, + "tower_amount": towerCount, + } + + return r.db.Model(&countAssets).Updates(updates).Error +} + +func (r *countAssetsRepo) createNewCountRecord() (entity.CountAssets, error) { + fishboneCount, _ := r.countFishbones() + backboneCount, _ := r.countBackbones() + deviceCount, _ := r.countDevices() + towerCount, _ := r.countTowers() + + countAssets := entity.CountAssets{ + ID: uuid.New(), + FishboneAmount: fishboneCount, + BackboneAmount: backboneCount, + DeviceAmount: deviceCount, + TowerAmount: towerCount, + } + + err := r.db.Create(&countAssets).Error + return countAssets, err +} + +// Helper methods to count each entity +func (r *countAssetsRepo) countFishbones() (int, error) { + var count int64 + err := r.db.Model(&entity.Fishbone{}).Count(&count).Error + return int(count), err +} + +func (r *countAssetsRepo) countBackbones() (int, error) { + var count int64 + err := r.db.Model(&entity.Backbone{}).Count(&count).Error + return int(count), err +} + +func (r *countAssetsRepo) countDevices() (int, error) { + var count int64 + err := r.db.Model(&entity.Device{}).Count(&count).Error + return int(count), err +} + +func (r *countAssetsRepo) countTowers() (int, error) { + var count int64 + err := r.db.Model(&entity.Tower{}).Count(&count).Error + return int(count), err +} \ No newline at end of file diff --git a/repository/device_details.go b/repository/device_details.go new file mode 100644 index 0000000..8a9cc37 --- /dev/null +++ b/repository/device_details.go @@ -0,0 +1,919 @@ +package repository + +import ( + "fmt" + "time" + "users_management/m/model/dto/req" + "users_management/m/model/entity" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +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 + + +} + +type deviceDetailsRepo struct { + db *gorm.DB +} + +func NewDeviceDetailsRepo(db *gorm.DB) DeviceDetailsRepo { + return &deviceDetailsRepo{ + db: db, + } +} + +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 + } + + // 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) + } + + // 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, + } + } + } + + // 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 + + // 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) + } + } + } + + // Update the port assignment + if update.NewCustomerName == nil { + // Remove customer from port + devicePort.PortAssignments[portIndex].CustomerName = nil + } else { + // Assign/update customer on port + devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName + } + + // 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 + } + + // 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) + } + + // 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, + } + } + } + + // 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 + } + + // Apply updates + for _, update := range updates { + finalAssignments[update.PortNumber] = update.NewCustomerName + } + + // 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 all updates + for _, update := range updates { + portIndex := update.PortNumber - 1 + devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName + } + + // 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 + + // 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 + } + + 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) + } + + 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) + } + + // Remove customer from port + 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) 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 { + return r.db.Transaction(func(tx *gorm.DB) error { + // 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, + } + + return tx.Create(&devicePort).Error + }) +} + +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("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("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 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 nil + }) +} + +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 +} + +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 + } + + 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) + } + + // 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, + } + } + } + + // 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 + }) +} + +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 +} + +// func (r *deviceDetailsRepo) ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error { +// var devicePort entity.DevicePort +// 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 +} + +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 +} + +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 +} + + + +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 +} + +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 + }) +} + +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 + } + + // 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) + } + + // 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, + } + } + } + + // 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 + }) +} + +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) + } + + // 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) + } + + devicePort.CustomerNames = newCustomerNames + devicePort.CustomerCount = len(devicePort.CustomerNames) + devicePort.PortAvailable = devicePort.PortAvailable + 1 + devicePort.UpdatedAt = time.Now() + + 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) { + // 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 +} + +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 + } + + // 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) + } + + // 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++ + } + } + + 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") + } + } + + // 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, 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) + } + } + + // Assign customer to port + devicePort.PortAssignments[portIndex].CustomerName = &assignment.CustomerName + } + + // 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 + } + + 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 diff --git a/repository/device_inspection.go b/repository/device_inspection.go new file mode 100644 index 0000000..95773a5 --- /dev/null +++ b/repository/device_inspection.go @@ -0,0 +1,155 @@ +package repository + +import ( + "users_management/m/model/entity" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type DeviceInspectionRepo interface { + Create(inspection entity.DeviceInspection) error + GetAll() ([]entity.DeviceInspection, error) + GetByID(id uuid.UUID) (entity.DeviceInspection, error) + GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.DeviceInspection, error) + Update(id uuid.UUID, updates map[string]interface{}) error + CheckDeviceExists(deviceID uuid.UUID) (bool, error) + CheckBackboneExists(backboneID uuid.UUID) (bool, error) + CheckFishboneExists(fishboneID uuid.UUID) (bool, error) + CheckTowerExists(towerID uuid.UUID) (bool, error) + GetDeviceByID(deviceID uuid.UUID) (entity.Device, error) + CountCablesByDevice(deviceID uuid.UUID, deviceType string) (int, error) + CountByUserID(userID uuid.UUID) (int64, error) + CountAll() (int64, error) + UpdateDeviceStatus(deviceID uuid.UUID, status string) error + GetDevicePortByDeviceID(deviceID uuid.UUID) (entity.DevicePort, error) + UpdateDevicePort(deviceID uuid.UUID, updates map[string]interface{}) error + CheckOwnership(inspectionID, userID uuid.UUID) (bool, error) // Add this line +} + +type deviceInspectionRepo struct { + db *gorm.DB +} + +func NewDeviceInspectionRepo(db *gorm.DB) DeviceInspectionRepo { + return &deviceInspectionRepo{ + db: db, + } +} + +func (r *deviceInspectionRepo) CheckOwnership(inspectionID, userID uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.DeviceInspection{}). + Where("id = ? AND user_id = ?", inspectionID, userID). + Count(&count).Error + return count > 0, err +} + +func (r *deviceInspectionRepo) Create(inspection entity.DeviceInspection) error { + return r.db.Create(&inspection).Error +} + +func (r *deviceInspectionRepo) GetAll() ([]entity.DeviceInspection, error) { + var inspections []entity.DeviceInspection + err := r.db.Preload("Device").Preload("User").Preload("Backbone"). + Preload("Fishbone").Preload("Tower").Find(&inspections).Error + return inspections, err +} + +func (r *deviceInspectionRepo) GetByID(id uuid.UUID) (entity.DeviceInspection, error) { + var inspection entity.DeviceInspection + err := r.db.Preload("Device").Preload("User").Preload("Backbone"). + Preload("Fishbone").Preload("Tower").Where("id = ?", id).First(&inspection).Error + return inspection, err +} + +func (r *deviceInspectionRepo) GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.DeviceInspection, error) { + var inspections []entity.DeviceInspection + err := r.db.Preload("Device").Preload("User").Preload("Backbone"). + Preload("Fishbone").Preload("Tower"). + Where("user_id = ?", userID). + Order("created_at DESC"). + Limit(limit).Offset(offset). + Find(&inspections).Error + return inspections, err +} + +func (r *deviceInspectionRepo) Update(id uuid.UUID, updates map[string]interface{}) error { + return r.db.Model(&entity.DeviceInspection{}).Where("id = ?", id).Updates(updates).Error +} + +func (r *deviceInspectionRepo) CheckDeviceExists(deviceID uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Device{}).Where("id = ?", deviceID).Count(&count).Error + return count > 0, err +} + +func (r *deviceInspectionRepo) CheckBackboneExists(backboneID uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Backbone{}).Where("id = ?", backboneID).Count(&count).Error + return count > 0, err +} + +func (r *deviceInspectionRepo) CheckFishboneExists(fishboneID uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Fishbone{}).Where("id = ?", fishboneID).Count(&count).Error + return count > 0, err +} + +func (r *deviceInspectionRepo) CheckTowerExists(towerID uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Tower{}).Where("id = ?", towerID).Count(&count).Error + return count > 0, err +} + +func (r *deviceInspectionRepo) GetDeviceByID(deviceID uuid.UUID) (entity.Device, error) { + var device entity.Device + err := r.db.Where("id = ?", deviceID).First(&device).Error + return device, err +} + +func (r *deviceInspectionRepo) CountCablesByDevice(deviceID uuid.UUID, deviceType string) (int, error) { + var count int64 + + if deviceType == "OTB" { + // Count backbones using this device + err := r.db.Model(&entity.Backbone{}). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Count(&count).Error + return int(count), err + } else if deviceType == "ODP" { + // Sum core amounts of fishbones using this device + var totalCores int64 + err := r.db.Model(&entity.Fishbone{}). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Select("COALESCE(SUM(core_amount), 0)").Scan(&totalCores).Error + return int(totalCores), err + } + + return 0, nil +} + +func (r *deviceInspectionRepo) CountByUserID(userID uuid.UUID) (int64, error) { + var count int64 + err := r.db.Model(&entity.DeviceInspection{}).Where("user_id = ?", userID).Count(&count).Error + return count, err +} + +func (r *deviceInspectionRepo) CountAll() (int64, error) { + var count int64 + err := r.db.Model(&entity.DeviceInspection{}).Count(&count).Error + return count, err +} + +func (r *deviceInspectionRepo) UpdateDeviceStatus(deviceID uuid.UUID, status string) error { + return r.db.Model(&entity.Device{}).Where("id = ?", deviceID).Update("status", status).Error +} + +func (r *deviceInspectionRepo) GetDevicePortByDeviceID(deviceID uuid.UUID) (entity.DevicePort, error) { + var devicePort entity.DevicePort + err := r.db.Where("device_id = ?", deviceID).First(&devicePort).Error + return devicePort, err +} + +func (r *deviceInspectionRepo) UpdateDevicePort(deviceID uuid.UUID, updates map[string]interface{}) error { + return r.db.Model(&entity.DevicePort{}).Where("device_id = ?", deviceID).Updates(updates).Error +} \ No newline at end of file diff --git a/repository/devices_repo.go b/repository/devices_repo.go index 5f74164..e62d9e1 100644 --- a/repository/devices_repo.go +++ b/repository/devices_repo.go @@ -1,6 +1,7 @@ package repository import ( + "time" "users_management/m/model/entity" "github.com/google/uuid" @@ -27,13 +28,40 @@ func NewDevicesRepo(db *gorm.DB) DevicesRepo { } func (r *devicesRepo) Post(device entity.Device) error { - err := r.db.Create(&device).Error - if err != nil { - return err - } - return nil -} + return r.db.Transaction(func(tx *gorm.DB) error { + // Create the device first + if err := tx.Create(&device).Error; err != nil { + return err + } + // Create the corresponding DevicePort record + customerCount := 0 + customerNames := make([]string, 0) + + // For ODP devices, initialize customer tracking + if device.DeviceType == "ODP" { + // Customer count starts at 0, will be updated when fishbones are connected + customerCount = 0 + } + + devicePort := entity.DevicePort{ + ID: uuid.New(), + DeviceID: device.ID, + PortUsed: 0, + PortAvailable: device.PortAmount, + CustomerCount: customerCount, + CustomerNames: customerNames, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := tx.Create(&devicePort).Error; err != nil { + return err + } + + return nil + }) +} func (r *devicesRepo) GetAll() ([]entity.Device, error) { var devices []entity.Device err := r.db.Find(&devices).Error diff --git a/repository/fishbone_repo.go b/repository/fishbone_repo.go index 8659c1c..27566d6 100644 --- a/repository/fishbone_repo.go +++ b/repository/fishbone_repo.go @@ -1,6 +1,7 @@ package repository import ( + "log" "users_management/m/model/entity" "github.com/google/uuid" @@ -16,6 +17,14 @@ type FishboneRepo interface { CountFishbone() (map[uuid.UUID]int, error) CountFishboneByBackboneID(backboneID uuid.UUID) (int, error) + GetAllWithRelations() ([]entity.Fishbone, error) + GetByIDWithRelations(id uuid.UUID) (entity.Fishbone, error) + CheckFishboneExists(id uuid.UUID) (bool, error) + CheckBackboneExists(id uuid.UUID) (bool, error) + 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 { @@ -36,15 +45,32 @@ func (r *fishboneRepo) Post(fishbone entity.Fishbone) error { return nil } -func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) { +func (r *fishboneRepo) GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error) { var fishbones []entity.Fishbone - err := r.db.Preload("DeviceStart").Preload("DeviceEnd").Preload("Backbone").Preload("Backbone.DeviceStart").Preload("Backbone.DeviceEnd").Find(&fishbones).Error + 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 + err := r.db.Preload("DeviceStart").Preload("DeviceEnd").Preload("Backbone").Find(&fishbones).Error if err != nil { return fishbones, err } + + log.Print(fishbones) return fishbones, nil } +func (r *fishboneRepo) WithTransaction(fn func(*gorm.DB) error) error { + return r.db.Transaction(fn) +} + func (r *fishboneRepo) Update(id uuid.UUID,updates map[string]interface{}) error { err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error if err != nil { @@ -93,4 +119,38 @@ func (r *fishboneRepo) CountFishboneByBackboneID(backboneID uuid.UUID) (int, err return 0, err } return int(count), nil +} + +func (r *fishboneRepo) GetAllWithRelations() ([]entity.Fishbone, error) { + var fishbones []entity.Fishbone + err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").Find(&fishbones).Error + return fishbones, err +} + +func (r *fishboneRepo) GetByIDWithRelations(id uuid.UUID) (entity.Fishbone, error) { + var fishbone entity.Fishbone + err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").Where("id = ?", id).First(&fishbone).Error + return fishbone, err +} + +func (r *fishboneRepo) CheckFishboneExists(id uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Count(&count).Error + return count > 0, err +} + +func (r *fishboneRepo) CheckBackboneExists(id uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Backbone{}).Where("id = ?", id).Count(&count).Error + return count > 0, err +} + +func (r *fishboneRepo) CheckDeviceExists(id uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Device{}).Where("id = ?", id).Count(&count).Error + return count > 0, err +} + +func (r *fishboneRepo) Delete(id uuid.UUID) error { + return r.db.Where("id = ?", id).Delete(&entity.Fishbone{}).Error } \ No newline at end of file diff --git a/repository/nearest_device_repo.go b/repository/nearest_device_repo.go new file mode 100644 index 0000000..025b364 --- /dev/null +++ b/repository/nearest_device_repo.go @@ -0,0 +1,118 @@ +package repository + +import ( + "users_management/m/model/entity" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type NearestDeviceRepo interface { + GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.DeviceWithDistance, error) + GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error) + GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) + GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) + GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) + CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error) + +} + +type nearestDeviceRepo struct { + db *gorm.DB +} + +func NewNearestDeviceRepo(db *gorm.DB) NearestDeviceRepo { + return &nearestDeviceRepo{ + db: db, + } +} + +func (r *nearestDeviceRepo) GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.DeviceWithDistance, error) { + var devices []entity.DeviceWithDistance + + // Build the subquery first + subQuery := r.db.Table("devices"). + Select(`id, device_code, device_type, longitude, latitude, port_amount, status, + region, province, city, district, image_url, created_at, updated_at, + (6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))) AS distance`, + latitude, longitude, latitude) + + // Apply location filters to subquery + if province != nil && *province != "" { + subQuery = subQuery.Where("province = ?", *province) + } + if city != nil && *city != "" { + subQuery = subQuery.Where("city = ?", *city) + } + if district != nil && *district != "" { + subQuery = subQuery.Where("district = ?", *district) + } + + // Use the subquery in the main query + err := r.db.Table("(?) as devices_with_distance", subQuery). + Where("distance <= ?", radius). + Order("distance ASC"). + Limit(limit). + Scan(&devices).Error + + return devices, err +} + +func (r *nearestDeviceRepo) GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error) { + var device entity.Device + err := r.db.Where("id = ?", id).First(&device).Error + return device, err +} + +func (r *nearestDeviceRepo) 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 +} + +func (r *nearestDeviceRepo) 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 +} + +func (r *nearestDeviceRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) { + var towers []entity.Tower + err := r.db.Preload("Device"). + Where("dev_id = ?", deviceID). + Find(&towers).Error + return towers, err +} + +func (r *nearestDeviceRepo) CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error) { + var backboneCountInt64, fishboneCountInt64, towerCountInt64 int64 + + // Count backbones + err = r.db.Model(&entity.Backbone{}). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Count(&backboneCountInt64).Error + if err != nil { + return 0, 0, 0, err + } + + // Count fishbones + err = r.db.Model(&entity.Fishbone{}). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Count(&fishboneCountInt64).Error + if err != nil { + return 0, 0, 0, err + } + + // Count towers + err = r.db.Model(&entity.Tower{}). + Where("dev_id = ?", deviceID). + Count(&towerCountInt64).Error + if err != nil { + return 0, 0, 0, err + } + + return int(backboneCountInt64), int(fishboneCountInt64), int(towerCountInt64), nil +} \ No newline at end of file diff --git a/repository/tower_repo.go b/repository/tower_repo.go index 117f0e4..ea798f1 100644 --- a/repository/tower_repo.go +++ b/repository/tower_repo.go @@ -12,6 +12,7 @@ type TowerRepo interface { GetAll() ([]entity.Tower, error) Update(id uuid.UUID,updates map[string]interface{}) error GetByID(id uuid.UUID) (entity.Tower, error) + CheckDeviceExists(deviceID uuid.UUID) (bool, error) } type towerRepo struct { @@ -42,6 +43,11 @@ func (r *towerRepo) GetAll() ([]entity.Tower, error) { return towers, nil } +func (r *towerRepo) CheckDeviceExists(deviceID uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Device{}).Where("id = ?", deviceID).Count(&count).Error + return count > 0, err +} func (r *towerRepo) Update(id uuid.UUID,updates map[string]interface{}) error { err := r.db.Model(&entity.Tower{}).Where("id = ?", id).Updates(updates).Error if err != nil { diff --git a/repository/users_repo.go b/repository/users_repo.go index cd2ca6d..055f3bd 100644 --- a/repository/users_repo.go +++ b/repository/users_repo.go @@ -1,50 +1,75 @@ package repository import ( - "users_management/m/model/entity" + "users_management/m/model/entity" - "gorm.io/gorm" + "github.com/google/uuid" + "gorm.io/gorm" ) type UsersRepo interface { - Post(user entity.User) error - GetRoleByDepartment(departmentName string) (entity.Role, error) - GetUserByUsername(username string) (entity.User, error) + Post(user entity.User) error + GetRoleByDepartment(departmentName string) (entity.Role, error) + GetUserByUsername(username string) (entity.User, error) + GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this + CreateUserFromExternal(user entity.User) error // Add this + UpdateUserRole(nomorInduk string, roleID uuid.UUID) error // Add this + GetAllUsers() ([]entity.User, error) // Add this } type usersRepo struct { - db *gorm.DB + db *gorm.DB } func NewUsersRepo(db *gorm.DB) UsersRepo { - return &usersRepo{ - db: db, - } + return &usersRepo{ + db: db, + } } func (r *usersRepo) Post(user entity.User) error { - err := r.db.Create(&user).Error - if err != nil { - return err - } - return nil + err := r.db.Create(&user).Error + if err != nil { + return err + } + return nil } func (r *usersRepo) GetRoleByDepartment(departmentName string) (entity.Role, error) { - var role entity.Role - err := r.db.Where("name = ?", departmentName).First(&role).Error - if err != nil { - return role, err - } - return role, nil + var role entity.Role + err := r.db.Where("name = ?", departmentName).First(&role).Error + if err != nil { + return role, err + } + return role, nil } func (r *usersRepo) GetUserByUsername(username string) (entity.User, error) { - var users entity.User - err := r.db.Where("username = ?", username).First(&users).Error - if err != nil { - return users, err - } - return users, nil + var users entity.User + err := r.db.Where("username = ?", username).Preload("Role").First(&users).Error + if err != nil { + return users, err + } + return users, nil } +// New methods for RBAC +func (r *usersRepo) GetUserByNomorInduk(nomorInduk string) (entity.User, error) { + var user entity.User + err := r.db.Where("nomor_induk = ?", nomorInduk).Preload("Role").First(&user).Error + return user, err +} + +func (r *usersRepo) CreateUserFromExternal(user entity.User) error { + return r.db.Create(&user).Error +} + +func (r *usersRepo) UpdateUserRole(nomorInduk string, roleID uuid.UUID) error { + return r.db.Model(&entity.User{}).Where("nomor_induk = ?", nomorInduk).Update("role_id", roleID).Error +} + +func (r *usersRepo) GetAllUsers() ([]entity.User, error) { + var users []entity.User + err := r.db.Preload("Role").Find(&users).Error + return users, err +} \ No newline at end of file diff --git a/uploads/towers/ae4227e6-8492-415f-af47-95e7464de18f_1749704734.jpg b/uploads/towers/ae4227e6-8492-415f-af47-95e7464de18f_1749704734.jpg new file mode 100644 index 0000000..ed5ce6a Binary files /dev/null and b/uploads/towers/ae4227e6-8492-415f-af47-95e7464de18f_1749704734.jpg differ diff --git a/uploads/towers/c990cf49-f6ea-4d81-97a7-d3748afec6c4_1749711025.jpeg b/uploads/towers/c990cf49-f6ea-4d81-97a7-d3748afec6c4_1749711025.jpeg new file mode 100644 index 0000000..d919e15 Binary files /dev/null and b/uploads/towers/c990cf49-f6ea-4d81-97a7-d3748afec6c4_1749711025.jpeg differ diff --git a/uploads/towers/f55d71d2-753f-4219-8650-a5f48ca09b0f_1749707561.png b/uploads/towers/f55d71d2-753f-4219-8650-a5f48ca09b0f_1749707561.png new file mode 100644 index 0000000..b5173f9 Binary files /dev/null and b/uploads/towers/f55d71d2-753f-4219-8650-a5f48ca09b0f_1749707561.png differ diff --git a/usecase/activity_logs_usecase.go b/usecase/activity_logs_usecase.go new file mode 100644 index 0000000..fe66043 --- /dev/null +++ b/usecase/activity_logs_usecase.go @@ -0,0 +1,101 @@ +package usecase + +import ( + "encoding/json" + "log" + "users_management/m/model/entity" + "users_management/m/repository" + + "github.com/google/uuid" +) + +type ActivityLogUseCase interface { + LogActivity(userID uuid.UUID, action, resource string, resourceID *string, oldData, newData interface{}, ipAddress, userAgent, details string) error + GetUserLogs(userID uuid.UUID, page, limit int) ([]entity.ActivityLog, int64, error) + GetAllLogs(page, limit int) ([]entity.ActivityLog, int64, error) + GetTeknisinLogs(page, limit int) ([]entity.ActivityLog, int64, error) +} + +type activityLogUseCase struct { + activityLogRepo repository.ActivityLogRepo +} + +func NewActivityLogUseCase(activityLogRepo repository.ActivityLogRepo) ActivityLogUseCase { + return &activityLogUseCase{ + activityLogRepo: activityLogRepo, + } +} + +func (uc *activityLogUseCase) LogActivity(userID uuid.UUID, action, resource string, resourceID *string, oldData, newData interface{}, ipAddress, userAgent, details string) error { + var oldDataJSON, newDataJSON *string + + if oldData != nil { + oldBytes, err := json.Marshal(oldData) + if err == nil { + oldStr := string(oldBytes) + oldDataJSON = &oldStr + } + } + + if newData != nil { + newBytes, err := json.Marshal(newData) + if err == nil { + newStr := string(newBytes) + newDataJSON = &newStr + } + } + + var detailsPtr *string + if details != "" { + detailsPtr = &details + } + + log := entity.ActivityLog{ + ID: uuid.New(), + UserID: userID, + Action: action, + Resource: resource, + ResourceID: resourceID, + OldData: oldDataJSON, + NewData: newDataJSON, + IPAddress: ipAddress, + UserAgent: userAgent, + Details: detailsPtr, + } + + return uc.activityLogRepo.Create(log) +} + +func (uc *activityLogUseCase) GetUserLogs(userID uuid.UUID, page, limit int) ([]entity.ActivityLog, int64, error) { + offset := (page - 1) * limit + log.Printf("Fetching logs for user %s, page: %d, limit: %d, offset: %d", userID, page, limit, offset) + logs, err := uc.activityLogRepo.GetByUserID(userID, limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := uc.activityLogRepo.CountByUserID(userID) + return logs, total, err +} + +func (uc *activityLogUseCase) GetAllLogs(page, limit int) ([]entity.ActivityLog, int64, error) { + offset := (page - 1) * limit + logs, err := uc.activityLogRepo.GetAllForAdmins(limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := uc.activityLogRepo.CountAll() + return logs, total, err +} + +func (uc *activityLogUseCase) GetTeknisinLogs(page, limit int) ([]entity.ActivityLog, int64, error) { + offset := (page - 1) * limit + logs, err := uc.activityLogRepo.GetByUserRole("Teknisi", limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := uc.activityLogRepo.CountByUserRole("Teknisi") + return logs, total, err +} \ No newline at end of file diff --git a/usecase/auth_usecase.go b/usecase/auth_usecase.go index 2d84e7e..9a50afb 100644 --- a/usecase/auth_usecase.go +++ b/usecase/auth_usecase.go @@ -2,6 +2,7 @@ package usecase import ( "bytes" + "crypto/tls" "encoding/json" "errors" "io/ioutil" @@ -28,13 +29,32 @@ type authUsecase struct { userRepo repository.UsersRepo validate *validator.Validate cfg *config.Config + client *http.Client } func NewAuthUsecase(userRepo repository.UsersRepo, cfg *config.Config) AuthUsecase { + var client *http.Client + + if cfg.LoginConfig.SkipSSLVerification { + // Create HTTP client with SSL verification disabled + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client = &http.Client{ + Transport: tr, + Timeout: 30 * time.Second, + } + } else { + // Use standard HTTP client with SSL verification enabled + client = &http.Client{ + Timeout: 30 * time.Second, + } + } return &authUsecase{ userRepo: userRepo, validate: validator.New(), cfg: cfg, + client: client, } } @@ -54,14 +74,16 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err return "","","", err } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - client := &http.Client{} - resp, err := client.Do(req) + // client := &http.Client{} + resp, err := u.client.Do(req) if err != nil { return "","","", err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -88,7 +110,8 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err meReq.Header.Set("Authorization", "Bearer "+token) meReq.Header.Set("Accept", "application/json") - meResp, err := client.Do(meReq) + meResp, err := u.client.Do(meReq) + if err != nil { return "","","", err } @@ -109,15 +132,25 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err return "","","", err } + departemen := meResponse.Data.Departemen - if departemen != "Teknisi" { + if departemen != "TECHNICAL PRORGAMMER" { return "","","", errors.New("user is not a technician") } - role , err := u.userRepo.GetRoleByDepartment(departemen) - - if err != nil { - return "","","", err + isUserExist, err := u.userRepo.GetUserByUsername(login.Username) + if isUserExist.ID != uuid.Nil { + // Validate the password + if !utils.CheckPasswordHash(login.Password, isUserExist.Password) { + return "","","", errors.New("incorrect password") + } + if err != nil { + return "","","", err + } + + return token, isUserExist.Role.Name, isUserExist.Name, nil + }else if err != nil && err != gorm.ErrRecordNotFound { + return "ERROR WHILE SEARCHING USERNAME","", "", err } password, err := utils.HashPassword(login.Password) @@ -125,19 +158,11 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err return "error while hasing password: ","", "", err } - isUserExist, err := u.userRepo.GetUserByUsername(login.Username) - - if isUserExist.ID != uuid.Nil { - // Validate the password - if !utils.CheckPasswordHash(login.Password, isUserExist.Password) { - return "","","", errors.New("incorrect password") - } - - return token, role.Name, isUserExist.Name, nil - }else if err != nil && err != gorm.ErrRecordNotFound { - return "ERROR WHILE SEARCHING USERNAME","", "", err - } - + default_role := "Teknisi" + role, err := u.userRepo.GetRoleByDepartment(default_role) + if err != nil { + return "","","", err + } user := entity.User{ ID: uuid.New(), @@ -148,7 +173,6 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err CreatedAt: time.Now(), UpdatedAt: time.Now(), } - err = u.userRepo.Post(user) if err != nil { return "","","", err @@ -166,8 +190,8 @@ func (u *authUsecase) Logout(token string) error { req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") - client := &http.Client{} - resp, err := client.Do(req) + // client := &http.Client{} + resp, err := u.client.Do(req) if err != nil { return err } diff --git a/usecase/backbone_usecase.go b/usecase/backbone_usecase.go index 26f390c..6ba58ae 100644 --- a/usecase/backbone_usecase.go +++ b/usecase/backbone_usecase.go @@ -11,6 +11,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/google/uuid" + "gorm.io/gorm" ) type BackboneUseCase interface { @@ -19,41 +20,107 @@ type BackboneUseCase interface { GetByID(id uuid.UUID) (res.BackboneResponse, error) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackboneDTO) error + } type backboneUseCase struct { backboneRepo repository.BackboneRepo fishboneRepo repository.FishboneRepo validate *validator.Validate + deviceDetailsRepo repository.DeviceDetailsRepo // Add this field } -func NewBackboneUseCase(backboneRepo repository.BackboneRepo, fishboneRepo repository.FishboneRepo) BackboneUseCase { - return &backboneUseCase{ - backboneRepo: backboneRepo, - fishboneRepo: fishboneRepo, - validate: validator.New(), - } +func NewBackboneUseCase(backboneRepo repository.BackboneRepo, fishboneRepo repository.FishboneRepo, deviceDetailsRepo repository.DeviceDetailsRepo) BackboneUseCase { + return &backboneUseCase{ + backboneRepo: backboneRepo, + fishboneRepo: fishboneRepo, + deviceDetailsRepo: deviceDetailsRepo, // Initialize the field + validate: validator.New(), + } } func (u *backboneUseCase) CreateBackbone(backbone req.BackboneDTO) error { - err := u.validate.Struct(backbone) - if err != nil { - return fmt.Errorf("validation error: %w", err) - } + err := u.validate.Struct(backbone) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } - newBackbone := entity.Backbone{ - ID: uuid.New(), - DeviceStartID: backbone.DeviceStartID, - DeviceEndID: backbone.DeviceEndID, - CoreAmount: backbone.CoreAmount, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Lock device records to prevent race conditions + var startDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", backbone.DeviceStartID).First(&startDevice).Error; err != nil { + return fmt.Errorf("start device not found: %w", err) + } + + var endDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", backbone.DeviceEndID).First(&endDevice).Error; err != nil { + return fmt.Errorf("end device not found: %w", err) + } - - return u.backboneRepo.Post(newBackbone) + // Validate device types - both devices must be OTB for backbones + if startDevice.DeviceType != "OTB" { + return fmt.Errorf("start device must be of type OTB, got %s", startDevice.DeviceType) + } + + if endDevice.DeviceType != "OTB" { + return fmt.Errorf("end device must be of type OTB, got %s", endDevice.DeviceType) + } + + // Check port availability with locking + var startDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", backbone.DeviceStartID).First(&startDevicePort).Error; err != nil { + return fmt.Errorf("start device port record not found: %w", err) + } + + var endDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", backbone.DeviceEndID).First(&endDevicePort).Error; err != nil { + return fmt.Errorf("end device port record not found: %w", err) + } + + // Validate port availability - each backbone uses 1 port regardless of core amount + if startDevicePort.PortAvailable < 1 { + return fmt.Errorf("start device has no available ports (available: %d, required: 1)", + startDevicePort.PortAvailable) + } + + if endDevicePort.PortAvailable < 1 { + return fmt.Errorf("end device has no available ports (available: %d, required: 1)", + endDevicePort.PortAvailable) + } + + newBackbone := entity.Backbone{ + ID: uuid.New(), + BackboneCode: backbone.BackboneCode, + DeviceStartID: backbone.DeviceStartID, + DeviceEndID: backbone.DeviceEndID, + CoreAmount: backbone.CoreAmount, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // Create backbone + if err := tx.Create(&newBackbone).Error; err != nil { + return err + } + + // Update port usage for both devices + if err := u.updateDevicePortUsageInTx(tx, backbone.DeviceStartID); err != nil { + return fmt.Errorf("failed to update start device port usage: %w", err) + } + + if err := u.updateDevicePortUsageInTx(tx, backbone.DeviceEndID); err != nil { + return fmt.Errorf("failed to update end device port usage: %w", err) + } + + return nil + }) } + func (u *backboneUseCase) GetAllBackbone() ([]res.BackboneResponse, error) { backbones, err := u.backboneRepo.GetAll() if err != nil { @@ -91,29 +158,142 @@ 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) - if err != nil { - return fmt.Errorf("validation error: %w", err) - } + err := u.validate.Struct(backbone) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } - updates := make(map[string]interface{}) + return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Get original backbone for comparison with lock + var originalBackbone entity.Backbone + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", id).First(&originalBackbone).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("backbone not found") + } + return err + } - if backbone.DeviceStartID != nil { - updates["DeviceStartID"] = backbone.DeviceStartID - } + updates := make(map[string]interface{}) + devicesToUpdate := make(map[uuid.UUID]bool) + devicesToUpdate[originalBackbone.DeviceStartID] = true + devicesToUpdate[originalBackbone.DeviceEndID] = true - if backbone.DeviceEndID != nil { - updates["DeviceEndID"] = backbone.DeviceEndID - } + // Validate device type changes if devices are being changed + if backbone.DeviceStartID != nil && *backbone.DeviceStartID != originalBackbone.DeviceStartID { + var newStartDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *backbone.DeviceStartID).First(&newStartDevice).Error; err != nil { + return fmt.Errorf("new start device not found: %w", err) + } + if newStartDevice.DeviceType != "OTB" { + return fmt.Errorf("new start device must be of type OTB, got %s", newStartDevice.DeviceType) + } + updates["dev_start_id"] = *backbone.DeviceStartID + devicesToUpdate[*backbone.DeviceStartID] = true + } - if backbone.CoreAmount != nil { - updates["CoreAmount"] = backbone.CoreAmount - } + if backbone.DeviceEndID != nil && *backbone.DeviceEndID != originalBackbone.DeviceEndID { + var newEndDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *backbone.DeviceEndID).First(&newEndDevice).Error; err != nil { + return fmt.Errorf("new end device not found: %w", err) + } + if newEndDevice.DeviceType != "OTB" { + return fmt.Errorf("new end device must be of type OTB, got %s", newEndDevice.DeviceType) + } + updates["dev_end_id"] = *backbone.DeviceEndID + devicesToUpdate[*backbone.DeviceEndID] = true + } - updates["UpdatedAt"] = time.Now() + // Handle core amount changes + if backbone.CoreAmount != nil && *backbone.CoreAmount != originalBackbone.CoreAmount { + updates["core_amount"] = *backbone.CoreAmount + } - return u.backboneRepo.Update(id, updates) -} \ No newline at end of file + if len(updates) == 0 { + return fmt.Errorf("no fields to update") + } + + updates["updated_at"] = time.Now() + + // Update backbone + if err := tx.Model(&entity.Backbone{}).Where("id = ?", id).Updates(updates).Error; err != nil { + return err + } + + // Update port usage for all affected devices + for deviceID := range devicesToUpdate { + if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil { + return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err) + } + } + + return nil + }) +} + +func (u *backboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error { + // Get device with lock + 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 + + switch device.DeviceType { + case "OTB": + // For OTB: count backbones (each backbone uses 1 port regardless of core amount) + 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) + customerCount = portUsed // For ODP, customer count equals port_used + } + + portAvailable := device.PortAmount - portUsed + if portAvailable < 0 { + portAvailable = 0 + } + + return tx.Model(&entity.DevicePort{}). + Where("device_id = ?", deviceID). + Updates(map[string]interface{}{ + "port_used": portUsed, + "port_available": portAvailable, + "customer_count": customerCount, + "updated_at": gorm.Expr("NOW()"), + }).Error +} diff --git a/usecase/count_assets_usecase.go b/usecase/count_assets_usecase.go new file mode 100644 index 0000000..6f8882f --- /dev/null +++ b/usecase/count_assets_usecase.go @@ -0,0 +1,31 @@ +package usecase + +import ( + "users_management/m/model/entity" + "users_management/m/repository" +) + +type CountAssetsUseCase interface { + GetCounts() (entity.CountAssets, error) +} + +type countAssetsUseCase struct { + countAssetsRepo repository.CountAssetsRepo +} + +func NewCountAssetsUseCase(countAssetsRepo repository.CountAssetsRepo) CountAssetsUseCase { + return &countAssetsUseCase{ + countAssetsRepo: countAssetsRepo, + } +} + +func (uc *countAssetsUseCase) GetCounts() (entity.CountAssets, error) { + // Update counts before returning + err := uc.countAssetsRepo.UpdateCounts() + if err != nil { + return entity.CountAssets{}, err + } + + // Get the updated counts + return uc.countAssetsRepo.Get() +} \ No newline at end of file diff --git a/usecase/devicePort_usecase.go b/usecase/devicePort_usecase.go index 06cb3a5..c449afd 100644 --- a/usecase/devicePort_usecase.go +++ b/usecase/devicePort_usecase.go @@ -39,7 +39,6 @@ func (u *devicePortUseCase) CreateDevicePort(devicePort req.DevicePort) error { newDevicePort := entity.DevicePort{ ID: uuid.New(), DeviceID: devicePort.DeviceID, - PortNumber: devicePort.PortNumber, CreatedAt: time.Now(), UpdatedAt: time.Now(), } diff --git a/usecase/device_details.go b/usecase/device_details.go new file mode 100644 index 0000000..37b3809 --- /dev/null +++ b/usecase/device_details.go @@ -0,0 +1,297 @@ +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" +) + +type DeviceDetailsUseCase interface { + CreateDeviceDetails(device req.DeviceDetailsDTO) error + GetAllDeviceDetails() ([]res.DeviceDetailsResponse, error) + GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error) + UpdateDeviceDetails(id uuid.UUID, device req.UpdateDeviceDetailsDTO) error + DeleteDeviceDetails(id uuid.UUID) error + + // 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 + 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 +} + +type deviceDetailsUseCase struct { + deviceDetailsRepo repository.DeviceDetailsRepo + geocoder service.GeocodingService + validate *validator.Validate +} + +func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, geocoder service.GeocodingService) DeviceDetailsUseCase { + return &deviceDetailsUseCase{ + deviceDetailsRepo: deviceDetailsRepo, + geocoder: geocoder, + validate: validator.New(), + } +} + +func (u *deviceDetailsUseCase) UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error { + // Validate port number + if update.PortNumber < 1 { + return errors.New("port number must be greater than 0") + } + + return u.deviceDetailsRepo.UpdateCustomerByPort(deviceID, update) +} + +func (u *deviceDetailsUseCase) BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error { + if len(updates) == 0 { + return errors.New("no updates provided") + } + + // Validate all updates + portNumbers := make(map[int]bool) + newCustomerNames := make(map[string]bool) + + for _, update := range updates { + // Check for duplicate port numbers in request + if portNumbers[update.PortNumber] { + return fmt.Errorf("duplicate port number in request: %d", update.PortNumber) + } + portNumbers[update.PortNumber] = true + + // Check for duplicate new customer names in request (ignore nulls) + if update.NewCustomerName != nil && *update.NewCustomerName != "" { + if newCustomerNames[*update.NewCustomerName] { + return fmt.Errorf("duplicate new customer name in request: %s", *update.NewCustomerName) + } + newCustomerNames[*update.NewCustomerName] = true + } + + // Validate port number + if update.PortNumber < 1 { + return fmt.Errorf("port number must be greater than 0: %d", update.PortNumber) + } + } + + return u.deviceDetailsRepo.BulkUpdateCustomersByPort(deviceID, updates) +} + +func (u *deviceDetailsUseCase) RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error { + if portNumber < 1 { + return errors.New("port number must be greater than 0") + } + + return u.deviceDetailsRepo.RemoveCustomerByPort(deviceID, portNumber) +} + +func (u *deviceDetailsUseCase) AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error { + if len(assignments) == 0 { + return errors.New("no assignments provided") + } + + // Validate all assignments first + customerNames := make(map[string]bool) + portNumbers := make(map[int]bool) + + for _, assignment := range assignments { + // Check for duplicate customer names in the request + if customerNames[assignment.CustomerName] { + return fmt.Errorf("duplicate customer name in request: %s", assignment.CustomerName) + } + customerNames[assignment.CustomerName] = true + + // Check for duplicate port numbers in the request + if assignment.PortNumber != nil { + if portNumbers[*assignment.PortNumber] { + return fmt.Errorf("duplicate port number in request: %d", *assignment.PortNumber) + } + portNumbers[*assignment.PortNumber] = true + } + } + + return u.deviceDetailsRepo.AssignMultipleCustomersToPort(deviceID, assignments) +} + + +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 { + return fmt.Errorf("validation error: %w", err) + } + + newDevice := entity.Device{ + ID: uuid.New(), + DeviceCode: deviceDTO.DeviceCode, + DeviceType: entity.DeviceType(deviceDTO.DeviceType), + Longitude: deviceDTO.Longitude, + Latitude: deviceDTO.Latitude, + PortAmount: deviceDTO.PortAmount, + Status: entity.DeviceStatus(deviceDTO.Status), + Province: deviceDTO.Province, + City: deviceDTO.City, + District: deviceDTO.District, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.deviceDetailsRepo.Create(newDevice) +} + +func (u *deviceDetailsUseCase) GetAllDeviceDetails() ([]res.DeviceDetailsResponse, error) { + devices, err := u.deviceDetailsRepo.GetAll() + if err != nil { + return nil, err + } + + return helper.ConvertToDeviceDetailsResponses(devices, u.geocoder) +} + +func (u *deviceDetailsUseCase) GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error) { + device, err := u.deviceDetailsRepo.GetByID(id) + if err != nil { + return res.DeviceDetailsResponse{}, err + } + + return helper.ConvertToDeviceDetailsResponse(device, u.geocoder) +} + +func (u *deviceDetailsUseCase) UpdateDeviceDetails(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO) error { + err := u.validate.Struct(deviceDTO) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } + + updates := map[string]interface{}{} + + if deviceDTO.DeviceCode != nil { + updates["device_code"] = *deviceDTO.DeviceCode + } + if deviceDTO.DeviceType != nil { + updates["device_type"] = *deviceDTO.DeviceType + } + if deviceDTO.Longitude != nil { + updates["longitude"] = *deviceDTO.Longitude + } + if deviceDTO.Latitude != nil { + updates["latitude"] = *deviceDTO.Latitude + } + if deviceDTO.PortAmount != nil { + // Validate port amount change + if *deviceDTO.PortAmount < 0 { + return fmt.Errorf("port amount cannot be negative") + } + + // If not setting to 0, check current usage + if *deviceDTO.PortAmount > 0 { + currentUsed, _, err := u.deviceDetailsRepo.GetPortUsageByDevice(id) + if err != nil { + return err + } + if *deviceDTO.PortAmount < currentUsed { + return fmt.Errorf("cannot reduce port amount to %d, currently using %d ports", *deviceDTO.PortAmount, currentUsed) + } + } + + updates["port_amount"] = *deviceDTO.PortAmount + } + if deviceDTO.Status != nil { + updates["status"] = *deviceDTO.Status + } + if deviceDTO.Region != nil { + updates["region"] = *deviceDTO.Region + } + if deviceDTO.Province != nil { + updates["province"] = *deviceDTO.Province + } + if deviceDTO.City != nil { + updates["city"] = *deviceDTO.City + } + if deviceDTO.District != nil { + updates["district"] = *deviceDTO.District + } + + if len(updates) == 0 { + return errors.New("no fields to update") + } + + updates["updated_at"] = time.Now() + + return u.deviceDetailsRepo.Update(id, updates) +} + +func (u *deviceDetailsUseCase) DeleteDeviceDetails(id uuid.UUID) error { + // Check if device has connections + backbones, err := u.deviceDetailsRepo.GetBackbonesByDeviceID(id) + if err != nil { + return err + } + if len(backbones) > 0 { + return errors.New("cannot delete device with active backbone connections") + } + + fishbones, err := u.deviceDetailsRepo.GetFishbonesByDeviceID(id) + if err != nil { + return err + } + if len(fishbones) > 0 { + return errors.New("cannot delete device with active fishbone connections") + } + + towers, err := u.deviceDetailsRepo.GetTowersByDeviceID(id) + if err != nil { + return err + } + if len(towers) > 0 { + return errors.New("cannot delete device with active tower connections") + } + + return u.deviceDetailsRepo.Delete(id) +} + + +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) +} \ No newline at end of file diff --git a/usecase/device_inspection.go b/usecase/device_inspection.go new file mode 100644 index 0000000..38d122a --- /dev/null +++ b/usecase/device_inspection.go @@ -0,0 +1,414 @@ +package usecase + +import ( + "errors" + "fmt" + "log" + "strconv" + "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 DeviceInspectionUseCase interface { + CreateInspection(userID uuid.UUID, inspection req.DeviceInspectionDTO) error + GetAllInspections(page, limit int) ([]res.DeviceInspectionResponse, int64, error) + GetUserInspections(userID uuid.UUID, page, limit int) ([]res.DeviceInspectionResponse, int64, error) + GetInspectionByID(id uuid.UUID) (res.DeviceInspectionDetailResponse, error) + + + UpdateInspection(id, userID uuid.UUID, inspection req.UpdateDeviceInspectionDTO, userRole string) error + ApproveInspection(id uuid.UUID, approval req.ApproveInspectionDTO) error + CheckInspectionOwnership(inspectionID, userID uuid.UUID) (bool, error) // Add this line +} + +type deviceInspectionUseCase struct { + inspectionRepo repository.DeviceInspectionRepo + activityLogUC ActivityLogUseCase + geoService service.GeocodingService + validate *validator.Validate +} + +func NewDeviceInspectionUseCase(inspectionRepo repository.DeviceInspectionRepo, activityLogUC ActivityLogUseCase, geoService service.GeocodingService) DeviceInspectionUseCase { + return &deviceInspectionUseCase{ + inspectionRepo: inspectionRepo, + activityLogUC: activityLogUC, + geoService: geoService, + validate: validator.New(), + } +} + + +func (u *deviceInspectionUseCase) CheckInspectionOwnership(inspectionID, userID uuid.UUID) (bool, error) { + return u.inspectionRepo.CheckOwnership(inspectionID, userID) +} + +func (u *deviceInspectionUseCase) CreateInspection(userID uuid.UUID, inspection req.DeviceInspectionDTO) error { + err := u.validate.Struct(inspection) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } + + // Check if device exists + deviceExists, err := u.inspectionRepo.CheckDeviceExists(inspection.DeviceID) + if err != nil { + return err + } + if !deviceExists { + return errors.New("device not found") + } + + // Get device details + device, err := u.inspectionRepo.GetDeviceByID(inspection.DeviceID) + if err != nil { + return err + } + + // Validate device type constraints + if device.DeviceType == "OTB" && inspection.BackboneID == nil { + return errors.New("backbone is required for OTB device type") + } + if device.DeviceType == "ODP" && inspection.FishboneID == nil { + return errors.New("fishbone is required for ODP device type") + } + + // Validate backbone exists if provided + if inspection.BackboneID != nil { + backboneExists, err := u.inspectionRepo.CheckBackboneExists(*inspection.BackboneID) + if err != nil { + return err + } + if !backboneExists { + return errors.New("backbone not found") + } + } + + // Validate fishbone exists if provided + if inspection.FishboneID != nil { + fishboneExists, err := u.inspectionRepo.CheckFishboneExists(*inspection.FishboneID) + if err != nil { + return err + } + if !fishboneExists { + return errors.New("fishbone not found") + } + } + + // Validate tower exists if provided + if inspection.TowerID != nil { + towerExists, err := u.inspectionRepo.CheckTowerExists(*inspection.TowerID) + if err != nil { + return err + } + if !towerExists { + return errors.New("tower not found") + } + } + + // Check port availability + portUsedInt, err := strconv.Atoi(inspection.PortUsed) + if err != nil { + return errors.New("invalid port_used value") + } + + // Get current device port usage + devicePort, err := u.inspectionRepo.GetDevicePortByDeviceID(inspection.DeviceID) + if err != nil { + // If device port doesn't exist, create it + return fmt.Errorf("device port not found for device %s: %w", inspection.DeviceID, err) + } + // Check if adding this inspection would exceed port capacity + newPortUsed := devicePort.PortUsed + portUsedInt + if newPortUsed > device.PortAmount { + return fmt.Errorf("insufficient ports available. Device has %d ports, currently %d used, requested %d additional", + device.PortAmount, devicePort.PortUsed, portUsedInt) + } + + // Calculate cable amount based on device type + cableAmount, err := u.inspectionRepo.CountCablesByDevice(inspection.DeviceID, string(device.DeviceType)) + if err != nil { + return err + } + + // Calculate port available + portAvailable := device.PortAmount - newPortUsed + + // Get address from coordinates using OpenStreetMap + longitude := inspection.InspectionPlacement.Longitude + latitude := inspection.InspectionPlacement.Latitude + + address, err := u.geoService.GetAddressFromCoordinates(latitude, longitude) + if err != nil { + log.Printf("Error getting address from coordinates (%.6f, %.6f): %v", latitude, longitude, err) + address = fmt.Sprintf("Coordinates: %.6f, %.6f", latitude, longitude) // Fallback to coordinates + } + + // Create inspection + newInspection := entity.DeviceInspection{ + ID: uuid.New(), + DeviceID: inspection.DeviceID, + BackboneID: inspection.BackboneID, + FishboneID: inspection.FishboneID, + TowerID: inspection.TowerID, + UserID: userID, + Status: inspection.Status, + PortUsed: inspection.PortUsed, + PortAvailable: strconv.Itoa(portAvailable), + CableAmount: cableAmount, + Description: inspection.Description, + ImageURL: inspection.ImageURL, + InspectionPlacement: address, // Store the full address from OpenStreetMap + Longitude: longitude, // Store longitude + Latitude: latitude, // Store latitude + InspectionApproval: "pending", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + err = u.inspectionRepo.Create(newInspection) + if err != nil { + return err + } + + // Log activity + inspectionIDStr := newInspection.ID.String() + u.activityLogUC.LogActivity( + userID, + "CREATE", + "device_inspection", + &inspectionIDStr, + nil, + fmt.Sprintf("Created device inspection for device %s at %s", device.DeviceCode, address), + "", + "", + "", + ) + + return nil +} + +func (u *deviceInspectionUseCase) UpdateInspection(id, userID uuid.UUID, inspection req.UpdateDeviceInspectionDTO, userRole string) error { + err := u.validate.Struct(inspection) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } + + // Get existing inspection + existingInspection, err := u.inspectionRepo.GetByID(id) + if err != nil { + return errors.New("inspection not found") + } + + // Check if user owns this inspection (for teknisi) + if userRole == "Teknisi" && existingInspection.UserID != userID { + return errors.New("unauthorized: you can only update your own inspections") + } + + updates := make(map[string]interface{}) + + if inspection.BackboneID != nil { + backboneExists, err := u.inspectionRepo.CheckBackboneExists(*inspection.BackboneID) + if err != nil { + return err + } + if !backboneExists { + return errors.New("backbone not found") + } + updates["backbone_id"] = *inspection.BackboneID + } + + if inspection.FishboneID != nil { + fishboneExists, err := u.inspectionRepo.CheckFishboneExists(*inspection.FishboneID) + if err != nil { + return err + } + if !fishboneExists { + return errors.New("fishbone not found") + } + updates["fishbone_id"] = *inspection.FishboneID + } + + if inspection.TowerID != nil { + towerExists, err := u.inspectionRepo.CheckTowerExists(*inspection.TowerID) + if err != nil { + return err + } + if !towerExists { + return errors.New("tower not found") + } + updates["tower_id"] = *inspection.TowerID + } + + if inspection.Status != nil { + updates["status"] = *inspection.Status + } + + if inspection.PortUsed != nil { + updates["port_used"] = *inspection.PortUsed + } + + if inspection.Description != nil { + updates["description"] = *inspection.Description + } + + if inspection.ImageURL != nil { + updates["image_url"] = *inspection.ImageURL + } + + // Handle location update + if inspection.InspectionPlacement != nil { + longitude := inspection.InspectionPlacement.Longitude + latitude := inspection.InspectionPlacement.Latitude + + // Get new address from coordinates + address, err := u.geoService.GetAddressFromCoordinates(latitude, longitude) + if err != nil { + log.Printf("Error getting address from coordinates (%.6f, %.6f): %v", latitude, longitude, err) + address = fmt.Sprintf("Coordinates: %.6f, %.6f", latitude, longitude) // Fallback to coordinates + } + + updates["inspection_placement"] = address + updates["longitude"] = longitude + updates["latitude"] = latitude + } + + if len(updates) == 0 { + return errors.New("no fields to update") + } + + updates["updated_at"] = time.Now() + + return u.inspectionRepo.Update(id, updates) +} + +// Keep the rest of the methods unchanged... +func (u *deviceInspectionUseCase) GetAllInspections(page, limit int) ([]res.DeviceInspectionResponse, int64, error) { + offset := (page - 1) * limit + + inspections, err := u.inspectionRepo.GetAll() + if err != nil { + return nil, 0, err + } + + total, err := u.inspectionRepo.CountAll() + if err != nil { + return nil, 0, err + } + + // Apply pagination manually since we need all data for processing + start := offset + end := offset + limit + if start > len(inspections) { + start = len(inspections) + } + if end > len(inspections) { + end = len(inspections) + } + + paginatedInspections := inspections[start:end] + responses := helper.ConvertToDeviceInspectionResponses(paginatedInspections) + + return responses, total, nil +} + +func (u *deviceInspectionUseCase) GetUserInspections(userID uuid.UUID, page, limit int) ([]res.DeviceInspectionResponse, int64, error) { + offset := (page - 1) * limit + + inspections, err := u.inspectionRepo.GetByUserID(userID, limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := u.inspectionRepo.CountByUserID(userID) + if err != nil { + return nil, 0, err + } + + responses := helper.ConvertToDeviceInspectionResponses(inspections) + + return responses, total, nil +} + +func (u *deviceInspectionUseCase) GetInspectionByID(id uuid.UUID) (res.DeviceInspectionDetailResponse, error) { + inspection, err := u.inspectionRepo.GetByID(id) + if err != nil { + return res.DeviceInspectionDetailResponse{}, err + } + + response := helper.ConvertToDeviceInspectionDetailResponse(inspection) + return response, nil +} + +func (u *deviceInspectionUseCase) ApproveInspection(id uuid.UUID, approval req.ApproveInspectionDTO) error { + err := u.validate.Struct(approval) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } + + // Get inspection details + inspection, err := u.inspectionRepo.GetByID(id) + if err != nil { + return errors.New("inspection not found") + } + + updates := map[string]interface{}{ + "inspection_approval": approval.InspectionApproval, + "updated_at": time.Now(), + } + + // Update inspection + err = u.inspectionRepo.Update(id, updates) + if err != nil { + return err + } + + // If approved, update device port and device status + if approval.InspectionApproval == "approved" { + // Update device status + err = u.inspectionRepo.UpdateDeviceStatus(inspection.DeviceID, inspection.Status) + if err != nil { + return err + } + + // Update device port usage + portUsedInt, _ := strconv.Atoi(inspection.PortUsed) + + devicePort, err := u.inspectionRepo.GetDevicePortByDeviceID(inspection.DeviceID) + if err != nil { + return err + } + + device, err := u.inspectionRepo.GetDeviceByID(inspection.DeviceID) + if err != nil { + return err + } + + newPortUsed := devicePort.PortUsed + portUsedInt + newPortAvailable := device.PortAmount - newPortUsed + + portUpdates := map[string]interface{}{ + "port_used": newPortUsed, + "port_available": newPortAvailable, + "updated_at": time.Now(), + } + + // For OTB devices, also update backbone_id + if device.DeviceType == "OTB" && inspection.BackboneID != nil { + portUpdates["backbone_id"] = *inspection.BackboneID + } + + err = u.inspectionRepo.UpdateDevicePort(inspection.DeviceID, portUpdates) + if err != nil { + return err + } + } + + return nil +} \ No newline at end of file diff --git a/usecase/device_usecase.go b/usecase/device_usecase.go index 4420c10..63c7d0c 100644 --- a/usecase/device_usecase.go +++ b/usecase/device_usecase.go @@ -43,18 +43,26 @@ func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error { return fmt.Errorf("validation error: %w", err) } - newDevice := entity.Device{ - ID: uuid.New(), - DeviceCode: device.DeviceCode, - DeviceType: entity.DeviceType(device.DeviceType), - Longitude: device.Longitude, - Latitude: device.Latitude, - PortAmount: device.PortAmount, - Status: entity.DeviceStatus(device.Status), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + if device.DeviceType == "OTB" || device.DeviceType == "ODP" && device.PortAmount <= 0 { + return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices") } + newDevice := entity.Device{ + ID: uuid.New(), + DeviceCode: device.DeviceCode, + DeviceType: entity.DeviceType(device.DeviceType), + Longitude: device.Longitude, + Latitude: device.Latitude, + PortAmount: device.PortAmount, + Status: entity.DeviceStatus(device.Status), + Province: device.Province, // Now nullable + City: device.City, // Now nullable + District: device.District, // Now nullable + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.deviceRepo.Post(newDevice) } @@ -88,7 +96,6 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e if err != nil { return fmt.Errorf("validation error: %w", err) } - updates := map[string]interface{}{} if device.DeviceCode != nil { @@ -106,6 +113,21 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e if device.PortAmount != nil { updates["PortAmount"] = *device.PortAmount } + if device.Status != nil { + updates["Status"] = *device.Status + } + if device.Province != nil { + updates["Province"] = *device.Province + } + if device.City != nil { + updates["City"] = *device.City + } + if device.District != nil { + updates["District"] = *device.District + } + if device.DeviceType != nil && (*device.DeviceType == "OTB" || *device.DeviceType == "ODP") && device.PortAmount != nil && *device.PortAmount <= 0 { + return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices") + } if len(updates) == 0 { return fmt.Errorf("no update data") @@ -113,6 +135,8 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e updates["UpdatedAt"] = time.Now() + + return u.deviceRepo.Update(id, updates) } diff --git a/usecase/fishbone_usecase.go b/usecase/fishbone_usecase.go index de4e146..3745040 100644 --- a/usecase/fishbone_usecase.go +++ b/usecase/fishbone_usecase.go @@ -2,96 +2,317 @@ 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" "github.com/go-playground/validator/v10" "github.com/google/uuid" + "gorm.io/gorm" ) type FishboneUseCase interface { - CreateFishbone(fishbone req.FishboneDTO) error - GetAllFishbone() ([]entity.Fishbone, error) - GetByID(id uuid.UUID) (entity.Fishbone, error) - - UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error + CreateFishbone(fishbone req.FishboneDTO) error + GetAllFishbone() ([]res.FishboneResponse, error) + GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) + 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 { - fishboneRepo repository.FishboneRepo - validate *validator.Validate +type fishboneUseCase struct { + fishboneRepo repository.FishboneRepo + backboneRepo repository.BackboneRepo + deviceDetailsRepo repository.DeviceDetailsRepo // Add this field + validate *validator.Validate } -func NewFishboneUseCase(fishboneRepo repository.FishboneRepo) FishboneUseCase { - return &fishboneUsecase{ - fishboneRepo: fishboneRepo, - validate: validator.New(), - } +func NewFishboneUseCase(fishboneRepo repository.FishboneRepo, backboneRepo repository.BackboneRepo, deviceDetailsRepo repository.DeviceDetailsRepo) FishboneUseCase { + return &fishboneUseCase{ + fishboneRepo: fishboneRepo, + backboneRepo: backboneRepo, + deviceDetailsRepo: deviceDetailsRepo, // Initialize the field + validate: validator.New(), + } } -func (u *fishboneUsecase) CreateFishbone(fishbone req.FishboneDTO) error { - err := u.validate.Struct(fishbone) - if err != nil { - return err - } +func (u *fishboneUseCase) GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error) { + fishbones, err := u.fishboneRepo.GetByBackboneID(backboneID) + if err != nil { + return nil, err + } - newFishbone := entity.Fishbone{ - ID: uuid.New(), - BackboneID: fishbone.BackboneID, - DeviceStartID: fishbone.DeviceStartID, - DeviceEndID: fishbone.DeviceEndID, - CoreAmount: fishbone.CoreAmount, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + fishboneResp, err := helper.ConvertToFishboneResponses(fishbones, nil) + if err != nil { + return nil, err + } - return u.fishboneRepo.Post(newFishbone) + return fishboneResp, nil } -func (u *fishboneUsecase) GetAllFishbone() ([]entity.Fishbone, error) { - fishbones, err := u.fishboneRepo.GetAll() - if err != nil { - return fishbones, err - } - return fishbones, nil + + +func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { + err := u.validate.Struct(fishbone) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } + + return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Lock device records to prevent race conditions + var startDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", fishbone.DeviceStartID).First(&startDevice).Error; err != nil { + return fmt.Errorf("start device not found: %w", err) + } + + var endDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", fishbone.DeviceEndID).First(&endDevice).Error; err != nil { + return fmt.Errorf("end device not found: %w", err) + } + + // Validate device types + if startDevice.DeviceType != "closure" { + return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType) + } + + if endDevice.DeviceType != "ODP" { + return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType) + } + + // Check port availability with locking + var startDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", fishbone.DeviceStartID).First(&startDevicePort).Error; err != nil { + return fmt.Errorf("start device port record not found: %w", err) + } + + var endDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", fishbone.DeviceEndID).First(&endDevicePort).Error; err != nil { + return fmt.Errorf("end device port record not found: %w", err) + } + + // Validate port availability + if startDevicePort.PortAvailable < 1 { + return fmt.Errorf("start device has no available ports (available: %d, required: 1)", + startDevicePort.PortAvailable) + } + + if endDevicePort.PortAvailable < fishbone.CoreAmount { + return fmt.Errorf("end device has insufficient available ports (available: %d, required: %d)", + endDevicePort.PortAvailable, fishbone.CoreAmount) + } + + newFishbone := entity.Fishbone{ + ID: uuid.New(), + FishboneCode: fishbone.FishboneCode, + BackboneID: fishbone.BackboneID, + DeviceStartID: fishbone.DeviceStartID, + DeviceEndID: fishbone.DeviceEndID, + CoreAmount: fishbone.CoreAmount, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // Create fishbone + if err := tx.Create(&newFishbone).Error; err != nil { + return err + } + + // Update port usage for both devices + if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceStartID); err != nil { + return fmt.Errorf("failed to update start device port usage: %w", err) + } + + if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceEndID); err != nil { + return fmt.Errorf("failed to update end device port usage: %w", err) + } + + return nil + }) } -func (u *fishboneUsecase) GetByID(id uuid.UUID) (entity.Fishbone, error) { - fishbone, err := u.fishboneRepo.GetByID(id) - if err != nil { - return fishbone, err - } - return fishbone, nil +func (u *fishboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error { + // Get device with lock + 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 + + switch device.DeviceType { + case "closure": + 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 + + case "ODP": + 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) + customerCount = portUsed // For ODP, customer count equals port_used + } + + portAvailable := device.PortAmount - portUsed + if portAvailable < 0 { + portAvailable = 0 + } + + return tx.Model(&entity.DevicePort{}). + Where("device_id = ?", deviceID). + Updates(map[string]interface{}{ + "port_used": portUsed, + "port_available": portAvailable, + "customer_count": customerCount, + "updated_at": gorm.Expr("NOW()"), + }).Error } -func (u *fishboneUsecase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error { - err := u.validate.Struct(fishbone) - if err != nil { - return err - } +func (u *fishboneUseCase) GetAllFishbone() ([]res.FishboneResponse, error) { + fishbones, err := u.fishboneRepo.GetAll() + if err != nil { + return nil, err + } - updates := make(map[string]interface{}) - - if fishbone.BackboneID != nil { - updates["BackboneID"] = *fishbone.BackboneID - } - if fishbone.DeviceStartID != nil { - updates["DeviceStartID"] = *fishbone.DeviceStartID - } - if fishbone.DeviceEndID != nil { - updates["DeviceEndID"] = *fishbone.DeviceEndID - } - if fishbone.CoreAmount != nil { - updates["CoreAmount"] = *fishbone.CoreAmount - } - if len(updates) == 0 { - return errors.New("no fields to update") - } - - updates["UpdatedAt"] = time.Now() - - return u.fishboneRepo.Update(id, updates) + return helper.ConvertToSimpleFishboneResponses(fishbones), nil } +func (u *fishboneUseCase) GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) { + fishbone, err := u.fishboneRepo.GetByIDWithRelations(id) + if err != nil { + return res.FishboneDetailResponse{}, err + } + + // Convert to detailed response using helper + return helper.ConvertToFishboneDetailResponse(fishbone), nil +} + +func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error { + err := u.validate.Struct(fishbone) + if err != nil { + return fmt.Errorf("validation error: %w", err) + } + + return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Get original fishbone for comparison with lock + var originalFishbone entity.Fishbone + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", id).First(&originalFishbone).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("fishbone not found") + } + return err + } + + updates := make(map[string]interface{}) + devicesToUpdate := make(map[uuid.UUID]bool) + devicesToUpdate[originalFishbone.DeviceStartID] = true + devicesToUpdate[originalFishbone.DeviceEndID] = true + + // Validate device type changes if devices are being changed + if fishbone.DeviceStartID != nil && *fishbone.DeviceStartID != originalFishbone.DeviceStartID { + var newStartDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *fishbone.DeviceStartID).First(&newStartDevice).Error; err != nil { + return fmt.Errorf("new start device not found: %w", err) + } + if newStartDevice.DeviceType != "closure" { + return fmt.Errorf("new start device must be of type closure, got %s", newStartDevice.DeviceType) + } + updates["dev_start_id"] = *fishbone.DeviceStartID + devicesToUpdate[*fishbone.DeviceStartID] = true + } + + if fishbone.DeviceEndID != nil && *fishbone.DeviceEndID != originalFishbone.DeviceEndID { + var newEndDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *fishbone.DeviceEndID).First(&newEndDevice).Error; err != nil { + return fmt.Errorf("new end device not found: %w", err) + } + if newEndDevice.DeviceType != "ODP" { + return fmt.Errorf("new end device must be of type ODP, got %s", newEndDevice.DeviceType) + } + updates["dev_end_id"] = *fishbone.DeviceEndID + devicesToUpdate[*fishbone.DeviceEndID] = true + } + + // Handle core amount changes + if fishbone.CoreAmount != nil && *fishbone.CoreAmount != originalFishbone.CoreAmount { + updates["core_amount"] = *fishbone.CoreAmount + } + + if fishbone.BackboneID != nil { + // Validate backbone exists + var backbone entity.Backbone + if err := tx.Where("id = ?", *fishbone.BackboneID).First(&backbone).Error; err != nil { + return fmt.Errorf("backbone not found: %w", err) + } + updates["bb_id"] = *fishbone.BackboneID + } + + if fishbone.FishboneCode != nil { + updates["fishbone_code"] = *fishbone.FishboneCode + } + + if len(updates) == 0 { + return fmt.Errorf("no fields to update") + } + + updates["updated_at"] = time.Now() + + // Update fishbone + if err := tx.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error; err != nil { + return err + } + + // Update port usage for all affected devices + for deviceID := range devicesToUpdate { + if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil { + return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err) + } + } + + return nil + }) +} +func (u *fishboneUseCase) DeleteFishbone(id uuid.UUID) error { + // Check if fishbone exists + exists, err := u.fishboneRepo.CheckFishboneExists(id) + if err != nil { + return err + } + if !exists { + return errors.New("fishbone not found") + } + + return u.fishboneRepo.Delete(id) +} + +func (u *fishboneUseCase) GetFishboneStats() (map[string]interface{}, error) { + fishbones, err := u.fishboneRepo.GetAll() + if err != nil { + return nil, err + } + + return helper.GetFishboneStats(fishbones), nil +} \ No newline at end of file diff --git a/usecase/nearest_device.go b/usecase/nearest_device.go new file mode 100644 index 0000000..504398d --- /dev/null +++ b/usecase/nearest_device.go @@ -0,0 +1,106 @@ +package usecase + +import ( + "math" + "users_management/m/model/dto/req" + "users_management/m/model/dto/res" + "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 NearestDeviceUseCase interface { + GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error) + GetNearestDeviceByID(id uuid.UUID, userLat, userLng float64) (res.NearestDeviceDetailResponse, error) +} + +type nearestDeviceUseCase struct { + nearestDeviceRepo repository.NearestDeviceRepo + geocoder service.GeocodingService + validate *validator.Validate +} + +func NewNearestDeviceUseCase(nearestDeviceRepo repository.NearestDeviceRepo, geocoder service.GeocodingService) NearestDeviceUseCase { + return &nearestDeviceUseCase{ + nearestDeviceRepo: nearestDeviceRepo, + geocoder: geocoder, + validate: validator.New(), + } +} + +func (u *nearestDeviceUseCase) GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error) { + err := u.validate.Struct(request) + if err != nil { + return nil, err + } + + // Set defaults + radius := request.Radius + if radius == 0 { + radius = 5.0 // Default 5km + } + + limit := request.Limit + if limit == 0 { + limit = 10 // Default 10 devices + } + + devices, err := u.nearestDeviceRepo.GetNearestDevices( + request.Longitude, + request.Latitude, + radius, + limit, + request.Province, + request.City, + request.District, + ) + if err != nil { + return nil, err + } + + // Updated function call - removed userLat, userLng parameters since distance is already calculated + responses, err := helper.ConvertToNearestDeviceResponses(devices, u.nearestDeviceRepo, u.geocoder) + if err != nil { + return nil, err + } + + return responses, nil +} + +func (u *nearestDeviceUseCase) GetNearestDeviceByID(id uuid.UUID, userLat, userLng float64) (res.NearestDeviceDetailResponse, error) { + device, err := u.nearestDeviceRepo.GetDeviceByIDWithConnections(id) + if err != nil { + return res.NearestDeviceDetailResponse{}, err + } + + // Calculate distance + distance := calculateDistance(userLat, userLng, device.Latitude, device.Longitude) + + response, err := helper.ConvertToNearestDeviceDetailResponse(device, distance, u.nearestDeviceRepo, u.geocoder) + if err != nil { + return res.NearestDeviceDetailResponse{}, err + } + + return response, nil +} + +// calculateDistance calculates the distance between two coordinates using Haversine formula +func calculateDistance(lat1, lng1, lat2, lng2 float64) float64 { + const earthRadius = 6371 // Earth's radius in kilometers + + lat1Rad := lat1 * math.Pi / 180 + lng1Rad := lng1 * math.Pi / 180 + lat2Rad := lat2 * math.Pi / 180 + lng2Rad := lng2 * math.Pi / 180 + + dlat := lat2Rad - lat1Rad + dlng := lng2Rad - lng1Rad + + a := math.Sin(dlat/2)*math.Sin(dlat/2) + math.Cos(lat1Rad)*math.Cos(lat2Rad)*math.Sin(dlng/2)*math.Sin(dlng/2) + c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) + + return earthRadius * c +} \ No newline at end of file diff --git a/usecase/tower_usecase.go b/usecase/tower_usecase.go index 86456b5..3601a22 100644 --- a/usecase/tower_usecase.go +++ b/usecase/tower_usecase.go @@ -2,6 +2,8 @@ package usecase import ( "errors" + "fmt" + "mime/multipart" "time" "users_management/m/model/dto/req" "users_management/m/model/dto/res" @@ -15,10 +17,10 @@ import ( ) type TowerUseCase interface { - Post(tower req.TowerDTO) error + Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error GetAll() ([]res.TowerResponse, error) GetByID(id uuid.UUID) (res.TowerResponse, error) - UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error + UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) error } type towerUsecase struct { @@ -35,23 +37,49 @@ func NewTowerUseCase(towerRepo repository.TowerRepo, geocoder service.GeocodingS } } -func (u *towerUsecase) Post(tower req.TowerDTO) error { - err := u.validate.Struct(tower) - if err != nil { - return err - } +func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error { + err := u.validate.Struct(tower) + if err != nil { + return err + } - newTower := entity.Tower{ - ID: uuid.New(), - DeviceID: tower.DeviceID, - TowerCode: tower.TowerCode, - Longitude: tower.Longitude, - Latitude: tower.Latitude, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + // Validate that if it's not an external tower, DeviceID must be provided + if tower.ExternalTower != nil && !*tower.ExternalTower && tower.DeviceID == nil { + return fmt.Errorf("device_id is required for internal towers") + } - return u.towerRepo.Post(newTower) + // Validate that if DeviceID is provided, the device exists + if tower.DeviceID != nil { + deviceExists, err := u.towerRepo.CheckDeviceExists(*tower.DeviceID) + if err != nil { + return err + } + if !deviceExists { + return fmt.Errorf("device not found") + } + } + + var imageURL string + if imageFile != nil { + imageURL, err = helper.SaveTowerImage(imageFile) + if err != nil { + return err + } + } + + newTower := entity.Tower{ + ID: uuid.New(), + DeviceID: tower.DeviceID, // Now nullable + TowerCode: tower.TowerCode, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + ImageURL: imageURL, + ExternalTower: tower.ExternalTower, // Now nullable + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.towerRepo.Post(newTower) } func (u *towerUsecase) GetAll() ([]res.TowerResponse, error) { @@ -80,7 +108,7 @@ func (u *towerUsecase) GetByID(id uuid.UUID) (res.TowerResponse, error) { return towerResp, nil } -func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error { +func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) error { err := u.validate.Struct(tower) if err != nil { return err // Return validation error @@ -99,6 +127,26 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error if tower.Latitude != nil { updates["Latitude"] = *tower.Latitude } + if tower.ExternalTower != nil { + updates["ExternalTower"] = *tower.ExternalTower + } + + // Handle image upload + if imageFile != nil { + // Get current tower to delete old image + currentTower, err := u.towerRepo.GetByID(id) + if err == nil && currentTower.ImageURL != "" { + // Delete old image + helper.DeleteTowerImage(currentTower.ImageURL) + } + + // Save new image + imageURL, err := helper.SaveTowerImage(imageFile) + if err != nil { + return err + } + updates["ImageURL"] = imageURL + } // If no fields are updated, return an error if len(updates) == 0 { @@ -108,4 +156,4 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error updates["UpdatedAt"] = time.Now() return u.towerRepo.Update(id, updates) -} \ No newline at end of file +} diff --git a/usecase/users_usecase.go b/usecase/users_usecase.go index 19ec93f..608406b 100644 --- a/usecase/users_usecase.go +++ b/usecase/users_usecase.go @@ -1,30 +1,33 @@ package usecase import ( - "users_management/m/model/entity" - "users_management/m/repository" + "time" + "users_management/m/model/entity" + "users_management/m/repository" - "github.com/google/uuid" + "github.com/google/uuid" ) type UsersUsecase interface { - GetRoleByDepartment(departmentName string) (uuid.UUID, error) - GetUserByUsername(username string) (entity.User, error) + GetRoleByDepartment(departmentName string) (uuid.UUID, error) + GetUserByUsername(username string) (entity.User, error) + GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this + CreateUserFromExternal(nomorInduk, name, roleName string) error // Add this + UpdateUserRole(nomorInduk, roleName string) error // Add this + GetAllUsers() ([]entity.User, error) // Add this } type usersUsecase struct { - userRepo repository.UsersRepo + userRepo repository.UsersRepo } func NewUsersUsecase(userRepo repository.UsersRepo) UsersUsecase { - return &usersUsecase{ - userRepo: userRepo, - } + return &usersUsecase{ + userRepo: userRepo, + } } - func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, error) { - role, err := u.userRepo.GetRoleByDepartment(departmentName) if err != nil { return uuid.Nil, err @@ -33,10 +36,49 @@ func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, er } func (u *usersUsecase) GetUserByUsername(username string) (entity.User, error) { - users, err := u.userRepo.GetUserByUsername(username) - if err != nil { - return users, err - } - return users, nil + users, err := u.userRepo.GetUserByUsername(username) + if err != nil { + return users, err + } + return users, nil } +// New methods for RBAC +func (u *usersUsecase) GetUserByNomorInduk(nomorInduk string) (entity.User, error) { + return u.userRepo.GetUserByNomorInduk(nomorInduk) +} + +func (u *usersUsecase) CreateUserFromExternal(nomorInduk, name, roleName string) error { + // Get role ID + roleID, err := u.GetRoleByDepartment(roleName) + if err != nil { + return err + } + + // Create user + user := entity.User{ + ID: uuid.New(), + NomorInduk: &nomorInduk, + RoleID: roleID, + Name: name, + Username: nomorInduk, // Use nomor_induk as username + Password: "", // External auth, no local password needed + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.userRepo.CreateUserFromExternal(user) +} + +func (u *usersUsecase) UpdateUserRole(nomorInduk, roleName string) error { + roleID, err := u.GetRoleByDepartment(roleName) + if err != nil { + return err + } + + return u.userRepo.UpdateUserRole(nomorInduk, roleID) +} + +func (u *usersUsecase) GetAllUsers() ([]entity.User, error) { + return u.userRepo.GetAllUsers() +} \ No newline at end of file diff --git a/utils/helper/activity_logs_helper.go b/utils/helper/activity_logs_helper.go new file mode 100644 index 0000000..66f0d50 --- /dev/null +++ b/utils/helper/activity_logs_helper.go @@ -0,0 +1,84 @@ +package helper + +import ( + "users_management/m/model/dto/res" + "users_management/m/model/entity" +) + +// ConvertToActivityLogResponses converts activity log entities to simple response format for users +func ConvertToActivityLogResponses(logs []entity.ActivityLog) []res.ActivityLogResponse { + var responses []res.ActivityLogResponse + + for _, log := range logs { + response := res.ActivityLogResponse{ + ID: log.ID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } + responses = append(responses, response) + } + + return responses +} + +// ConvertToActivityLogDetailResponses converts activity log entities to detailed response format for admins +func ConvertToActivityLogDetailResponses(logs []entity.ActivityLog) []res.ActivityLogDetailResponse { + var responses []res.ActivityLogDetailResponse + + for _, log := range logs { + response := res.ActivityLogDetailResponse{ + ID: log.ID, + UserID: log.UserID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + ResourceID: log.ResourceID, + OldData: log.OldData, + NewData: log.NewData, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } + responses = append(responses, response) + } + + return responses +} + +// ConvertToSingleActivityLogResponse converts single activity log entity to simple response +func ConvertToSingleActivityLogResponse(log entity.ActivityLog) res.ActivityLogResponse { + return res.ActivityLogResponse{ + ID: log.ID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } +} + +// ConvertToSingleActivityLogDetailResponse converts single activity log entity to detailed response +func ConvertToSingleActivityLogDetailResponse(log entity.ActivityLog) res.ActivityLogDetailResponse { + return res.ActivityLogDetailResponse{ + ID: log.ID, + UserID: log.UserID, + Username: log.User.Username, + Role: log.User.Role.Name, + Action: log.Action, + Resource: log.Resource, + ResourceID: log.ResourceID, + OldData: log.OldData, + NewData: log.NewData, + IPAddress: log.IPAddress, + UserAgent: log.UserAgent, + Timestamp: log.Timestamp, + } +} \ No newline at end of file diff --git a/utils/helper/backboneHelperRes.go b/utils/helper/backboneHelperRes.go index c281850..e24c2dd 100644 --- a/utils/helper/backboneHelperRes.go +++ b/utils/helper/backboneHelperRes.go @@ -16,6 +16,7 @@ func ConvertToBackboneResponses(backbone []entity.Backbone, totalFishbone map[uu } backboneResp := res.BackboneResponse { ID: backbone.ID, + BackboneCode: backbone.BackboneCode, DevStart: backbone.DeviceStart.DeviceCode, DevEnd: backbone.DeviceEnd.DeviceCode, CoreAmount: backbone.CoreAmount, diff --git a/utils/helper/deviceHelperRes.go b/utils/helper/deviceHelperRes.go index 710ae61..3f8a31f 100644 --- a/utils/helper/deviceHelperRes.go +++ b/utils/helper/deviceHelperRes.go @@ -1,6 +1,7 @@ package helper import ( + "fmt" "log" "users_management/m/model/dto/res" "users_management/m/model/entity" @@ -14,6 +15,7 @@ func ConvertToDeviceTypeResponse(devices []entity.Device) []res.DeviceTypeRespon ID: devices.ID, DeviceType: string(devices.DeviceType), DeviceCode: devices.DeviceCode, + } responses = append(responses, deviceResp) } @@ -22,41 +24,38 @@ func ConvertToDeviceTypeResponse(devices []entity.Device) []res.DeviceTypeRespon } -func ConvertToDeviceResponse (devices []entity.Device, geocoder service.GeocodingService) ([]res.DeviceResponse, error) { - var responses []res.DeviceResponse - for _, devices := range devices { - var address string - - if geocoder != nil { - generatedAddress, err := geocoder.GetAddressFromCoordinates(devices.Latitude, devices.Longitude) - if err != nil { - // Log specific geocoding error - log.Printf("Geocoding error for device %s: %v", devices.DeviceCode, err) - } else { - address = generatedAddress - } - } else{ - log.Println("WARNING: Geocoder is nil") - } - - deviceResp := res.DeviceResponse{ - ID: devices.ID, - DeviceCode: devices.DeviceCode, - DeviceType: string(devices.DeviceType), - Longitude: devices.Longitude, - Latitude: devices.Latitude, - Address: address, - PortAmount: devices.PortAmount, - Status: string(devices.Status), - CreatedAt: devices.CreatedAt, - UpdatedAt: devices.UpdatedAt, - } - responses = append(responses, deviceResp) - } - - return responses, nil +func ConvertToDeviceResponse(devices []entity.Device, geocoder service.GeocodingService) ([]res.DeviceResponse, error) { + var responses []res.DeviceResponse + + for _, device := range devices { + // Get address from coordinates + address, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude) + log.Printf("Geocoding for device %s: %.6f, %.6f -> %s", device.DeviceCode, device.Latitude, device.Longitude, address) + if err != nil { + address = fmt.Sprintf("Coordinates: %.6f, %.6f", device.Latitude, device.Longitude) + } + + response := res.DeviceResponse{ + ID: device.ID, + DeviceCode: device.DeviceCode, + DeviceType: string(device.DeviceType), + Longitude: device.Longitude, + Latitude: device.Latitude, + Address: address, + PortAmount: device.PortAmount, + Status: string(device.Status), + Province: device.Province, // Nullable field + City: device.City, // Nullable field + District: device.District, // Nullable field + CreatedAt: device.CreatedAt, + UpdatedAt: device.UpdatedAt, + } + + responses = append(responses, response) + } + + return responses, nil } - func ConvertToDeviceResponseId (devices entity.Device, geocoder service.GeocodingService) (res.DeviceResponse, error) { var address string @@ -79,6 +78,9 @@ func ConvertToDeviceResponseId (devices entity.Device, geocoder service.Geocodin Longitude: devices.Longitude, Latitude: devices.Latitude, Address: address, + Province: devices.Province, + City: devices.City, + District: devices.District, PortAmount: devices.PortAmount, Status: string(devices.Status), CreatedAt: devices.CreatedAt, diff --git a/utils/helper/device_details.go b/utils/helper/device_details.go new file mode 100644 index 0000000..201c28d --- /dev/null +++ b/utils/helper/device_details.go @@ -0,0 +1,230 @@ +package helper + +import ( + "fmt" + "log" + "users_management/m/model/dto/res" + "users_management/m/model/entity" + "users_management/m/utils/service" +) + +func ConvertToDeviceDetailsResponses(devices []entity.DeviceDetails, geocoder service.GeocodingService) ([]res.DeviceDetailsResponse, error) { + var responses []res.DeviceDetailsResponse + + for _, device := range devices { + response, err := ConvertToDeviceDetailsResponse(device, geocoder) + if err != nil { + return nil, err + } + responses = append(responses, response) + } + + return responses, nil +} + +func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder service.GeocodingService) (res.DeviceDetailsResponse, error) { + // Get address + address := "" + if geocoder != nil { + addr, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude) + if err != nil { + log.Printf("Geocoding error for device %s: %v", device.DeviceCode, err) + address = fmt.Sprintf("Coordinates: %.6f, %.6f", device.Latitude, device.Longitude) + } else { + address = addr + } + } + + // Get all backbones connected to this device + allBackbones := device.GetAllBackbones() + backboneInfos := make([]res.BackboneConnectionInfo, 0) + for _, backbone := range allBackbones { + isStartDevice := backbone.DeviceStartID == device.ID + connectedTo := "" + + if isStartDevice && backbone.DeviceEnd.DeviceCode != "" { + connectedTo = backbone.DeviceEnd.DeviceCode + } else if !isStartDevice && backbone.DeviceStart.DeviceCode != "" { + connectedTo = backbone.DeviceStart.DeviceCode + } + + info := res.BackboneConnectionInfo{ + ID: backbone.ID, + BackboneCode: backbone.BackboneCode, + CoreAmount: backbone.CoreAmount, + IsStartDevice: isStartDevice, + ConnectedTo: connectedTo, + } + backboneInfos = append(backboneInfos, info) + } + + // Get all fishbones connected to this device + allFishbones := device.GetAllFishbones() + fishboneInfos := make([]res.FishboneConnectionInfo, 0) + for _, fishbone := range allFishbones { + isStartDevice := fishbone.DeviceStartID == device.ID + connectedTo := "" + + if isStartDevice && fishbone.DeviceEnd.DeviceCode != "" { + connectedTo = fishbone.DeviceEnd.DeviceCode + } else if !isStartDevice && fishbone.DeviceStart.DeviceCode != "" { + connectedTo = fishbone.DeviceStart.DeviceCode + } + + info := res.FishboneConnectionInfo{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + CoreAmount: fishbone.CoreAmount, + BackboneCode: fishbone.Backbone.BackboneCode, + IsStartDevice: isStartDevice, + ConnectedTo: connectedTo, + } + fishboneInfos = append(fishboneInfos, info) + } + + // Convert tower connections - safely handle nullable ExternalTower + towerInfos := make([]res.TowerConnectionDetail, 0) + for _, tower := range device.Towers { + distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude) + + // Safely handle nullable ExternalTower field + var externalTower *bool + if tower.ExternalTower != nil { + externalTower = tower.ExternalTower + } // If tower.ExternalTower is nil, externalTower remains nil + + // Safely handle nullable ImageURL + var imageURL *string + if tower.ImageURL != "" { + imageURL = &tower.ImageURL + } + + info := res.TowerConnectionDetail{ + ID: tower.ID, + TowerCode: tower.TowerCode, + Distance: distance, + ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil + ImageURL: imageURL, + } + 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, + }) + } + } + + var customerNames []string + + // If port_used is 0, return empty port assignments + if device.PortAmount == 0 { + finalPortAssignments = []res.PortAssignmentResponse{} + customerNames = []string{} + } else if device.DevicePort.PortUsed == 0 { + // If port_used is 0, return empty port assignments but keep port_amount structure + finalPortAssignments = []res.PortAssignmentResponse{} + customerNames = []string{} + } 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, + } + } + + // Get customer names + customerNames = device.DevicePort.GetCustomerNamesOnly() + } + + 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, + PortAvailable: device.DevicePort.PortAvailable, + CustomerNames: customerNames, // 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 +} + + + +// ... rest of helper functions remain the same \ No newline at end of file diff --git a/utils/helper/device_inspection.go b/utils/helper/device_inspection.go new file mode 100644 index 0000000..1690398 --- /dev/null +++ b/utils/helper/device_inspection.go @@ -0,0 +1,103 @@ +package helper + +import ( + "bytes" + "users_management/m/model/dto/res" + "users_management/m/model/entity" +) + +// IsEmptyUUID checks if a UUID is empty (all zeros) +func IsEmptyUUID(id [16]byte) bool { + return bytes.Equal(id[:], make([]byte, 16)) +} + +func ConvertToDeviceInspectionResponses(inspections []entity.DeviceInspection) []res.DeviceInspectionResponse { + var responses []res.DeviceInspectionResponse + + for _, inspection := range inspections { + response := res.DeviceInspectionResponse{ + ID: inspection.ID, + DeviceCode: inspection.Device.DeviceCode, + DeviceType: string(inspection.Device.DeviceType), + UserName: inspection.User.Name, + Status: inspection.Status, + PortUsed: inspection.PortUsed, + PortAvailable: inspection.PortAvailable, + CableAmount: inspection.CableAmount, + Description: inspection.Description, + ImageURL: inspection.ImageURL, + InspectionPlacement: inspection.InspectionPlacement, // Full address + Longitude: inspection.Longitude, + Latitude: inspection.Latitude, + InspectionApproval: inspection.InspectionApproval, + CreatedAt: inspection.CreatedAt, + UpdatedAt: inspection.UpdatedAt, + } + + if inspection.Backbone != nil && !IsEmptyUUID(inspection.Backbone.ID) { + response.BackboneCode = &inspection.Backbone.BackboneCode + } + if inspection.Fishbone != nil && !IsEmptyUUID(inspection.Fishbone.ID) { + response.FishboneCode = &inspection.Fishbone.FishboneCode + } + if inspection.Tower != nil && !IsEmptyUUID(inspection.Tower.ID) { + response.TowerCode = &inspection.Tower.TowerCode + } + + responses = append(responses, response) + } + + return responses +} + + +func ConvertToDeviceInspectionDetailResponse(inspection entity.DeviceInspection) res.DeviceInspectionDetailResponse { + response := res.DeviceInspectionDetailResponse{ + ID: inspection.ID, + Device: res.DeviceInfo{ + ID: inspection.Device.ID, + DeviceCode: inspection.Device.DeviceCode, + DeviceType: string(inspection.Device.DeviceType), + }, + User: res.UserInfo{ + ID: inspection.User.ID, + Name: inspection.User.Name, + Username: inspection.User.Username, + }, + Status: inspection.Status, + PortUsed: inspection.PortUsed, + PortAvailable: inspection.PortAvailable, + CableAmount: inspection.CableAmount, + Description: inspection.Description, + ImageURL: inspection.ImageURL, + InspectionPlacement: inspection.InspectionPlacement, // Full address + Longitude: inspection.Longitude, + Latitude: inspection.Latitude, + InspectionApproval: inspection.InspectionApproval, + CreatedAt: inspection.CreatedAt, + UpdatedAt: inspection.UpdatedAt, + } + + if inspection.Backbone != nil && !IsEmptyUUID(inspection.Backbone.ID) { + response.Backbone = &res.BackboneInfo{ + ID: inspection.Backbone.ID, + BackboneCode: inspection.Backbone.BackboneCode, + } + } + + if inspection.Fishbone != nil && !IsEmptyUUID(inspection.Fishbone.ID) { + response.Fishbone = &res.FishboneInfo{ + ID: inspection.Fishbone.ID, + FishboneCode: inspection.Fishbone.FishboneCode, + } + } + + if inspection.Tower != nil && !IsEmptyUUID(inspection.Tower.ID) { + response.Tower = &res.TowerInfo{ + ID: inspection.Tower.ID, + TowerCode: inspection.Tower.TowerCode, + } + } + + return response +} \ No newline at end of file diff --git a/utils/helper/fishboneHelperRes.go b/utils/helper/fishboneHelperRes.go new file mode 100644 index 0000000..1fb4258 --- /dev/null +++ b/utils/helper/fishboneHelperRes.go @@ -0,0 +1,83 @@ +package helper + +import ( + "users_management/m/model/dto/res" + "users_management/m/model/entity" + + "github.com/google/uuid" +) + +// ConvertToFishboneResponses converts fishbone entities to response DTOs +func ConvertToFishboneResponses(fishbones []entity.Fishbone, totalFishbone map[uuid.UUID]int) ([]res.FishboneResponse, error) { + var responses []res.FishboneResponse + + for _, fishbone := range fishbones { + fishboneResp := res.FishboneResponse{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + BackboneCode: fishbone.Backbone.BackboneCode, + DeviceStart: fishbone.DeviceStart.DeviceCode, + DeviceEnd: fishbone.DeviceEnd.DeviceCode, + CoreAmount: fishbone.CoreAmount, + CreatedAt: fishbone.CreatedAt, + UpdatedAt: fishbone.UpdatedAt, + } + responses = append(responses, fishboneResp) + } + + return responses, nil +} + +// ConvertToFishboneDetailResponse converts a single fishbone entity to detailed response +func ConvertToFishboneDetailResponse(fishbone entity.Fishbone) res.FishboneDetailResponse { + return res.FishboneDetailResponse{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + Backbone: res.BackboneInfo{ + BackboneCode: fishbone.Backbone.BackboneCode,// Fixed missing field + }, + CoreAmount: fishbone.CoreAmount, + CreatedAt: fishbone.CreatedAt, + UpdatedAt: fishbone.UpdatedAt, + } +} + +// ConvertToSimpleFishboneResponses converts fishbone entities to simple response format +func ConvertToSimpleFishboneResponses(fishbones []entity.Fishbone) []res.FishboneResponse { + var responses []res.FishboneResponse + + for _, fishbone := range fishbones { + fishboneResp := res.FishboneResponse{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + BackboneCode: fishbone.Backbone.BackboneCode, + DeviceStart: fishbone.DeviceStart.DeviceCode, + DeviceEnd: fishbone.DeviceEnd.DeviceCode, + CoreAmount: fishbone.CoreAmount, + CreatedAt: fishbone.CreatedAt, + UpdatedAt: fishbone.UpdatedAt, + } + responses = append(responses, fishboneResp) + } + + return responses +} + +// GetFishboneStats calculates statistics for fishbones +func GetFishboneStats(fishbones []entity.Fishbone) map[string]interface{} { + totalFishbones := len(fishbones) + totalCores := 0 + backboneCount := make(map[uuid.UUID]int) + + for _, fishbone := range fishbones { + totalCores += fishbone.CoreAmount + backboneCount[fishbone.BackboneID]++ + } + + return map[string]interface{}{ + "total_cores": totalCores, + "average_cores": float64(totalCores) / float64(totalFishbones), + "unique_backbones": len(backboneCount), + "backbone_usage": backboneCount, + } +} \ No newline at end of file diff --git a/utils/helper/image_uploader.go b/utils/helper/image_uploader.go new file mode 100644 index 0000000..3c83c79 --- /dev/null +++ b/utils/helper/image_uploader.go @@ -0,0 +1,90 @@ +package helper + +import ( + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" +) + +const ( + MaxFileSize = 5 << 20 // 5MB + UploadDir = "./uploads/towers" +) + +// SaveTowerImage saves uploaded image and returns the URL +func SaveTowerImage(file *multipart.FileHeader) (string, error) { + // Validate file size + if file.Size > MaxFileSize { + return "", fmt.Errorf("file size exceeds 5MB limit") + } + + // Validate file type + allowedTypes := []string{".jpg", ".jpeg", ".png", ".webp"} + ext := strings.ToLower(filepath.Ext(file.Filename)) + isAllowed := false + for _, allowedType := range allowedTypes { + if ext == allowedType { + isAllowed = true + break + } + } + if !isAllowed { + return "", fmt.Errorf("file type not allowed. Only jpg, jpeg, png, webp are allowed") + } + + // Create upload directory if it doesn't exist + if err := os.MkdirAll(UploadDir, 0755); err != nil { + return "", fmt.Errorf("failed to create upload directory: %v", err) + } + + // Generate unique filename + filename := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext) + filePath := filepath.Join(UploadDir, filename) + + // Open uploaded file + src, err := file.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + // Create destination file + dst, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("failed to create destination file: %v", err) + } + defer dst.Close() + + // Copy file + if _, err := io.Copy(dst, src); err != nil { + return "", fmt.Errorf("failed to copy file: %v", err) + } + + // Return URL (adjust based on your server setup) + return fmt.Sprintf("/uploads/towers/%s", filename), nil +} + +// DeleteTowerImage deletes an image file +func DeleteTowerImage(imageURL string) error { + if imageURL == "" { + return nil + } + + // Extract filename from URL + filename := filepath.Base(imageURL) + filePath := filepath.Join(UploadDir, filename) + + // Check if file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil // File doesn't exist, nothing to delete + } + + // Delete file + return os.Remove(filePath) +} \ No newline at end of file diff --git a/utils/helper/nearest_device_helper.go b/utils/helper/nearest_device_helper.go new file mode 100644 index 0000000..e3b209b --- /dev/null +++ b/utils/helper/nearest_device_helper.go @@ -0,0 +1,212 @@ +package helper + +import ( + "fmt" + "log" + "math" + "users_management/m/model/dto/res" + "users_management/m/model/entity" + "users_management/m/repository" + "users_management/m/utils/service" + + "github.com/google/uuid" +) + +func ConvertToNearestDeviceResponses(devices []entity.DeviceWithDistance, repo repository.NearestDeviceRepo, geocoder service.GeocodingService) ([]res.NearestDeviceResponse, error) { + var responses []res.NearestDeviceResponse + + for _, device := range devices { + // Get address + address := "" + if geocoder != nil { + addr, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude) + if err != nil { + log.Printf("Geocoding error for device %s: %v", device.DeviceCode, err) + address = fmt.Sprintf("Coordinates: %.6f, %.6f", device.Latitude, device.Longitude) + } else { + address = addr + } + } + + // Count connections + backboneCount, fishboneCount, towerCount, err := repo.CountConnectionsByDeviceID(device.ID) + if err != nil { + log.Printf("Error counting connections for device %s: %v", device.DeviceCode, err) + backboneCount, fishboneCount, towerCount = 0, 0, 0 + } + + response := res.NearestDeviceResponse{ + ID: device.ID, + DeviceCode: device.DeviceCode, + DeviceType: string(device.DeviceType), + Distance: device.Distance, // Use the calculated distance from query + Address: address, + Longitude: device.Longitude, + Latitude: device.Latitude, + Status: string(device.Status), + PortAmount: device.PortAmount, + Region: device.Region, + Province: device.Province, + City: device.City, + District: device.District, + BackboneCount: backboneCount, + FishboneCount: fishboneCount, + TowerCount: towerCount, + CreatedAt: device.CreatedAt, + } + + responses = append(responses, response) + } + + return responses, nil +} + +func ConvertToNearestDeviceDetailResponse(device entity.Device, distance float64, repo repository.NearestDeviceRepo, geocoder service.GeocodingService) (res.NearestDeviceDetailResponse, error) { + // Get address + address := "" + if geocoder != nil { + addr, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude) + if err != nil { + log.Printf("Geocoding error for device %s: %v", device.DeviceCode, err) + address = fmt.Sprintf("Coordinates: %.6f, %.6f", device.Latitude, device.Longitude) + } else { + address = addr + } + } + + // Get detailed connection information + backbones, err := repo.GetBackbonesByDeviceID(device.ID) + if err != nil { + return res.NearestDeviceDetailResponse{}, err + } + + fishbones, err := repo.GetFishbonesByDeviceID(device.ID) + if err != nil { + return res.NearestDeviceDetailResponse{}, err + } + + towers, err := repo.GetTowersByDeviceID(device.ID) + if err != nil { + return res.NearestDeviceDetailResponse{}, err + } + + // Convert to response format + backboneInfos := convertToBackboneConnectionInfos(backbones, device.ID) + fishboneInfos := convertToFishboneConnectionInfos(fishbones, device.ID) + towerInfos := convertToTowerConnectionInfos(towers, device.Latitude, device.Longitude) + + response := res.NearestDeviceDetailResponse{ + ID: device.ID, + DeviceCode: device.DeviceCode, + DeviceType: string(device.DeviceType), + Distance: distance, + Address: address, + Longitude: device.Longitude, + Latitude: device.Latitude, + Status: string(device.Status), + PortAmount: device.PortAmount, + 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 +} + +func convertToBackboneConnectionInfos(backbones []entity.Backbone, deviceID uuid.UUID) []res.BackboneConnectionInfo { + var infos []res.BackboneConnectionInfo + + for _, backbone := range backbones { + isStartDevice := backbone.DeviceStartID == deviceID + connectedTo := "" + + if isStartDevice && backbone.DeviceEnd.DeviceCode != "" { + connectedTo = backbone.DeviceEnd.DeviceCode + } else if !isStartDevice && backbone.DeviceStart.DeviceCode != "" { + connectedTo = backbone.DeviceStart.DeviceCode + } + + info := res.BackboneConnectionInfo{ + ID: backbone.ID, + BackboneCode: backbone.BackboneCode, + CoreAmount: backbone.CoreAmount, + IsStartDevice: isStartDevice, + ConnectedTo: connectedTo, + } + + infos = append(infos, info) + } + + return infos +} + +func convertToFishboneConnectionInfos(fishbones []entity.Fishbone, deviceID uuid.UUID) []res.FishboneConnectionInfo { + var infos []res.FishboneConnectionInfo + + for _, fishbone := range fishbones { + isStartDevice := fishbone.DeviceStartID == deviceID + connectedTo := "" + + if isStartDevice && fishbone.DeviceEnd.DeviceCode != "" { + connectedTo = fishbone.DeviceEnd.DeviceCode + } else if !isStartDevice && fishbone.DeviceStart.DeviceCode != "" { + connectedTo = fishbone.DeviceStart.DeviceCode + } + + info := res.FishboneConnectionInfo{ + ID: fishbone.ID, + FishboneCode: fishbone.FishboneCode, + CoreAmount: fishbone.CoreAmount, + BackboneCode: fishbone.Backbone.BackboneCode, + IsStartDevice: isStartDevice, + ConnectedTo: connectedTo, + } + + infos = append(infos, info) + } + + return infos +} + +func convertToTowerConnectionInfos(towers []entity.Tower, deviceLat, deviceLng float64) []res.TowerConnectionInfo { + var infos []res.TowerConnectionInfo + + for _, tower := range towers { + // Calculate distance from device to tower + distance := calculateDistance(deviceLat, deviceLng, tower.Latitude, tower.Longitude) + + info := res.TowerConnectionInfo{ + ID: tower.ID, + TowerCode: tower.TowerCode, + Distance: distance, + ImageURL: &tower.ImageURL, + } + + infos = append(infos, info) + } + + return infos +} + +func calculateDistance(lat1, lng1, lat2, lng2 float64) float64 { + const earthRadius = 6371 // Earth's radius in kilometers + + lat1Rad := lat1 * math.Pi / 180 + lng1Rad := lng1 * math.Pi / 180 + lat2Rad := lat2 * math.Pi / 180 + lng2Rad := lng2 * math.Pi / 180 + + dlat := lat2Rad - lat1Rad + dlng := lng2Rad - lng1Rad + + a := math.Sin(dlat/2)*math.Sin(dlat/2) + math.Cos(lat1Rad)*math.Cos(lat2Rad)*math.Sin(dlng/2)*math.Sin(dlng/2) + c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) + + return earthRadius * c +} \ No newline at end of file diff --git a/utils/helper/towerHelperRes.go b/utils/helper/towerHelperRes.go index 2533c1a..ad898d3 100644 --- a/utils/helper/towerHelperRes.go +++ b/utils/helper/towerHelperRes.go @@ -8,61 +8,70 @@ import ( ) func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingService) ([]res.TowerResponse, error) { - var responses []res.TowerResponse - - - for _, tower := range towers { - var address string + var responses []res.TowerResponse + + for _, tower := range towers { + var address string - if geocoder != nil { - generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) - if err != nil { - // Log specific geocoding error - log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) - } else { - address = generatedAddress - } - } else { - log.Println("WARNING: Geocoder is nil") - } + if geocoder != nil { + generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) + if err != nil { + log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) + } else { + address = generatedAddress + } + } - towerResp := res.TowerResponse{ - ID: tower.ID, - DeviceCode: tower.Device.DeviceCode, - TowerCode: tower.TowerCode, - Longitude: tower.Longitude, - Latitude: tower.Latitude, - Address: address, - CreatedAt: tower.CreatedAt, - } - responses = append(responses, towerResp) - } - return responses, nil + var deviceCode *string + if tower.Device != nil { + deviceCode = &tower.Device.DeviceCode + } + + towerResp := res.TowerResponse{ + ID: tower.ID, + DeviceCode: deviceCode, // Now nullable + TowerCode: &tower.TowerCode, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + Address: address, + ImageURL: tower.ImageURL, + ExternalTower: tower.ExternalTower, // Now nullable + CreatedAt: tower.CreatedAt, + UpdatedAt: tower.UpdatedAt, + } + responses = append(responses, towerResp) + } + return responses, nil } func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingService) (res.TowerResponse, error) { - var address string + var address string - if geocoder != nil { - generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) - if err != nil { - // Log specific geocoding error - log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) - } else { - address = generatedAddress - } - } else { - log.Println("WARNING: Geocoder is nil") - } + if geocoder != nil { + generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) + if err != nil { + log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) + } else { + address = generatedAddress + } + } - towerResp := res.TowerResponse{ - ID: tower.ID, - DeviceCode: tower.Device.DeviceCode, - TowerCode: tower.TowerCode, - Longitude: tower.Longitude, - Latitude: tower.Latitude, - Address: address, - CreatedAt: tower.CreatedAt, - } - return towerResp, nil + var deviceCode *string + if tower.Device != nil { + deviceCode = &tower.Device.DeviceCode + } + + towerResp := res.TowerResponse{ + ID: tower.ID, + DeviceCode: deviceCode, // Now nullable + TowerCode: &tower.TowerCode, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + Address: address, + ImageURL: tower.ImageURL, + ExternalTower: tower.ExternalTower, // Now nullable + CreatedAt: tower.CreatedAt, + UpdatedAt: tower.UpdatedAt, + } + return towerResp, nil } \ No newline at end of file diff --git a/utils/insecure_http.go b/utils/insecure_http.go new file mode 100644 index 0000000..65858b2 --- /dev/null +++ b/utils/insecure_http.go @@ -0,0 +1,34 @@ +package utils + +import ( + "crypto/tls" + "net/http" + "time" +) + +// CreateInsecureHTTPClient creates an HTTP client that skips SSL verification +func CreateInsecureHTTPClient() *http.Client { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + return &http.Client{ + Transport: tr, + Timeout: 30 * time.Second, + } +} + +// CreateSecureHTTPClient creates a standard HTTP client with SSL verification +func CreateSecureHTTPClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + } +} + +// GetHTTPClient returns appropriate client based on environment +func GetHTTPClient(skipSSL bool) *http.Client { + if skipSSL { + return CreateInsecureHTTPClient() + } + return CreateSecureHTTPClient() +} \ No newline at end of file diff --git a/utils/service/geo_service.go b/utils/service/geo_service.go index 14a92e2..2ac7952 100644 --- a/utils/service/geo_service.go +++ b/utils/service/geo_service.go @@ -17,7 +17,7 @@ type nominatimGeocoder struct { client *http.Client } -// NewGeocodingService creates a new geocoding service using Nominatim (OpenStreetMap) + func NewGeocodingService() GeocodingService { client := &http.Client{ Timeout: 10 * time.Second,