From 32c775ab24a42dd20103525f435acf45e4608359 Mon Sep 17 00:00:00 2001 From: areeqakbr Date: Wed, 11 Jun 2025 22:48:11 +0700 Subject: [PATCH] adding nearest device and some addtion feature more complex --- delivery/controller/device_details.go | 137 ++++++ .../device_inspections_controller.go | 255 +++++++++++ .../controller/nearest_device_controller.go | 101 +++++ delivery/server.go | 3 + manager/repository_manager.go | 15 + manager/usecase_manager.go | 41 +- model/dto/req/deviceDetails.go | 28 ++ model/dto/req/device_dto.go | 39 +- model/dto/req/device_inspections_dto.go | 38 ++ model/dto/req/nearest_devices.go | 11 + model/dto/res/device_details.go | 39 ++ model/dto/res/device_inspections_res.go | 72 +++ model/dto/res/device_res.go | 25 +- model/dto/res/nearest_devices.go | 80 ++++ model/dto/res/tower_res.go | 4 +- model/entity/device_details.go | 51 +++ model/entity/device_inspection.go | 38 ++ model/entity/device_port.go | 14 +- model/entity/device_with_distance.go | 25 ++ model/entity/devices.go | 18 +- model/entity/tower_inspection.go | 13 + repository/device_details.go | 254 +++++++++++ repository/device_inspection.go | 155 +++++++ repository/nearest_device_repo.go | 118 +++++ usecase/backbone_usecase.go | 166 +++++-- usecase/devicePort_usecase.go | 1 - usecase/device_details.go | 186 ++++++++ usecase/device_inspection.go | 414 ++++++++++++++++++ usecase/device_usecase.go | 32 +- usecase/fishbone_usecase.go | 224 ++++++---- usecase/nearest_device.go | 106 +++++ utils/helper/deviceHelperRes.go | 70 +-- utils/helper/device_details.go | 125 ++++++ utils/helper/device_inspection.go | 103 +++++ utils/helper/nearest_device_helper.go | 212 +++++++++ utils/helper/towerHelperRes.go | 4 +- 36 files changed, 2991 insertions(+), 226 deletions(-) create mode 100644 delivery/controller/device_details.go create mode 100644 delivery/controller/device_inspections_controller.go create mode 100644 delivery/controller/nearest_device_controller.go create mode 100644 model/dto/req/deviceDetails.go create mode 100644 model/dto/req/device_inspections_dto.go create mode 100644 model/dto/req/nearest_devices.go create mode 100644 model/dto/res/device_details.go create mode 100644 model/dto/res/device_inspections_res.go create mode 100644 model/dto/res/nearest_devices.go create mode 100644 model/entity/device_details.go create mode 100644 model/entity/device_inspection.go create mode 100644 model/entity/device_with_distance.go create mode 100644 model/entity/tower_inspection.go create mode 100644 repository/device_details.go create mode 100644 repository/device_inspection.go create mode 100644 repository/nearest_device_repo.go create mode 100644 usecase/device_details.go create mode 100644 usecase/device_inspection.go create mode 100644 usecase/nearest_device.go create mode 100644 utils/helper/device_details.go create mode 100644 utils/helper/device_inspection.go create mode 100644 utils/helper/nearest_device_helper.go diff --git a/delivery/controller/device_details.go b/delivery/controller/device_details.go new file mode 100644 index 0000000..10b23c0 --- /dev/null +++ b/delivery/controller/device_details.go @@ -0,0 +1,137 @@ +package controller + +import ( + "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) + } +} + +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) +} \ 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/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/server.go b/delivery/server.go index 0fd51b2..7018172 100644 --- a/delivery/server.go +++ b/delivery/server.go @@ -60,6 +60,9 @@ func (s *Server) setupController() { 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() } } diff --git a/manager/repository_manager.go b/manager/repository_manager.go index c07e3f0..084fd2f 100644 --- a/manager/repository_manager.go +++ b/manager/repository_manager.go @@ -12,6 +12,10 @@ type RepositoryManager interface { NewDevicePortRepository() repository.DevicePortRepo NewCountAssetsRepository() repository.CountAssetsRepo NewActivityLogRepository() repository.ActivityLogRepo + NewDeviceInspectionRepository() repository.DeviceInspectionRepo + NewNearestDeviceRepository() repository.NearestDeviceRepo + + NewDeviceDetailsRepository() repository.DeviceDetailsRepo } @@ -25,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()) } @@ -55,4 +66,8 @@ func (rm *repositoryManager) NewCountAssetsRepository() repository.CountAssetsRe 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 da1d3ea..635ed77 100644 --- a/manager/usecase_manager.go +++ b/manager/usecase_manager.go @@ -19,6 +19,12 @@ type UsecaseManager interface { NewCountAssetsUsecase() usecase.CountAssetsUseCase NewActivityLogUsecase() usecase.ActivityLogUseCase + + NewDeviceInspectionUsecase() usecase.DeviceInspectionUseCase + + NewNearestDeviceUsecase() usecase.NearestDeviceUseCase + + NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase } @@ -34,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()) } @@ -47,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 { @@ -69,4 +98,12 @@ func (um *usecaseManager) NewCountAssetsUsecase() usecase.CountAssetsUseCase { 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/model/dto/req/deviceDetails.go b/model/dto/req/deviceDetails.go new file mode 100644 index 0000000..8c29d94 --- /dev/null +++ b/model/dto/req/deviceDetails.go @@ -0,0 +1,28 @@ +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"` + 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,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"` +} \ No newline at end of file diff --git a/model/dto/req/device_dto.go b/model/dto/req/device_dto.go index 9498304..e8e04ce 100644 --- a/model/dto/req/device_dto.go +++ b/model/dto/req/device_dto.go @@ -1,28 +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"` - Region string `json:"region" validate:"required"` - Province string `json:"province" validate:"required"` - City string `json:"city" validate:"required"` - District string `json:"district" validate:"required"` + 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"` + 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"` - Region *string `json:"region,omitempty" validate:"omitempty,min=3"` - Province *string `json:"province,omitempty" validate:"omitempty,min=3"` - City *string `json:"city,omitempty" validate:"omitempty,min=3"` - District *string `json:"district,omitempty" validate:"omitempty,min=3"` + DeviceCode *string `json:"device_code,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"` + 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/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/res/device_details.go b/model/dto/res/device_details.go new file mode 100644 index 0000000..2a95562 --- /dev/null +++ b/model/dto/res/device_details.go @@ -0,0 +1,39 @@ +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"` + 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 + ImageURL *string `json:"image_url,omitempty"` +} \ 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..42d5b0b 100644 --- a/model/dto/res/device_res.go +++ b/model/dto/res/device_res.go @@ -7,14 +7,17 @@ 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 + 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/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 408991c..6344360 100644 --- a/model/dto/res/tower_res.go +++ b/model/dto/res/tower_res.go @@ -9,8 +9,8 @@ import ( type TowerResponse struct { ID uuid.UUID `json:"id"` DeviceCode string `json:"device_code"` - TowerCode string `json:"tower_code"` - Longitude float64 `json:"longtitude"` + TowerCode *string `json:"tower_code"` + Longitude float64 `json:"longitude"` Latitude float64 `json:"latitude"` Address string `json:"address"` ImageURL string `json:"image_url"` 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 b1d70cb..b3ec993 100644 --- a/model/entity/device_port.go +++ b/model/entity/device_port.go @@ -6,13 +6,15 @@ import ( ) type DevicePort struct { - ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` - DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;column:device_id"` - PortNumber int `json:"port_number"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;column:device_id;unique"` + PortUsed int `json:"port_used" gorm:"default:0"` // Auto-calculated + PortAvailable int `json:"port_available" gorm:"default:0"` // Auto-calculated + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - Device Device `gorm:"foreignKey:DeviceID"` + // Relationships + Device Device `json:"device" gorm:"foreignKey:DeviceID"` } func (DevicePort) TableName() string { 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 e3e75a1..11da6ad 100644 --- a/model/entity/devices.go +++ b/model/entity/devices.go @@ -9,10 +9,12 @@ 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" + + ActiveDev DeviceStatus = "active" + InactiveDev DeviceStatus = "inactive" + MaintenanceDev DeviceStatus = "maintenance" ) type Device struct { @@ -23,10 +25,10 @@ type Device struct { Latitude float64 `json:"latitude"` PortAmount int `json:"port_amount"` Status DeviceStatus `json:"status"` - Region string `json:"region"` - Province string `json:"province"` - City string `json:"city"` - District string `json:"district"` + 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"` } 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/repository/device_details.go b/repository/device_details.go new file mode 100644 index 0000000..806d24a --- /dev/null +++ b/repository/device_details.go @@ -0,0 +1,254 @@ +package repository + +import ( + "errors" + "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 + CheckDeviceExists(deviceID uuid.UUID) (bool, error) + GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error) +} + +type deviceDetailsRepo struct { + db *gorm.DB +} + +func NewDeviceDetailsRepo(db *gorm.DB) DeviceDetailsRepo { + return &deviceDetailsRepo{ + db: db, + } +} + + + +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"). + 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"). + 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 errors.New("cannot reduce port amount below current usage") + } + + // Update port available + newPortAvailable := newPortAmount - devicePort.PortUsed + return tx.Model(&entity.DevicePort{}). + Where("device_id = ?", deviceID). + Updates(map[string]interface{}{ + "port_available": newPortAvailable, + "updated_at": gorm.Expr("NOW()"), + }).Error +} + +func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // Get device info + var device entity.Device + if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil { + return err + } + + var portUsed int + + if device.DeviceType == "OTB" { + // For OTB: count backbones (each backbone uses 1 port) + var backboneCount int64 + if err := tx.Model(&entity.Backbone{}). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Count(&backboneCount).Error; err != nil { + return err + } + portUsed = int(backboneCount) + } else if device.DeviceType == "ODP" { + // For ODP: sum fishbone core amounts (each core uses 1 port) + var totalCores int64 + if err := tx.Model(&entity.Fishbone{}). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Select("COALESCE(SUM(core_amount), 0)"). + Scan(&totalCores).Error; err != nil { + return err + } + portUsed = int(totalCores) + } + + portAvailable := device.PortAmount - portUsed + + return tx.Model(&entity.DevicePort{}). + Where("device_id = ?", deviceID). + Updates(map[string]interface{}{ + "port_used": portUsed, + "port_available": portAvailable, + "updated_at": gorm.Expr("NOW()"), + }).Error + }) +} + +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.PortAvailable < 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) 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 *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 + }) +} \ 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/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/usecase/backbone_usecase.go b/usecase/backbone_usecase.go index f029acc..df25410 100644 --- a/usecase/backbone_usecase.go +++ b/usecase/backbone_usecase.go @@ -19,42 +19,80 @@ 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(), - BackboneCode: backbone.BackboneCode, - DeviceStartID: backbone.DeviceStartID, - DeviceEndID: backbone.DeviceEndID, - CoreAmount: backbone.CoreAmount, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + // Validate that both devices exist and are OTB type + startExists, err := u.deviceDetailsRepo.CheckDeviceExists(backbone.DeviceStartID) + if err != nil { + return fmt.Errorf("error checking start device: %w", err) + } + if !startExists { + return fmt.Errorf("start device does not exist") + } - - return u.backboneRepo.Post(newBackbone) + endExists, err := u.deviceDetailsRepo.CheckDeviceExists(backbone.DeviceEndID) + if err != nil { + return fmt.Errorf("error checking end device: %w", err) + } + if !endExists { + return fmt.Errorf("end device does not exist") + } + + // Validate port availability (each backbone connection uses 1 port) + if err := u.deviceDetailsRepo.ValidatePortAvailability(backbone.DeviceStartID, 1); err != nil { + return fmt.Errorf("start device: %w", err) + } + if err := u.deviceDetailsRepo.ValidatePortAvailability(backbone.DeviceEndID, 1); err != nil { + return fmt.Errorf("end device: %w", err) + } + + 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 + err = u.backboneRepo.Post(newBackbone) + if err != nil { + return err + } + + // Update port usage for both devices + u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceStartID) + u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceEndID) + + return nil } + func (u *backboneUseCase) GetAllBackbone() ([]res.BackboneResponse, error) { backbones, err := u.backboneRepo.GetAll() if err != nil { @@ -95,26 +133,80 @@ func (u *backboneUseCase) GetByID(id uuid.UUID) (res.BackboneResponse, error) { } 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{}) + // Get original backbone to track changes + originalBackbone, err := u.backboneRepo.GetByID(id) + if err != nil { + return err + } - if backbone.DeviceStartID != nil { - updates["DeviceStartID"] = backbone.DeviceStartID - } + updates := make(map[string]interface{}) - if backbone.DeviceEndID != nil { - updates["DeviceEndID"] = backbone.DeviceEndID - } + // Track devices that need port recalculation + devicesToUpdate := make(map[uuid.UUID]bool) + devicesToUpdate[originalBackbone.DeviceStartID] = true + devicesToUpdate[originalBackbone.DeviceEndID] = true - if backbone.CoreAmount != nil { - updates["CoreAmount"] = backbone.CoreAmount - } + if backbone.DeviceStartID != nil { + // Validate new start device exists and has available ports + exists, err := u.deviceDetailsRepo.CheckDeviceExists(*backbone.DeviceStartID) + if err != nil { + return fmt.Errorf("error checking new start device: %w", err) + } + if !exists { + return fmt.Errorf("new start device does not exist") + } - updates["UpdatedAt"] = time.Now() + if err := u.deviceDetailsRepo.ValidatePortAvailability(*backbone.DeviceStartID, 1); err != nil { + return fmt.Errorf("new start device: %w", err) + } - return u.backboneRepo.Update(id, updates) + updates["dev_start_id"] = *backbone.DeviceStartID + devicesToUpdate[*backbone.DeviceStartID] = true + } + + if backbone.DeviceEndID != nil { + // Validate new end device exists and has available ports + exists, err := u.deviceDetailsRepo.CheckDeviceExists(*backbone.DeviceEndID) + if err != nil { + return fmt.Errorf("error checking new end device: %w", err) + } + if !exists { + return fmt.Errorf("new end device does not exist") + } + + if err := u.deviceDetailsRepo.ValidatePortAvailability(*backbone.DeviceEndID, 1); err != nil { + return fmt.Errorf("new end device: %w", err) + } + + updates["dev_end_id"] = *backbone.DeviceEndID + devicesToUpdate[*backbone.DeviceEndID] = true + } + + if backbone.CoreAmount != nil { + updates["core_amount"] = *backbone.CoreAmount + } + + if len(updates) == 0 { + return fmt.Errorf("no fields to update") + } + + updates["updated_at"] = time.Now() + + // Update backbone + err = u.backboneRepo.Update(id, updates) + if err != nil { + return err + } + + // Recalculate port usage for all affected devices + for deviceID := range devicesToUpdate { + u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) + } + + return nil } \ 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..59d67e9 --- /dev/null +++ b/usecase/device_details.go @@ -0,0 +1,186 @@ +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 +} + +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) 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) + } + + // Check if device exists + exists, err := u.deviceDetailsRepo.CheckDeviceExists(id) + if err != nil { + return err + } + if !exists { + return errors.New("device not found") + } + + 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 + 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) ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error { + return u.deviceDetailsRepo.ValidatePortAvailability(deviceID, requiredPorts) +} + +func (u *deviceDetailsUseCase) RecalculatePortUsage(deviceID uuid.UUID) error { + return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) +} \ 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 73f2af8..ff8d5ff 100644 --- a/usecase/device_usecase.go +++ b/usecase/device_usecase.go @@ -43,21 +43,20 @@ 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), - Region: device.Region, - Province: device.Province, - City: device.City, - District: device.District, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + 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) } @@ -113,9 +112,6 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e if device.Status != nil { updates["Status"] = *device.Status } - if device.Region != nil { - updates["Region"] = *device.Region - } if device.Province != nil { updates["Province"] = *device.Province } diff --git a/usecase/fishbone_usecase.go b/usecase/fishbone_usecase.go index e9ba47d..57db4fe 100644 --- a/usecase/fishbone_usecase.go +++ b/usecase/fishbone_usecase.go @@ -1,16 +1,17 @@ package usecase import ( - "errors" - "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" + "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" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" ) type FishboneUseCase interface { @@ -22,53 +23,58 @@ type FishboneUseCase interface { GetFishboneStats() (map[string]interface{}, 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 { + + +func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { err := u.validate.Struct(fishbone) if err != nil { - return err + return fmt.Errorf("validation error: %w", err) } - // Check if backbone exists - backboneExists, err := u.fishboneRepo.CheckBackboneExists(fishbone.BackboneID) + // Validate that both devices exist and are ODP type + startExists, err := u.deviceDetailsRepo.CheckDeviceExists(fishbone.DeviceStartID) if err != nil { - return err + return fmt.Errorf("error checking start device: %w", err) } - if !backboneExists { - return errors.New("backbone not found") + if !startExists { + return fmt.Errorf("start device does not exist") } - // Check if devices exist - deviceStartExists, err := u.fishboneRepo.CheckDeviceExists(fishbone.DeviceStartID) + endExists, err := u.deviceDetailsRepo.CheckDeviceExists(fishbone.DeviceEndID) if err != nil { - return err + return fmt.Errorf("error checking end device: %w", err) } - if !deviceStartExists { - return errors.New("start device not found") + if !endExists { + return fmt.Errorf("end device does not exist") } - deviceEndExists, err := u.fishboneRepo.CheckDeviceExists(fishbone.DeviceEndID) - if err != nil { - return err + // Validate port availability for ODP devices (each core needs 1 port) + if err := u.deviceDetailsRepo.ValidatePortAvailability(fishbone.DeviceStartID, fishbone.CoreAmount); err != nil { + return fmt.Errorf("start device: %w", err) } - if !deviceEndExists { - return errors.New("end device not found") + if err := u.deviceDetailsRepo.ValidatePortAvailability(fishbone.DeviceEndID, fishbone.CoreAmount); err != nil { + return fmt.Errorf("end device: %w", err) } newFishbone := entity.Fishbone{ ID: uuid.New(), - FishboneCode: fishbone.FishboneCode, + FishboneCode: fishbone.FishboneCode, BackboneID: fishbone.BackboneID, DeviceStartID: fishbone.DeviceStartID, DeviceEndID: fishbone.DeviceEndID, @@ -77,10 +83,20 @@ func (u *fishboneUsecase) CreateFishbone(fishbone req.FishboneDTO) error { UpdatedAt: time.Now(), } - return u.fishboneRepo.Post(newFishbone) + // Create fishbone + err = u.fishboneRepo.Post(newFishbone) + if err != nil { + return err + } + + // Update port usage for both devices + u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceStartID) + u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceEndID) + + return nil } -func (u *fishboneUsecase) GetAllFishbone() ([]res.FishboneResponse, error) { +func (u *fishboneUseCase) GetAllFishbone() ([]res.FishboneResponse, error) { fishbones, err := u.fishboneRepo.GetAll() if err != nil { return nil, err @@ -89,7 +105,7 @@ func (u *fishboneUsecase) GetAllFishbone() ([]res.FishboneResponse, error) { return helper.ConvertToSimpleFishboneResponses(fishbones), nil } -func (u *fishboneUsecase) GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) { +func (u *fishboneUseCase) GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) { fishbone, err := u.fishboneRepo.GetByIDWithRelations(id) if err != nil { return res.FishboneDetailResponse{}, err @@ -99,73 +115,113 @@ func (u *fishboneUsecase) GetByID(id uuid.UUID) (res.FishboneDetailResponse, err return helper.ConvertToFishboneDetailResponse(fishbone), nil } -func (u *fishboneUsecase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error { +func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error { err := u.validate.Struct(fishbone) if err != nil { - return err + return fmt.Errorf("validation error: %w", err) } - // Check if fishbone exists - exists, err := u.fishboneRepo.CheckFishboneExists(id) + // Get original fishbone for comparison + originalFishbone, err := u.fishboneRepo.GetByID(id) if err != nil { return err } - if !exists { - return errors.New("fishbone not found") - } updates := make(map[string]interface{}) - if fishbone.FishboneCode != nil { - updates["fishbone_code"] = *fishbone.FishboneCode - } - if fishbone.BackboneID != nil { - // Validate backbone exists - backboneExists, err := u.fishboneRepo.CheckBackboneExists(*fishbone.BackboneID) - if err != nil { - return err + // Track devices that need port recalculation + devicesToUpdate := make(map[uuid.UUID]bool) + devicesToUpdate[originalFishbone.DeviceStartID] = true + devicesToUpdate[originalFishbone.DeviceEndID] = true + + // If core amount is changed, validate port availability + if fishbone.CoreAmount != nil && *fishbone.CoreAmount != originalFishbone.CoreAmount { + coreDiff := *fishbone.CoreAmount - originalFishbone.CoreAmount + if coreDiff > 0 { + // Increasing cores - check port availability + if err := u.deviceDetailsRepo.ValidatePortAvailability(originalFishbone.DeviceStartID, coreDiff); err != nil { + return fmt.Errorf("start device: %w", err) + } + if err := u.deviceDetailsRepo.ValidatePortAvailability(originalFishbone.DeviceEndID, coreDiff); err != nil { + return fmt.Errorf("end device: %w", err) + } } - if !backboneExists { - return errors.New("backbone not found") - } - updates["bb_id"] = *fishbone.BackboneID - } - if fishbone.DeviceStartID != nil { - // Validate device exists - deviceExists, err := u.fishboneRepo.CheckDeviceExists(*fishbone.DeviceStartID) - if err != nil { - return err - } - if !deviceExists { - return errors.New("start device not found") - } - updates["dev_start_id"] = *fishbone.DeviceStartID - } - if fishbone.DeviceEndID != nil { - // Validate device exists - deviceExists, err := u.fishboneRepo.CheckDeviceExists(*fishbone.DeviceEndID) - if err != nil { - return err - } - if !deviceExists { - return errors.New("end device not found") - } - updates["dev_end_id"] = *fishbone.DeviceEndID - } - if fishbone.CoreAmount != nil { updates["core_amount"] = *fishbone.CoreAmount } + if fishbone.DeviceStartID != nil { + exists, err := u.deviceDetailsRepo.CheckDeviceExists(*fishbone.DeviceStartID) + if err != nil { + return fmt.Errorf("error checking new start device: %w", err) + } + if !exists { + return fmt.Errorf("new start device does not exist") + } + + coreAmount := originalFishbone.CoreAmount + if fishbone.CoreAmount != nil { + coreAmount = *fishbone.CoreAmount + } + + if err := u.deviceDetailsRepo.ValidatePortAvailability(*fishbone.DeviceStartID, coreAmount); err != nil { + return fmt.Errorf("new start device: %w", err) + } + + updates["dev_start_id"] = *fishbone.DeviceStartID + devicesToUpdate[*fishbone.DeviceStartID] = true + } + + if fishbone.DeviceEndID != nil { + exists, err := u.deviceDetailsRepo.CheckDeviceExists(*fishbone.DeviceEndID) + if err != nil { + return fmt.Errorf("error checking new end device: %w", err) + } + if !exists { + return fmt.Errorf("new end device does not exist") + } + + coreAmount := originalFishbone.CoreAmount + if fishbone.CoreAmount != nil { + coreAmount = *fishbone.CoreAmount + } + + if err := u.deviceDetailsRepo.ValidatePortAvailability(*fishbone.DeviceEndID, coreAmount); err != nil { + return fmt.Errorf("new end device: %w", err) + } + + updates["dev_end_id"] = *fishbone.DeviceEndID + devicesToUpdate[*fishbone.DeviceEndID] = true + } + + if fishbone.BackboneID != nil { + updates["backbone_id"] = *fishbone.BackboneID + } + + if fishbone.FishboneCode != nil { + updates["fishbone_code"] = *fishbone.FishboneCode + } + if len(updates) == 0 { - return errors.New("no fields to update") + return fmt.Errorf("no fields to update") } updates["updated_at"] = time.Now() - return u.fishboneRepo.Update(id, updates) + // Update fishbone + err = u.fishboneRepo.Update(id, updates) + if err != nil { + return err + } + + // Recalculate port usage for all affected devices + for deviceID := range devicesToUpdate { + u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) + } + + return nil } -func (u *fishboneUsecase) DeleteFishbone(id uuid.UUID) error { +func (u *fishboneUseCase) DeleteFishbone(id uuid.UUID) error { // Check if fishbone exists exists, err := u.fishboneRepo.CheckFishboneExists(id) if err != nil { @@ -178,7 +234,7 @@ func (u *fishboneUsecase) DeleteFishbone(id uuid.UUID) error { return u.fishboneRepo.Delete(id) } -func (u *fishboneUsecase) GetFishboneStats() (map[string]interface{}, error) { +func (u *fishboneUseCase) GetFishboneStats() (map[string]interface{}, error) { fishbones, err := u.fishboneRepo.GetAll() if err != nil { return nil, err 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/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..5f054c0 --- /dev/null +++ b/utils/helper/device_details.go @@ -0,0 +1,125 @@ +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 + towerInfos := make([]res.TowerConnectionDetail, 0) + for _, tower := range device.Towers { + distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude) + + info := res.TowerConnectionDetail{ + ID: tower.ID, + TowerCode: tower.TowerCode, + Distance: distance, + ImageURL: &tower.ImageURL, + } + towerInfos = append(towerInfos, info) + } + + 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, + 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/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 763cc42..b6b9f69 100644 --- a/utils/helper/towerHelperRes.go +++ b/utils/helper/towerHelperRes.go @@ -29,7 +29,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe towerResp := res.TowerResponse{ ID: tower.ID, DeviceCode: tower.Device.DeviceCode, - TowerCode: tower.TowerCode, + TowerCode: &tower.TowerCode, Longitude: tower.Longitude, Latitude: tower.Latitude, Address: address, @@ -59,7 +59,7 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer towerResp := res.TowerResponse{ ID: tower.ID, DeviceCode: tower.Device.DeviceCode, - TowerCode: tower.TowerCode, + TowerCode: &tower.TowerCode, Longitude: tower.Longitude, Latitude: tower.Latitude, Address: address,