Merge pull request 'feature/responses-v2' (#15) from feature/responses-v2 into dev

Reviewed-on: winter-access/backend_nam#15
This commit is contained in:
areeqakbr 2025-06-15 02:10:06 +00:00
commit 1c1287d075
89 changed files with 7013 additions and 647 deletions

3
.gitignore vendored
View File

@ -41,4 +41,5 @@ node_modules/
.env
# Coverage files
coverage.out
coverage.out
uploads/towers

View File

@ -2,8 +2,10 @@ package config
import (
"errors"
"github.com/joho/godotenv"
"os"
"strconv"
"github.com/joho/godotenv"
)
type DbConfig struct {
@ -17,6 +19,7 @@ type LoginConfig struct {
LoginAPI string
AuthMeAPI string
LogoutAPI string
SkipSSLVerification bool
}
type TokenConfig struct {
@ -67,10 +70,17 @@ func (c *Config) readConfig() error {
TokenApiKey: os.Getenv("TOKEN_API_KEY"),
}
skipSSL, err := strconv.ParseBool(os.Getenv("SKIP_SSL_VERIFICATION"))
if err != nil {
return errors.New("failed to read SKIP_SSL_VERIFICATION environment variable")
}
c.LoginConfig = LoginConfig{
LoginAPI: os.Getenv("API_LOGIN_URL"),
AuthMeAPI: os.Getenv("API_ME_URL"),
LogoutAPI: os.Getenv("API_LOGOUT_URL"),
SkipSSLVerification: skipSSL,
}
if c.ApiConfig.ApiPort == "" {

View File

@ -0,0 +1,144 @@
package controller
import (
"net/http"
"strconv"
"users_management/m/middleware"
"users_management/m/usecase"
"users_management/m/utils/common"
"users_management/m/utils/helper"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type ActivityLogController struct {
activityLogUC usecase.ActivityLogUseCase
rg *gin.RouterGroup
}
func NewActivityLogController(activityLogUC usecase.ActivityLogUseCase, rg *gin.RouterGroup) *ActivityLogController {
return &ActivityLogController{
activityLogUC: activityLogUC,
rg: rg,
}
}
func (c *ActivityLogController) Route() {
logs := c.rg.Group("/logs")
{
// Users can see their own logs (simplified response)
logs.GET("/my-logs", c.getMyLogs)
// Only admins and superadmins can see all logs (detailed response)
logs.GET("/all", middleware.RequireAdminRole(), c.getAllLogs)
// Admins and superadmins can see teknisi logs specifically (detailed response)
logs.GET("/teknisi", middleware.RequireAdminRole(), c.getTeknisinLogs)
}
}
func (c *ActivityLogController) getMyLogs(ctx *gin.Context) {
userID, exists := ctx.Get("userID")
if !exists {
common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found")
return
}
uid, ok := userID.(uuid.UUID)
if !ok {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID")
return
}
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10"))
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
logs, total, err := c.activityLogUC.GetUserLogs(uid, page, limit)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
// Convert to simplified response using helper
simplifiedLogs := helper.ConvertToActivityLogResponses(logs)
response := gin.H{
"logs": simplifiedLogs,
"total": total,
"page": page,
"limit": limit,
"total_pages": (total + int64(limit) - 1) / int64(limit),
}
common.SingleResponses(ctx, "User logs retrieved successfully", response)
}
func (c *ActivityLogController) getAllLogs(ctx *gin.Context) {
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10"))
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
logs, total, err := c.activityLogUC.GetAllLogs(page, limit)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
// Convert to detailed response for admins using helper
detailedLogs := helper.ConvertToActivityLogDetailResponses(logs)
response := gin.H{
"logs": detailedLogs,
"total": total,
"page": page,
"limit": limit,
"total_pages": (total + int64(limit) - 1) / int64(limit),
}
common.SingleResponses(ctx, "All logs retrieved successfully", response)
}
func (c *ActivityLogController) getTeknisinLogs(ctx *gin.Context) {
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10"))
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
logs, total, err := c.activityLogUC.GetTeknisinLogs(page, limit)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
// Convert to detailed response for admins using helper
detailedLogs := helper.ConvertToActivityLogDetailResponses(logs)
response := gin.H{
"logs": detailedLogs,
"total": total,
"page": page,
"limit": limit,
"total_pages": (total + int64(limit) - 1) / int64(limit),
}
common.SingleResponses(ctx, "Teknisi logs retrieved successfully", response)
}

View File

@ -1,6 +1,7 @@
package controller
import (
"log"
"net/http"
"users_management/m/middleware"
"users_management/m/model/dto/req"
@ -18,15 +19,13 @@ type BackboneController struct {
func (bc *BackboneController) Route() {
rg := bc.rg.Group("/backbone")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.RateLimitMiddleware())
{
rg.GET("", bc.GetBackbone())
rg.POST("", bc.CreateBackbone())
rg.GET("/:uuid", bc.GetBackboneByID())
rg.PUT("/:uuid", bc.UpdateBackbone())
rg.POST("", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), bc.CreateBackbone())
rg.PUT("/:uuid", middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"), bc.UpdateBackbone())
}
}
@ -56,6 +55,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc {
err := c.ShouldBindJSON(&backboneDTO)
if err != nil {
log.Println("Error binding JSON:", err)
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
@ -63,6 +63,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc {
err = bc.bu.CreateBackbone(backboneDTO)
if err != nil {
log.Println("Error creating backbone:", err)
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}

View File

@ -0,0 +1,35 @@
package controller
import (
"net/http"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
)
type CountAssetsController struct {
countAssetsUC usecase.CountAssetsUseCase
rg *gin.RouterGroup
}
func NewCountAssetsController(countAssetsUC usecase.CountAssetsUseCase, rg *gin.RouterGroup) *CountAssetsController {
return &CountAssetsController{
countAssetsUC: countAssetsUC,
rg: rg,
}
}
func (c *CountAssetsController) Route() {
c.rg.GET("/count-assets", c.getCounts)
}
func (c *CountAssetsController) getCounts(ctx *gin.Context) {
counts, err := c.countAssetsUC.GetCounts()
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Count assets retrieved successfully", counts)
}

View File

@ -2,7 +2,6 @@ package controller
import (
"net/http"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
"users_management/m/utils/common"
@ -18,9 +17,6 @@ type DevicePortController struct {
func (dc *DevicePortController) Route() {
rg := dc.rg.Group("/device-port")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.RateLimitMiddleware())
rg.Use(middleware.CORSMiddleware())
{
rg.GET("", dc.GetDevicePort())
rg.POST("", dc.CreateDevicePort())

View File

@ -0,0 +1,331 @@
package controller
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type DeviceDetailsController struct {
deviceDetailsUC usecase.DeviceDetailsUseCase
rg *gin.RouterGroup
}
func NewDeviceDetailsController(deviceDetailsUC usecase.DeviceDetailsUseCase, rg *gin.RouterGroup) *DeviceDetailsController {
return &DeviceDetailsController{
deviceDetailsUC: deviceDetailsUC,
rg: rg,
}
}
func (c *DeviceDetailsController) Route() {
deviceDetails := c.rg.Group("/device-details")
deviceDetails.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"))
{
deviceDetails.GET("", c.getAllDeviceDetails)
deviceDetails.POST("", c.createDeviceDetails)
deviceDetails.GET("/:id", c.getDeviceDetailsByID)
deviceDetails.PUT("/:id", c.updateDeviceDetails)
deviceDetails.DELETE("/:id", c.deleteDeviceDetails)
deviceDetails.POST("/:id/recalculate-ports", c.recalculatePortUsage)
deviceDetails.POST("/:id/assign-customer", c.assignCustomer)
deviceDetails.PUT("/:id/port-usage", c.updatePortUsage) // New endpoint
deviceDetails.PUT("/:id/port-assignments", c.updatePortAssignments) // New endpoint
deviceDetails.PUT("/:id/update-customer-by-port", c.updateCustomerByPort) // Updated endpoint name
deviceDetails.PUT("/:id/bulk-update-customers-by-port", c.bulkUpdateCustomersByPort) // Updated endpoint name
deviceDetails.DELETE("/:id/remove-customer-by-port", c.removeCustomerByPort)
}
}
func (c *DeviceDetailsController) updateCustomerByPort(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.UpdateCustomerByPortDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.UpdateCustomerByPort(deviceID, request)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
if request.NewCustomerName == nil {
common.SingleResponses(ctx, fmt.Sprintf("Customer removed from port %d successfully", request.PortNumber), nil)
} else {
common.SingleResponses(ctx, fmt.Sprintf("Customer assigned to port %d successfully", request.PortNumber), nil)
}
}
func (c *DeviceDetailsController) bulkUpdateCustomersByPort(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.BulkUpdateCustomersByPortDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.BulkUpdateCustomersByPort(deviceID, request.Updates)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, fmt.Sprintf("%d port assignments updated successfully", len(request.Updates)), nil)
}
func (c *DeviceDetailsController) removeCustomerByPort(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.RemoveCustomerByPortDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.RemoveCustomerByPort(deviceID, request.PortNumber)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, fmt.Sprintf("Customer removed from port %d successfully", request.PortNumber), nil)
}
func (c *DeviceDetailsController) assignCustomer(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
// Try to parse as array first
var arrayRequest []req.AssignMultipleCustomersDTO
// Try to parse as single object
var singleRequest struct {
CustomerName string `json:"customer_name" binding:"required"`
PortNumber *int `json:"port_number,omitempty"`
}
// Get raw JSON to determine the structure
rawData, err := ctx.GetRawData()
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid JSON data")
return
}
// Reset the request body for subsequent parsing
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(rawData))
// Check if it's an array by looking at the first character
trimmed := bytes.TrimSpace(rawData)
if len(trimmed) > 0 && trimmed[0] == '[' {
// It's an array
if err := json.Unmarshal(rawData, &arrayRequest); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid array format: "+err.Error())
return
}
// Process array of assignments
err = c.deviceDetailsUC.AssignMultipleCustomersToPort(deviceID, arrayRequest)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, fmt.Sprintf("%d customers assigned successfully", len(arrayRequest)), nil)
} else {
// It's a single object
if err := json.Unmarshal(rawData, &singleRequest); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid object format: "+err.Error())
return
}
// Process single assignment
err = c.deviceDetailsUC.AssignCustomerToPort(deviceID, singleRequest.CustomerName, singleRequest.PortNumber)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Customer assigned successfully", nil)
}
}
func (c *DeviceDetailsController) getAllDeviceDetails(ctx *gin.Context) {
devices, err := c.deviceDetailsUC.GetAllDeviceDetails()
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Device details retrieved successfully", devices)
}
func (c *DeviceDetailsController) createDeviceDetails(ctx *gin.Context) {
var request req.DeviceDetailsDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err := c.deviceDetailsUC.CreateDeviceDetails(request)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Device details created successfully", nil)
}
func (c *DeviceDetailsController) getDeviceDetailsByID(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
device, err := c.deviceDetailsUC.GetDeviceDetailsByID(deviceID)
if err != nil {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
common.SingleResponses(ctx, "Device details retrieved successfully", device)
}
func (c *DeviceDetailsController) updateDeviceDetails(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.UpdateDeviceDetailsDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.UpdateDeviceDetails(deviceID, request)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Device details updated successfully", nil)
}
func (c *DeviceDetailsController) deleteDeviceDetails(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
err = c.deviceDetailsUC.DeleteDeviceDetails(deviceID)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Device details deleted successfully", nil)
}
func (c *DeviceDetailsController) recalculatePortUsage(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
err = c.deviceDetailsUC.RecalculatePortUsage(deviceID)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Port usage recalculated successfully", nil)
}
func (c *DeviceDetailsController) updatePortUsage(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.UpdatePortUsageDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.UpdatePortUsage(deviceID, request.PortUsed)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Port usage updated successfully", nil)
}
func (c *DeviceDetailsController) updatePortAssignments(ctx *gin.Context) {
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
var request req.UpdatePortAssignmentsDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.deviceDetailsUC.UpdatePortAssignments(deviceID, request.PortAssignments)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "Port assignments updated successfully", nil)
}

View File

@ -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)
}

View File

@ -18,9 +18,7 @@ type DeviceController struct {
func (dc *DeviceController) Route() {
rg := dc.rg.Group("/devices")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.RateLimitMiddleware())
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"))
{
rg.POST("", dc.CreateDevice())
rg.GET("", dc.GetAllDevices())

View File

@ -1,6 +1,7 @@
package controller
import (
"log"
"net/http"
"users_management/m/middleware"
"users_management/m/model/dto/req"
@ -18,14 +19,14 @@ type FishboneController struct {
func (fc *FishboneController) Route() {
rg := fc.rg.Group("/fishbone")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.RateLimitMiddleware())
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin"))
// Apply middleware to all routes
{
rg.GET("", fc.GetFishbone())
rg.POST("", fc.CreateFishbone())
rg.GET("/:uuid", fc.GetFishboneByID())
rg.PUT("/:uuid", fc.UpdateFishbone())
rg.GET("/backbone/:backbone_id", fc.GetFishboneByBackboneID())
}
}
@ -36,83 +37,151 @@ func NewFishboneController(fu usecase.FishboneUseCase, rg *gin.RouterGroup) *Fis
}
}
func (fc *FishboneController) GetFishboneByBackboneID() gin.HandlerFunc {
return func(c *gin.Context) {
backboneIDStr := c.Param("backbone_id")
backboneID, err := uuid.Parse(backboneIDStr)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid backbone ID format")
return
}
fishbones, err := fc.fu.GetByBackboneID(backboneID)
if err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
return
}
if len(fishbones) == 0 {
common.SingleResponses(c, "No fishbones found for this backbone", []interface{}{})
return
}
common.SingleResponses(c, "Success", fishbones)
}
}
func (fc *FishboneController) GetFishbone() gin.HandlerFunc {
return func(c *gin.Context) {
fishbones, err := fc.fu.GetAllFishbone()
return func(c *gin.Context) {
log.Println("Fetching all fishbones")
fishbones, err := fc.fu.GetAllFishbone()
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Success", fishbones)
}
common.SingleResponses(c, "Success", fishbones)
}
}
func (fc *FishboneController) CreateFishbone() gin.HandlerFunc {
return func(c *gin.Context) {
var fishboneDTO req.FishboneDTO
err := c.ShouldBindJSON(&fishboneDTO)
return func(c *gin.Context) {
var fishboneDTO req.FishboneDTO
err := c.ShouldBindJSON(&fishboneDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
err = fc.fu.CreateFishbone(fishboneDTO)
err = fc.fu.CreateFishbone(fishboneDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Fishbone has been created", nil)
}
common.SingleResponses(c, "Fishbone has been created successfully", nil)
}
}
func (fc *FishboneController) GetFishboneByID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("uuid")
uuid, err := uuid.Parse(id)
if err != nil{
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
fishbone, err := fc.fu.GetByID(uuid)
return func(c *gin.Context) {
id := c.Param("uuid")
fishboneUUID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format")
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
fishbone, err := fc.fu.GetByID(fishboneUUID)
if err != nil {
if err.Error() == "fishbone not found" {
common.ErrorResponses(c, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(c, "Success", fishbone)
}
common.SingleResponses(c, "Success", fishbone)
}
}
func (fc *FishboneController) UpdateFishbone() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("uuid")
uuid, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
return func(c *gin.Context) {
id := c.Param("uuid")
fishboneUUID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format")
return
}
var fishboneDTO req.UpdateFishboneDTO
err = c.ShouldBindJSON(&fishboneDTO)
var fishboneDTO req.UpdateFishboneDTO
err = c.ShouldBindJSON(&fishboneDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
err = fc.fu.UpdateFishbone(uuid, fishboneDTO)
err = fc.fu.UpdateFishbone(fishboneUUID, fishboneDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
if err.Error() == "fishbone not found" {
common.ErrorResponses(c, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Fishbone has been updated", nil)
}
common.SingleResponses(c, "Fishbone has been updated successfully", nil)
}
}
func (fc *FishboneController) DeleteFishbone() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("uuid")
fishboneUUID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID format")
return
}
err = fc.fu.DeleteFishbone(fishboneUUID)
if err != nil {
if err.Error() == "fishbone not found" {
common.ErrorResponses(c, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(c, "Fishbone has been deleted successfully", nil)
}
}
func (fc *FishboneController) GetFishboneStats() gin.HandlerFunc {
return func(c *gin.Context) {
stats, err := fc.fu.GetFishboneStats()
if err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(c, "Success", stats)
}
}

View File

@ -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)
}

View File

@ -2,6 +2,8 @@ package controller
import (
"net/http"
"strconv"
"strings"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -18,12 +20,11 @@ type TowerController struct {
func (tc *TowerController) Route() {
rg := tc.rg.Group("/tower")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.RateLimitMiddleware())
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.RequireAnyRole("Teknisi", "Admin", "Superadmin")) // Apply middleware to all routes
{
rg.GET("", tc.GetTower())
rg.POST("", tc.CreateTower())
rg.GET("/:uuid", tc.GetTowerByID())
rg.PUT("/:uuid", tc.UpdateTower())
}
@ -50,24 +51,103 @@ func (tc *TowerController) GetTower() gin.HandlerFunc {
}
func (tc *TowerController) CreateTower() gin.HandlerFunc {
return func(c *gin.Context) {
var towerDTO req.TowerDTO
err := c.ShouldBindJSON(&towerDTO)
return func(c *gin.Context) {
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
contentType := c.GetHeader("Content-Type")
// Handle JSON request
if strings.Contains(contentType, "application/json") {
var towerDTO req.TowerDTO
if err := c.ShouldBindJSON(&towerDTO); err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid JSON format: "+err.Error())
return
}
err = tc.tu.Post(towerDTO)
// No image file for JSON requests
err := tc.tu.Post(towerDTO, nil)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Tower has been created", nil)
return
}
// Parse multipart form
err := c.Request.ParseMultipartForm(10 << 20) // 10MB max
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
return
}
common.SingleResponses(c, "Tower has been created", nil)
}
// Extract form data
deviceIDStr := c.PostForm("dev_id")
towerCode := c.PostForm("tower_code")
longitudeStr := c.PostForm("longitude")
latitudeStr := c.PostForm("latitude")
externalTowerStr := c.PostForm("external_tower")
// Validate required fields
if towerCode == "" || longitudeStr == "" || latitudeStr == "" {
common.ErrorResponses(c, http.StatusBadRequest, "Missing required fields")
return
}
var deviceID *uuid.UUID
if deviceIDStr != "" {
parsedDeviceID, err := uuid.Parse(deviceIDStr)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID")
return
}
deviceID = &parsedDeviceID
}
// Parse coordinates
longitude, err := strconv.ParseFloat(longitudeStr, 64)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid longitude")
return
}
latitude, err := strconv.ParseFloat(latitudeStr, 64)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid latitude")
return
}
var externalTower *bool
if externalTowerStr != "" {
parsedExternalTower, err := strconv.ParseBool(externalTowerStr)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid external_tower value")
return
}
externalTower = &parsedExternalTower
}
// Create DTO
towerDTO := req.TowerDTO{
DeviceID: deviceID,
TowerCode: towerCode,
Longitude: longitude,
Latitude: latitude,
ExternalTower: externalTower,
}
// Get image file (optional)
imageFile, _ := c.FormFile("image")
err = tc.tu.Post(towerDTO, imageFile)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Tower has been created", nil)
}
}
func (tc *TowerController) GetTowerByID() gin.HandlerFunc {
@ -90,27 +170,102 @@ func (tc *TowerController) GetTowerByID() gin.HandlerFunc {
}
func (tc *TowerController) UpdateTower() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("uuid")
uuid, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
}
var towerUpdateDTO req.UpdateTowerDTO
err = c.ShouldBindJSON(&towerUpdateDTO)
return func(c *gin.Context) {
id := c.Param("uuid")
tower_uuid, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
contentType := c.GetHeader("Content-Type")
// Handle JSON request
if strings.Contains(contentType, "application/json") {
var towerUpdateDTO req.UpdateTowerDTO
if err := c.ShouldBindJSON(&towerUpdateDTO); err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid JSON format: "+err.Error())
return
}
err = tc.tu.UpdateTower(uuid, towerUpdateDTO)
// No image file for JSON requests
err := tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, nil)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Tower has been updated", nil)
return
}
common.SingleResponses(c, "Tower has been updated", nil)
}
// Handle multipart form request (existing logic)
err = c.Request.ParseMultipartForm(10 << 20) // 10MB max
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
return
}
// Create update DTO
towerUpdateDTO := req.UpdateTowerDTO{}
// Optional fields - handle form data
if deviceIDStr := c.PostForm("device_id"); deviceIDStr != "" {
if deviceID, err := uuid.Parse(deviceIDStr); err == nil {
towerUpdateDTO.DeviceID = &deviceID
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID format")
return
}
}
if towerCode := c.PostForm("tower_code"); towerCode != "" {
towerUpdateDTO.TowerCode = &towerCode
}
if longitudeStr := c.PostForm("longitude"); longitudeStr != "" {
if longitude, err := strconv.ParseFloat(longitudeStr, 64); err == nil {
towerUpdateDTO.Longitude = &longitude
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid longitude format")
return
}
}
if latitudeStr := c.PostForm("latitude"); latitudeStr != "" {
if latitude, err := strconv.ParseFloat(latitudeStr, 64); err == nil {
towerUpdateDTO.Latitude = &latitude
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid latitude format")
return
}
}
// Handle external_tower field
if externalTowerStr := c.PostForm("external_tower"); externalTowerStr != "" {
if externalTower, err := strconv.ParseBool(externalTowerStr); err == nil {
towerUpdateDTO.ExternalTower = &externalTower
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid external_tower value")
return
}
}
// Handle image_url field (for updating image URL directly)
if imageURL := c.PostForm("image_url"); imageURL != "" {
towerUpdateDTO.ImageURL = &imageURL
}
// Get image file (optional)
imageFile, _ := c.FormFile("image")
err = tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, imageFile)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Tower has been updated", nil)
}
}

View File

@ -0,0 +1,89 @@
package controller
import (
"net/http"
"users_management/m/middleware"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
)
type UserManagementController struct {
userUC usecase.UsersUsecase
rg *gin.RouterGroup
}
func NewUserManagementController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *UserManagementController {
return &UserManagementController{
userUC: userUC,
rg: rg,
}
}
func (c *UserManagementController) Route() {
users := c.rg.Group("/user-management")
{
// Only superadmin can manage user roles
users.PUT("/role", middleware.RequireSuperAdminRole(), c.updateUserRole)
// Admins and superadmins can view all users
users.GET("/users", middleware.RequireAdminRole(), c.getAllUsers)
// Users can view their own profile
users.GET("/profile", c.getMyProfile)
}
}
type UpdateRoleRequest struct {
NomorInduk string `json:"nomor_induk" binding:"required"`
RoleName string `json:"role_name" binding:"required"`
}
func (c *UserManagementController) updateUserRole(ctx *gin.Context) {
var req UpdateRoleRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err := c.userUC.UpdateUserRole(req.NomorInduk, req.RoleName)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "User role updated successfully", nil)
}
func (c *UserManagementController) getAllUsers(ctx *gin.Context) {
users, err := c.userUC.GetAllUsers()
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Users retrieved successfully", users)
}
func (c *UserManagementController) getMyProfile(ctx *gin.Context) {
userID, exists := ctx.Get("userID")
if !exists {
common.ErrorResponses(ctx, http.StatusUnauthorized, "User ID not found")
return
}
nomorInduk, ok := userID.(string)
if !ok {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID")
return
}
user, err := c.userUC.GetUserByNomorInduk(nomorInduk)
if err != nil {
common.ErrorResponses(ctx, http.StatusNotFound, "User not found")
return
}
common.SingleResponses(ctx, "User profile retrieved successfully", user)
}

View File

@ -42,18 +42,31 @@ func NewServer() *Server {
}
func (s *Server) setupController() {
s.engine.Static("/uploads", "./uploads")
s.engine.Use(middleware.CORSMiddleware())
rg := s.engine.Group("/api/v1")
controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route()
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.AuthMiddleware(s.ucManager.NewUserUsecase()))
rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase()))
{
controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route()
controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), rg).Route()
controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), rg).Route()
controller.NewTowerController(s.ucManager.NewTowerUsecase(), rg).Route()
controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), rg).Route()
controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route()
controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), rg).Route()
controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), rg).Route()
controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), rg).Route()
controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), rg).Route()
}
}
func (s *Server) Run() {
s.setupController()
if err := s.engine.Run(s.host); err != nil {

5
go.mod
View File

@ -7,6 +7,8 @@ require (
github.com/go-playground/validator/v10 v10.24.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.33.0
golang.org/x/time v0.10.0
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12
)
@ -15,7 +17,6 @@ require (
github.com/bytedance/sonic v1.12.8 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@ -39,12 +40,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.10.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

2
go.sum
View File

@ -10,8 +10,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=

View File

@ -3,6 +3,8 @@ package manager
import (
"fmt"
"users_management/m/config"
"users_management/m/model/entity"
"gorm.io/driver/postgres"
"gorm.io/gorm"
@ -24,10 +26,30 @@ func (im *infraManager) openConn() error {
if err != nil {
return err
}
err = im.autoMigrate(db)
if err != nil {
return fmt.Errorf("failed to migrate database schema: %w", err)
}
im.db = db
return nil
}
func (im *infraManager) autoMigrate(db *gorm.DB) error {
return db.AutoMigrate(
&entity.Role{},
&entity.User{},
&entity.Device{},
&entity.Backbone{},
&entity.Fishbone{},
&entity.Tower{},
&entity.DevicePort{},
&entity.CountAssets{},
&entity.ActivityLog{},
)
}
func NewInfraManager(cfg *config.Config) (InfraManager, error) {
im := &infraManager{
cfg: cfg,

View File

@ -10,6 +10,13 @@ type RepositoryManager interface {
NewTowerRepository() repository.TowerRepo
NewDevicePortRepository() repository.DevicePortRepo
NewCountAssetsRepository() repository.CountAssetsRepo
NewActivityLogRepository() repository.ActivityLogRepo
NewDeviceInspectionRepository() repository.DeviceInspectionRepo
NewNearestDeviceRepository() repository.NearestDeviceRepo
NewDeviceDetailsRepository() repository.DeviceDetailsRepo
}
type repositoryManager struct {
@ -22,6 +29,13 @@ func NewRepositoryManager(infra InfraManager) RepositoryManager {
}
}
func (rm *repositoryManager) NewDeviceDetailsRepository() repository.DeviceDetailsRepo {
return repository.NewDeviceDetailsRepo(rm.infra.Conn())
}
func (rm *repositoryManager) NewNearestDeviceRepository() repository.NearestDeviceRepo {
return repository.NewNearestDeviceRepo(rm.infra.Conn())
}
func (rm *repositoryManager) NewUserRepository() repository.UsersRepo {
return repository.NewUsersRepo(rm.infra.Conn())
}
@ -44,4 +58,16 @@ func (rm *repositoryManager) NewTowerRepository() repository.TowerRepo {
func (rm *repositoryManager) NewDevicePortRepository() repository.DevicePortRepo {
return repository.NewDevicePortRepo(rm.infra.Conn())
}
func (rm *repositoryManager) NewCountAssetsRepository() repository.CountAssetsRepo {
return repository.NewCountAssetsRepo(rm.infra.Conn())
}
func (rm *repositoryManager) NewActivityLogRepository() repository.ActivityLogRepo {
return repository.NewActivityLogRepo(rm.infra.Conn())
}
func (rm *repositoryManager) NewDeviceInspectionRepository() repository.DeviceInspectionRepo {
return repository.NewDeviceInspectionRepo(rm.infra.Conn())
}

View File

@ -16,6 +16,16 @@ type UsecaseManager interface {
NewTowerUsecase() usecase.TowerUseCase
NewDevicePortUsecase() usecase.DevicePortUseCase
NewCountAssetsUsecase() usecase.CountAssetsUseCase
NewActivityLogUsecase() usecase.ActivityLogUseCase
NewDeviceInspectionUsecase() usecase.DeviceInspectionUseCase
NewNearestDeviceUsecase() usecase.NearestDeviceUseCase
NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase
}
type usecaseManager struct {
@ -30,6 +40,20 @@ func NewUsecaseManager(repo RepositoryManager, cfg *config.Config) UsecaseManage
return &usecaseManager{repo: repo,cfg: cfg , geocoder: cachedGeocoder}
}
func (um *usecaseManager) NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase {
return usecase.NewDeviceDetailsUseCase(
um.repo.NewDeviceDetailsRepository(),
um.geocoder,
)
}
func (um *usecaseManager) NewNearestDeviceUsecase() usecase.NearestDeviceUseCase {
return usecase.NewNearestDeviceUseCase(
um.repo.NewNearestDeviceRepository(),
um.geocoder,
)
}
func (um *usecaseManager) NewUserUsecase() usecase.UsersUsecase {
return usecase.NewUsersUsecase(um.repo.NewUserRepository())
}
@ -43,11 +67,20 @@ func (um *usecaseManager) NewDeviceUsecase() usecase.DeviceUseCase {
}
func (um *usecaseManager) NewBackboneUsecase() usecase.BackboneUseCase {
return usecase.NewBackboneUseCase(um.repo.NewBackboneRepository(),um.repo.NewFishboneRepository())
return usecase.NewBackboneUseCase(
um.repo.NewBackboneRepository(),
um.repo.NewFishboneRepository(),
um.repo.NewDeviceDetailsRepository(), // Add this
)
}
func (um *usecaseManager) NewFishboneUsecase() usecase.FishboneUseCase {
return usecase.NewFishboneUseCase(um.repo.NewFishboneRepository())
return usecase.NewFishboneUseCase(
um.repo.NewFishboneRepository(),
um.repo.NewBackboneRepository(),
um.repo.NewDeviceDetailsRepository(), // Add this line
)
}
func (um *usecaseManager) NewTowerUsecase() usecase.TowerUseCase {
@ -56,4 +89,21 @@ func (um *usecaseManager) NewTowerUsecase() usecase.TowerUseCase {
func (um *usecaseManager) NewDevicePortUsecase() usecase.DevicePortUseCase {
return usecase.NewDevicePortUseCase(um.repo.NewDevicePortRepository())
}
func (um *usecaseManager) NewCountAssetsUsecase() usecase.CountAssetsUseCase {
return usecase.NewCountAssetsUseCase(um.repo.NewCountAssetsRepository())
}
func (um *usecaseManager) NewActivityLogUsecase() usecase.ActivityLogUseCase {
return usecase.NewActivityLogUseCase(um.repo.NewActivityLogRepository())
}
func (um *usecaseManager) NewDeviceInspectionUsecase() usecase.DeviceInspectionUseCase {
return usecase.NewDeviceInspectionUseCase(
um.repo.NewDeviceInspectionRepository(),
um.NewActivityLogUsecase(),
service.NewGeocodingService(), // Add the geocoding service
)
}

View File

@ -0,0 +1,141 @@
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"users_management/m/usecase"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type responseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r responseWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
func ActivityLoggingMiddleware(activityLogUC usecase.ActivityLogUseCase) gin.HandlerFunc {
return func(c *gin.Context) {
// Skip logging for certain endpoints
if shouldSkipLogging(c.Request.URL.Path) {
c.Next()
return
}
// Get user info from context (set by auth middleware)
userID, userExists := c.Get("userID")
if !userExists {
c.Next()
return
}
// Convert userID to UUID
uid, ok := userID.(uuid.UUID)
if !ok {
c.Next()
return
}
// Read request body
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
// Wrap response writer to capture response
w := &responseWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
c.Writer = w
// Process request
c.Next()
// Log the activity in background
go func() {
action := getActionFromMethod(c.Request.Method)
resource := getResourceFromPath(c.Request.URL.Path)
resourceID := getResourceIDFromPath(c.Request.URL.Path)
var oldData, newData interface{}
// For updates, you might want to fetch old data before the operation
// This is a simplified version
if len(requestBody) > 0 {
json.Unmarshal(requestBody, &newData)
}
details := ""
if c.Writer.Status() >= 400 {
details = fmt.Sprintf("Request failed with status: %d", c.Writer.Status())
}
activityLogUC.LogActivity(
uid,
action,
resource,
resourceID,
oldData,
newData,
c.ClientIP(),
c.Request.UserAgent(),
details,
)
}()
}
}
func shouldSkipLogging(path string) bool {
skipPaths := []string{
"/api/v1/logs",
"/api/v1/users/login",
"/api/v1/users/logout",
"/uploads",
}
for _, skipPath := range skipPaths {
if strings.HasPrefix(path, skipPath) {
return true
}
}
return false
}
func getActionFromMethod(method string) string {
switch method {
case "POST":
return "CREATE"
case "PUT", "PATCH":
return "UPDATE"
case "DELETE":
return "DELETE"
case "GET":
return "READ"
default:
return method
}
}
func getResourceFromPath(path string) string {
parts := strings.Split(path, "/")
if len(parts) >= 4 {
return parts[3] // /api/v1/devices -> devices
}
return "unknown"
}
func getResourceIDFromPath(path string) *string {
parts := strings.Split(path, "/")
if len(parts) >= 5 {
id := parts[4]
return &id
}
return nil
}

View File

@ -1,67 +1,96 @@
package middleware
import (
"crypto/tls"
"encoding/json"
"log"
"net/http"
"strings"
"time"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
func AuthMiddleware(userUC usecase.UsersUsecase) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required")
c.Abort()
return
}
if token == "" {
common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required")
c.Abort()
return
}
token = strings.TrimPrefix(token, "Bearer ")
token = strings.TrimPrefix(token, "Bearer ")
c.Set("token", token)
c.Set("token", token)
req, err := http.NewRequest("POST", "https://demo.api-hrm.winteraccess.id/api/v2/auth/me", nil)
req, err := http.NewRequest("POST", "https://demo.api-hrm.winteraccess.id/api/v2/auth/me", nil)
if err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
c.Abort()
return
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
c.Abort()
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
common.ErrorResponses(c, http.StatusUnauthorized, "Unauthroized")
c.Abort()
return
}
var authResponse res.AuthMeResponse
if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil {
if err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
c.Abort()
return
}
c.Set("userID", authResponse.Data.NomorInduk)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
c.Next()
var client *http.Client
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
c.Abort()
return
}
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
common.ErrorResponses(c, http.StatusUnauthorized, "Unauthorized")
c.Abort()
return
}
var authResponse res.AuthMeResponse
if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
c.Abort()
return
}
// Set basic user info from external API
var user entity.User
// Check if user exists in local database
user, err = userUC.GetUserByUsername(strings.ToLower(authResponse.Data.Username))
c.Set("userID", user.ID)
c.Set("userName", user.Username)
log.Println("User data from local DB:", user.ID)
if err != nil {
defaultRole := "Teknisi"
c.Set("userRole", defaultRole)
} else {
// User exists in local DB, use their assigned role
c.Set("userRole", user.Role.Name)
}
c.Next()
}
}

View File

@ -1,23 +1,61 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // Change to specific domains if needed
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
// Define allowed origins (add your React app's URL)
allowedOrigins := []string{
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000",
"http://127.0.0.1:5173", // Add production URL
}
// Check if origin is in allowed list
isAllowed := false
for _, allowed := range allowedOrigins {
if origin == allowed {
isAllowed = true
break
}
}
// For development, also allow localhost variations
if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") {
isAllowed = true
}
if origin != "" && isAllowed {
// For allowed origins, set specific origin and enable credentials
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
} else if origin == "" {
// For requests without origin (direct API calls, mobile apps, etc.)
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
// Don't set credentials for wildcard
} else {
// For disallowed origins, still set basic CORS but no credentials
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
}
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE, PATCH")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
// Handle preflight requests
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
// Allow OPTIONS method to pass through
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
c.Next()
}
}

View File

@ -1,18 +1,20 @@
package middleware
import (
"net/http"
"sync"
"time"
"users_management/m/utils/common"
"log"
"net/http"
"sync"
"time"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/time/rate"
)
var (
rateLimiters = make(map[string]*rate.Limiter)
mu sync.Mutex
rateLimiters = make(map[string]*rate.Limiter)
mu sync.Mutex
)
func getRateLimiter(userID string) *rate.Limiter {
@ -21,7 +23,7 @@ func getRateLimiter(userID string) *rate.Limiter {
limiter, exists := rateLimiters[userID]
if !exists {
limiter = rate.NewLimiter(rate.Every(1*time.Minute), 50) // 1 request per second with a burst of 2 requests
limiter = rate.NewLimiter(rate.Every(1*time.Minute), 50) // 50 requests per minute
rateLimiters[userID] = limiter
}
@ -29,32 +31,49 @@ func getRateLimiter(userID string) *rate.Limiter {
}
func getLoginLimiter() *rate.Limiter {
mu.Lock()
defer mu.Unlock()
mu.Lock()
defer mu.Unlock()
limiter, exists := rateLimiters["login"]
if !exists {
limiter = rate.NewLimiter(rate.Every(1*time.Minute), 10) // 5 request per second with a burst of 10 requests
rateLimiters["login"] = limiter
}
limiter, exists := rateLimiters["login"]
if !exists {
limiter = rate.NewLimiter(rate.Every(1*time.Minute), 10) // 10 requests per minute for login
rateLimiters["login"] = limiter
}
return limiter
return limiter
}
func RateLimitMiddleware() gin.HandlerFunc{
return func(c *gin.Context) {
if c.Request.Method == http.MethodOptions {
c.Next()
return
}
userID, exists := c.Get("userID")
return func(c *gin.Context) {
if c.Request.Method == http.MethodOptions {
c.Next()
return
}
userID, exists := c.Get("userID")
if !exists {
common.ErrorResponses(c, http.StatusUnauthorized, "Unauthorized: No user ID found")
c.Abort()
return
}
limiter := getRateLimiter(userID.(string))
// Convert UUID to string for rate limiting
var userIDStr string
switch v := userID.(type) {
case uuid.UUID:
userIDStr = v.String()
case string:
userIDStr = v
default:
log.Printf("Unexpected userID type: %T", userID)
common.ErrorResponses(c, http.StatusInternalServerError, "Invalid user ID type")
c.Abort()
return
}
log.Printf("User ID for rate limiting: %s", userIDStr)
limiter := getRateLimiter(userIDStr)
if !limiter.Allow() {
common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests")
@ -62,24 +81,23 @@ func RateLimitMiddleware() gin.HandlerFunc{
return
}
c.Next()
}
c.Next()
}
}
func RateLoginMiddleware() gin.HandlerFunc{
return func(c *gin.Context) {
if c.Request.Method == http.MethodOptions {
c.Next()
return
}
limiter := getLoginLimiter()
if !limiter.Allow() {
common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests")
c.Abort()
return
}
c.Next()
}
}
return func(c *gin.Context) {
if c.Request.Method == http.MethodOptions {
c.Next()
return
}
limiter := getLoginLimiter()
if !limiter.Allow() {
common.ErrorResponses(c, http.StatusTooManyRequests, "Too many requests")
c.Abort()
return
}
c.Next()
}
}

161
middleware/rbac.go Normal file
View File

@ -0,0 +1,161 @@
package middleware
import (
"net/http"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
)
// RequireRole middleware to check if user has required role
func RequireRole(allowedRoles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("userRole")
if !exists {
common.ErrorResponses(c, http.StatusUnauthorized, "User role not found")
c.Abort()
return
}
role, ok := userRole.(string)
if !ok {
common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role")
c.Abort()
return
}
// Check if user role is in allowed roles
for _, allowedRole := range allowedRoles {
if role == allowedRole {
c.Next()
return
}
}
common.ErrorResponses(c, http.StatusForbidden, "Insufficient permissions")
c.Abort()
}
}
// RequireAnyRole middleware - user needs at least one of the specified roles
func RequireAnyRole(roles ...string) gin.HandlerFunc {
return RequireRole(roles...)
}
// RequireAdminRole middleware for admin-only access
func RequireAdminRole() gin.HandlerFunc {
return RequireRole("Admin", "Superadmin")
}
// RequireSuperAdminRole middleware for superadmin-only access
func RequireSuperAdminRole() gin.HandlerFunc {
return RequireRole("Superadmin")
}
// RequireTeknisiRole middleware for teknisi access
func RequireTeknisiRole() gin.HandlerFunc {
return RequireRole("Teknisi")
}
// RequireNonTeknisiRole middleware to block teknisi users
func RequireNonTeknisiRole() gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("userRole")
if !exists {
common.ErrorResponses(c, http.StatusUnauthorized, "User role not found")
c.Abort()
return
}
role, ok := userRole.(string)
if !ok {
common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role")
c.Abort()
return
}
if role == "Teknisi" {
common.ErrorResponses(c, http.StatusForbidden, "Teknisi users are not allowed to access this resource")
c.Abort()
return
}
c.Next()
}
}
// PermissionMiddleware for more granular permissions
func PermissionMiddleware(resource, action string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("userRole")
if !exists {
common.ErrorResponses(c, http.StatusUnauthorized, "User role not found")
c.Abort()
return
}
role, ok := userRole.(string)
if !ok {
common.ErrorResponses(c, http.StatusUnauthorized, "Invalid user role")
c.Abort()
return
}
if !hasPermission(role, resource, action) {
common.ErrorResponses(c, http.StatusForbidden, "Insufficient permissions for this action")
c.Abort()
return
}
c.Next()
}
}
// Permission checker function
func hasPermission(role, resource, action string) bool {
permissions := getPermissions()
rolePermissions, exists := permissions[role]
if !exists {
return false
}
resourcePermissions, exists := rolePermissions[resource]
if !exists {
return false
}
for _, allowedAction := range resourcePermissions {
if allowedAction == action || allowedAction == "*" {
return true
}
}
return false
}
// Define permissions for each role
func getPermissions() map[string]map[string][]string {
return map[string]map[string][]string{
"Superadmin": {
"*": {"*"}, // Full access to everything
},
"Admin": {
"backbone": {"CREATE", "READ", "UPDATE", "DELETE"},
"fishbone": {"CREATE", "READ", "UPDATE", "DELETE"},
"devices": {"CREATE", "READ", "UPDATE", "DELETE"},
"towers": {"CREATE", "READ", "UPDATE", "DELETE"},
"ports": {"CREATE", "READ", "UPDATE", "DELETE"},
"logs": {"READ"},
"users": {"READ"},
},
"Teknisi": {
"backbone": {"CREATE", "READ", "UPDATE"},
"fishbone": {"CREATE", "READ", "UPDATE"},
"devices": {"CREATE", "READ", "UPDATE"},
"towers": {"CREATE", "READ", "UPDATE"},
"ports": {"CREATE", "READ", "UPDATE"},
"logs": {"READ"}, // Only own logs
},
}
}

View File

@ -3,6 +3,7 @@ package req
import "github.com/google/uuid"
type BackboneDTO struct {
BackboneCode string `json:"backbone_code" validate:"required,min=3"`
DeviceStartID uuid.UUID `json:"dev_start_id"`
DeviceEndID uuid.UUID `json:"dev_end_id"`
CoreAmount int `json:"core_amount"`

View File

@ -0,0 +1,46 @@
package req
type DeviceDetailsDTO struct {
DeviceCode string `json:"device_code" validate:"required"`
DeviceType string `json:"device_type" validate:"required,oneof=OTB ODP"`
Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"`
PortAmount int `json:"port_amount" validate:"required,min=1,max=100"`
Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
Region *string `json:"region,omitempty" validate:"omitempty,min=3"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
}
type UpdateDeviceDetailsDTO struct {
DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"`
DeviceType *string `json:"device_type,omitempty" validate:"omitempty,oneof=OTB ODP CLOSURE"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty,min=0,max=100"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"`
Region *string `json:"region,omitempty" validate:"omitempty,min=3"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
}
type AssignMultipleCustomersDTO struct {
CustomerName string `json:"customer_name" binding:"required"`
PortNumber *int `json:"port_number,omitempty"`
}
type UpdateCustomerByPortDTO struct {
PortNumber int `json:"port_number" binding:"required,min=1"`
NewCustomerName *string `json:"new_customer_name,omitempty"` // null to remove, string to update/assign
}
type BulkUpdateCustomersByPortDTO struct {
Updates []UpdateCustomerByPortDTO `json:"updates" binding:"required"`
}
type RemoveCustomerByPortDTO struct {
PortNumber int `json:"port_number" binding:"required,min=1"`
}

View File

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

View File

@ -1,20 +1,25 @@
package req
type DeviceDTO struct {
DeviceCode string `json:"device_code" validate:"required"`
DeviceType string `json:"device_type" validate:"required"`
Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"`
PortAmount int `json:"port_amount" validate:"required"`
Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
DeviceCode string `json:"device_code" validate:"required"`
DeviceType string `json:"device_type" validate:"required"`
Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"`
PortAmount int `json:"port_amount"`
Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
}
type UpdateDeviceDTO struct {
DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"`
DeviceType *string `json:"device_type,omitempty" validate:"omitempty,min=3"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty,min=1"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"`
DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"`
DeviceType *string `json:"device_type,omitempty" validate:"omitempty,min=3"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
}

View File

@ -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"`
}

View File

@ -3,15 +3,17 @@ package req
import "github.com/google/uuid"
type FishboneDTO struct {
BackboneID uuid.UUID `json:"bb_id"`
DeviceStartID uuid.UUID `json:"dev_start_id"`
DeviceEndID uuid.UUID `json:"dev_end_id"`
CoreAmount int `json:"core_amount"`
FishboneCode string `json:"fishbone_code" validate:"required,min=3"`
BackboneID uuid.UUID `json:"bb_id" validate:"required"`
DeviceStartID uuid.UUID `json:"dev_start_id" validate:"required"`
DeviceEndID uuid.UUID `json:"dev_end_id" validate:"required"`
CoreAmount int `json:"core_amount" validate:"required,min=1"`
}
type UpdateFishboneDTO struct {
BackboneID *uuid.UUID `json:"bb_id,omitempty" validate:"omitempty,min=3"`
DeviceStartID *uuid.UUID `json:"dev_start_id,omitempty" validate:"omitempty,min=3"`
DeviceEndID *uuid.UUID `json:"dev_end_id,omitempty" validate:"omitempty,min=3"`
CoreAmount *int `json:"core_amount,omitempty" validate:"omitempty,min=1"`
FishboneCode *string `json:"fishbone_code,omitempty" validate:"omitempty,min=3"`
BackboneID *uuid.UUID `json:"bb_id,omitempty"`
DeviceStartID *uuid.UUID `json:"dev_start_id,omitempty"`
DeviceEndID *uuid.UUID `json:"dev_end_id,omitempty"`
CoreAmount *int `json:"core_amount,omitempty" validate:"omitempty,min=1"`
}

View File

@ -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"`
}

View File

@ -3,15 +3,20 @@ package req
import "github.com/google/uuid"
type TowerDTO struct {
DeviceID uuid.UUID `json:"dev_id"`
TowerCode string `json:"tower_code"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
DeviceID *uuid.UUID `json:"dev_id,omitempty"`
DeviceName *string `json:"device_name,omitempty"`
TowerCode string `json:"tower_code" validate:"required"`
Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"`
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
}
type UpdateTowerDTO struct {
DeviceID *string `json:"device_id,omitempty" validate:"omitempty,min=3"`
TowerCode *string `json:"tower_code,omitempty" validate:"omitempty"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
DeviceID *uuid.UUID `json:"device_id,omitempty"`
DeviceName *string `json:"device_name,omitempty"`
TowerCode *string `json:"tower_code,omitempty"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
ImageURL *string `json:"image_url,omitempty"`
}

View File

@ -0,0 +1,34 @@
package res
import (
"time"
"github.com/google/uuid"
)
// ActivityLogResponse - simplified response for users
type ActivityLogResponse struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
Action string `json:"action"`
Resource string `json:"resource"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Timestamp time.Time `json:"timestamp"`
}
// ActivityLogDetailResponse - detailed response for admins (if needed)
type ActivityLogDetailResponse struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID *string `json:"resource_id,omitempty"`
OldData *string `json:"old_data,omitempty"`
NewData *string `json:"new_data,omitempty"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Timestamp time.Time `json:"timestamp"`
}

View File

@ -8,6 +8,7 @@ import (
type BackboneResponse struct {
ID uuid.UUID `json:"id"`
BackboneCode string `json:"backbone_code"`
DevStart string `json:"dev_start"`
DevEnd string `json:"dev_end"`
CoreAmount int `json:"core_amount"`

View File

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

View File

@ -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"`
}

View File

@ -7,14 +7,18 @@ import (
)
type DeviceResponse struct {
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType string `json:"device_type"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
PortAmount int `json:"port_amount"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType string `json:"device_type"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
PortAmount int `json:"port_amount"`
Status string `json:"status"` // Always include, even if null
CustomerNames []string `json:"customer_names"` // Always include, even if empty
Province *string `json:"province"` // Always include, even if null
City *string `json:"city"` // Always include, even if null
District *string `json:"district"` // Always include, even if null
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -1,17 +1,32 @@
package res
import (
"time"
"time"
"github.com/google/uuid"
"github.com/google/uuid"
)
type FishboneResponse struct {
ID uuid.UUID `json:"id"`
DeviceName string `json:"device_name"`
TowerCode string `json:"tower_code"`
Longtitude float64 `json:"longtitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
CreatedAt time.Time `json:"created_at"`
}
ID uuid.UUID `json:"id"`
FishboneCode string `json:"fishbone_code"`
BackboneCode string `json:"backbone_code"`
DeviceStart string `json:"device_start"`
DeviceEnd string `json:"device_end"`
CoreAmount int `json:"core_amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type FishboneDetailResponse struct {
ID uuid.UUID `json:"id"`
FishboneCode string `json:"fishbone_code"`
Backbone BackboneInfo `json:"backbone"`
CoreAmount int `json:"core_amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type BackboneInfo struct {
ID uuid.UUID `json:"id"`
BackboneCode string `json:"backbone_code"`
}

View File

@ -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"`
}

View File

@ -1,17 +1,19 @@
package res
import (
"time"
"github.com/google/uuid"
"time"
"github.com/google/uuid"
)
type TowerResponse struct {
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
TowerCode string `json:"tower_code"`
Longitude float64 `json:"longtitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
CreatedAt time.Time `json:"created_at"`
}
ID uuid.UUID `json:"id"`
DeviceCode *string `json:"device_code,omitempty"` // Make nullable
TowerCode *string `json:"tower_code"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
ImageURL string `json:"image_url"`
ExternalTower *bool `json:"external_tower"` // Make nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -0,0 +1,25 @@
package entity
import (
"time"
"github.com/google/uuid"
)
type ActivityLog struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
UserID uuid.UUID `json:"user_id" gorm:"type:uuid;not null"`
User User `json:"user" gorm:"foreignKey:UserID"`
Action string `json:"action" gorm:"not null"` // CREATE, UPDATE, DELETE, LOGIN, LOGOUT
Resource string `json:"resource" gorm:"not null"` // devices, backbone, fishbone, towers, etc.
ResourceID *string `json:"resource_id,omitempty"` // ID of the affected resource
OldData *string `json:"old_data,omitempty" gorm:"type:text"` // JSON of old data for updates
NewData *string `json:"new_data,omitempty" gorm:"type:text"` // JSON of new data
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Timestamp time.Time `json:"timestamp" gorm:"autoCreateTime"`
Details *string `json:"details,omitempty"` // Additional details
}
func (ActivityLog) TableName() string {
return "activity_logs"
}

View File

@ -1,21 +1,21 @@
package entity
import (
"time"
"github.com/google/uuid"
"time"
"github.com/google/uuid"
)
type Backbone struct {
ID uuid.UUID `json:"id"`
DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"column:dev_start_id"`
DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"column:dev_end_id"`
CoreAmount int `json:"core_amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
BackboneCode string `json:"backbone_code" gorm:"unique"`
DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"`
DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"`
CoreAmount int `json:"core_amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeviceStart Device `gorm:"foreignKey:DeviceStartID"`
DeviceEnd Device `gorm:"foreignKey:DeviceEndID"`
DeviceStart Device `gorm:"foreignKey:DeviceStartID"`
DeviceEnd Device `gorm:"foreignKey:DeviceEndID"`
}
func (Backbone) TableName() string {

View File

@ -0,0 +1,20 @@
package entity
import (
"time"
"github.com/google/uuid"
)
type CountAssets struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
FishboneAmount int `json:"fishbone_amount" gorm:"column:fishbone_amount"`
BackboneAmount int `json:"backbone_amount" gorm:"column:backbone_amount"`
DeviceAmount int `json:"device_amount" gorm:"column:device_amount"`
TowerAmount int `json:"tower_amount" gorm:"column:tower_amount"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
}
func (CountAssets) TableName() string {
return "count_assets"
}

View File

@ -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
}

View File

@ -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"
}

View File

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

View File

@ -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
}

View File

@ -1,29 +1,40 @@
package entity
import (
"time"
"github.com/google/uuid"
"time"
"github.com/google/uuid"
)
type DeviceType string
type DeviceStatus string
const (
Odp DeviceType = "ODP"
activeDev DeviceStatus = "active"
inactiveDev DeviceStatus = "inactive"
maintenanceDev DeviceStatus = "maintenance"
ODP DeviceType = "ODP"
OTB DeviceType = "OTB"
Closure DeviceType = "closure"
ActiveDev DeviceStatus = "active"
InactiveDev DeviceStatus = "inactive"
MaintenanceDev DeviceStatus = "maintenance"
)
type Device struct {
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType DeviceType `json:"device_type"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
PortAmount int `json:"port_amount"`
Status DeviceStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceCode string `json:"device_code" gorm:"unique"`
DeviceType DeviceType `json:"device_type"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
PortAmount int `json:"port_amount"`
Status DeviceStatus `json:"status"`
Province *string `json:"province,omitempty" gorm:"type:varchar(255)"`
City *string `json:"city,omitempty" gorm:"type:varchar(255)"`
District *string `json:"district,omitempty" gorm:"type:varchar(255)"`
ImageURL *string `json:"image_url,omitempty" gorm:"type:text"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Device) TableName() string {
return "devices"
}

View File

@ -1,27 +1,26 @@
package entity
import (
"time"
"github.com/google/uuid"
"time"
"github.com/google/uuid"
)
type Fishbone struct {
ID uuid.UUID `json:"id"`
BackboneID uuid.UUID `json:"backbone_id" gorm:"column:bb_id"`
DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"column:dev_start_id"`
DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"column:dev_end_id"`
CoreAmount int `json:"core_amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
FishboneCode string `json:"fishbone_code" gorm:"unique"`
BackboneID uuid.UUID `json:"bb_id" gorm:"type:uuid;column:bb_id"`
DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"`
DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"`
CoreAmount int `json:"core_amount" gorm:"column:core_amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeviceStart Device `gorm:"foreignKey:DeviceStartID"`
DeviceEnd Device `gorm:"foreignKey:DeviceEndID"`
Backbone Backbone `gorm:"foreignKey:BackboneID"`
// Relationships
Backbone Backbone `json:"backbone" gorm:"foreignKey:BackboneID"`
DeviceStart Device `json:"device_start" gorm:"foreignKey:DeviceStartID"`
DeviceEnd Device `json:"device_end" gorm:"foreignKey:DeviceEndID"`
}
func (Fishbone) TableName() string {
return "fishbone"
return "fishbone"
}

View File

@ -1,10 +1,18 @@
package entity
import "github.com/google/uuid"
import (
"time"
"github.com/google/uuid"
)
type Role struct {
Id uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Id uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
Name string `json:"name" gorm:"type:varchar(100);not null"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Role) TableName() string {
return "roles"
}

View File

@ -1,21 +1,22 @@
package entity
import (
"time"
"github.com/google/uuid"
"time"
"github.com/google/uuid"
)
type Tower struct {
ID uuid.UUID `json:"id"`
DeviceID uuid.UUID`json:"dev_id" gorm:"column:dev_id"`
TowerCode string `json:"tower_code"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceID *uuid.UUID `json:"dev_id,omitempty" gorm:"type:uuid;column:dev_id;null"` // Make nullable
TowerCode string `json:"tower_code" gorm:"unique"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
ImageURL string `json:"image_url" gorm:"column:image_url"`
ExternalTower *bool `json:"external_tower,omitempty" gorm:"column:external_tower;null"` // Make nullable and fix typo
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Device Device `gorm:"foreignKey:DeviceID"`
Device *Device `json:"device,omitempty" gorm:"foreignKey:DeviceID"` // Make nullable
}
func (Tower) TableName() string {

View File

@ -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"`
}

View File

@ -2,17 +2,21 @@ package entity
import (
"time"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID `json:"id"`
RoleID uuid.UUID `json:"role_id"`
Role Role `json:"role"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
NomorInduk *string `json:"nomor_induk,omitempty" gorm:"unique"` // Add this field
RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"`
Role Role `json:"role" gorm:"foreignKey:RoleID"`
Name string `json:"name"`
Username string `json:"username" gorm:"unique"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (User) TableName() string {
return "users"
}

View File

@ -0,0 +1,90 @@
package repository
import (
"users_management/m/model/entity"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ActivityLogRepo interface {
Create(log entity.ActivityLog) error
GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.ActivityLog, error)
GetAllForAdmins(limit, offset int) ([]entity.ActivityLog, error)
GetByUserRole(role string, limit, offset int) ([]entity.ActivityLog, error)
CountByUserID(userID uuid.UUID) (int64, error)
CountAll() (int64, error)
CountByUserRole(role string) (int64, error)
}
type activityLogRepo struct {
db *gorm.DB
}
func NewActivityLogRepo(db *gorm.DB) ActivityLogRepo {
return &activityLogRepo{
db: db,
}
}
func (r *activityLogRepo) Create(log entity.ActivityLog) error {
return r.db.Create(&log).Error
}
func (r *activityLogRepo) GetByUserID(userID uuid.UUID, limit, offset int) ([]entity.ActivityLog, error) {
var logs []entity.ActivityLog
err := r.db.Where("user_id = ?", userID).
Preload("User").
Preload("User.Role").
Order("timestamp DESC").
Limit(limit).
Offset(offset).
Find(&logs).Error
return logs, err
}
func (r *activityLogRepo) GetAllForAdmins(limit, offset int) ([]entity.ActivityLog, error) {
var logs []entity.ActivityLog
err := r.db.Preload("User").
Preload("User.Role").
Order("timestamp DESC").
Limit(limit).
Offset(offset).
Find(&logs).Error
return logs, err
}
func (r *activityLogRepo) GetByUserRole(role string, limit, offset int) ([]entity.ActivityLog, error) {
var logs []entity.ActivityLog
err := r.db.Joins("JOIN users ON users.id = activity_logs.user_id").
Joins("JOIN roles ON roles.id = users.role_id").
Where("roles.name = ?", role).
Preload("User").
Preload("User.Role").
Order("activity_logs.timestamp DESC").
Limit(limit).
Offset(offset).
Find(&logs).Error
return logs, err
}
func (r *activityLogRepo) CountByUserID(userID uuid.UUID) (int64, error) {
var count int64
err := r.db.Model(&entity.ActivityLog{}).Where("user_id = ?", userID).Count(&count).Error
return count, err
}
func (r *activityLogRepo) CountAll() (int64, error) {
var count int64
err := r.db.Model(&entity.ActivityLog{}).Count(&count).Error
return count, err
}
func (r *activityLogRepo) CountByUserRole(role string) (int64, error) {
var count int64
err := r.db.Model(&entity.ActivityLog{}).
Joins("JOIN users ON users.id = activity_logs.user_id").
Joins("JOIN roles ON roles.id = users.role_id").
Where("roles.name = ?", role).
Count(&count).Error
return count, err
}

View File

@ -12,6 +12,7 @@ type BackboneRepo interface {
GetAll() ([]entity.Backbone, error)
Update(id uuid.UUID, updates map[string]interface{}) error
GetByID(id uuid.UUID) (entity.Backbone, error)
WithTransaction(fn func(*gorm.DB) error) error
}
type backboneRepo struct {
@ -57,4 +58,8 @@ func (r *backboneRepo) GetByID(id uuid.UUID) (entity.Backbone, error) {
return backbone, err
}
return backbone, nil
}
func (r *backboneRepo) WithTransaction(fn func(*gorm.DB) error) error {
return r.db.Transaction(fn)
}

136
repository/count_assets.go Normal file
View File

@ -0,0 +1,136 @@
package repository
import (
"users_management/m/model/entity"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CountAssetsRepo interface {
Get() (entity.CountAssets, error)
UpdateCounts() error
}
type countAssetsRepo struct {
db *gorm.DB
}
func NewCountAssetsRepo(db *gorm.DB) CountAssetsRepo {
return &countAssetsRepo{
db: db,
}
}
func (r *countAssetsRepo) Get() (entity.CountAssets, error) {
var countAssets entity.CountAssets
// Try to get existing record, if not found create a new one
err := r.db.First(&countAssets).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// Create new record with current counts
return r.createNewCountRecord()
}
return countAssets, err
}
return countAssets, nil
}
func (r *countAssetsRepo) UpdateCounts() error {
var countAssets entity.CountAssets
// Get current counts from each table
fishboneCount, err := r.countFishbones()
if err != nil {
return err
}
backboneCount, err := r.countBackbones()
if err != nil {
return err
}
deviceCount, err := r.countDevices()
if err != nil {
return err
}
towerCount, err := r.countTowers()
if err != nil {
return err
}
// Check if record exists
err = r.db.First(&countAssets).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// Create new record
countAssets = entity.CountAssets{
ID: uuid.New(),
FishboneAmount: fishboneCount,
BackboneAmount: backboneCount,
DeviceAmount: deviceCount,
TowerAmount: towerCount,
}
return r.db.Create(&countAssets).Error
}
return err
}
// Update existing record
updates := map[string]interface{}{
"fishbone_amount": fishboneCount,
"backbone_amount": backboneCount,
"device_amount": deviceCount,
"tower_amount": towerCount,
}
return r.db.Model(&countAssets).Updates(updates).Error
}
func (r *countAssetsRepo) createNewCountRecord() (entity.CountAssets, error) {
fishboneCount, _ := r.countFishbones()
backboneCount, _ := r.countBackbones()
deviceCount, _ := r.countDevices()
towerCount, _ := r.countTowers()
countAssets := entity.CountAssets{
ID: uuid.New(),
FishboneAmount: fishboneCount,
BackboneAmount: backboneCount,
DeviceAmount: deviceCount,
TowerAmount: towerCount,
}
err := r.db.Create(&countAssets).Error
return countAssets, err
}
// Helper methods to count each entity
func (r *countAssetsRepo) countFishbones() (int, error) {
var count int64
err := r.db.Model(&entity.Fishbone{}).Count(&count).Error
return int(count), err
}
func (r *countAssetsRepo) countBackbones() (int, error) {
var count int64
err := r.db.Model(&entity.Backbone{}).Count(&count).Error
return int(count), err
}
func (r *countAssetsRepo) countDevices() (int, error) {
var count int64
err := r.db.Model(&entity.Device{}).Count(&count).Error
return int(count), err
}
func (r *countAssetsRepo) countTowers() (int, error) {
var count int64
err := r.db.Model(&entity.Tower{}).Count(&count).Error
return int(count), err
}

View File

@ -0,0 +1,919 @@
package repository
import (
"fmt"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/entity"
"github.com/google/uuid"
"gorm.io/gorm"
)
type DeviceDetailsRepo interface {
Create(device entity.Device) error
GetAll() ([]entity.DeviceDetails, error)
GetByID(id uuid.UUID) (entity.DeviceDetails, error)
Update(id uuid.UUID, updates map[string]interface{}) error
Delete(id uuid.UUID) error
// Port management
UpdateDevicePortUsage(deviceID uuid.UUID) error
// ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error
// Connection management
GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error)
GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error)
GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error)
// Validation helpers
GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error)
AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error
RemoveCustomerFromPort(deviceID uuid.UUID, customerName string) error
UpdatePortUsage(deviceID uuid.UUID, portUsed int) error
UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error
MigrateCustomerNamesToPortAssignments(devicePort *entity.DevicePort, devicePortAmount int) error
AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error
UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error
BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error
RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error
}
type deviceDetailsRepo struct {
db *gorm.DB
}
func NewDeviceDetailsRepo(db *gorm.DB) DeviceDetailsRepo {
return &deviceDetailsRepo{
db: db,
}
}
func (r *deviceDetailsRepo) UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock both device and device_port records
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
// Only ODP devices can have customer assignments
if device.DeviceType != "ODP" {
return fmt.Errorf("customer assignments can only be updated for ODP devices")
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Initialize port assignments if empty
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
// Validate port number
if update.PortNumber > device.PortAmount {
return fmt.Errorf("port number %d exceeds device capacity (%d)", update.PortNumber, device.PortAmount)
}
portIndex := update.PortNumber - 1
// If assigning a new customer name
if update.NewCustomerName != nil && *update.NewCustomerName != "" {
// Check if customer name already exists on another port
for i, assignment := range devicePort.PortAssignments {
if i != portIndex && assignment.CustomerName != nil && *assignment.CustomerName == *update.NewCustomerName {
return fmt.Errorf("customer %s is already assigned to port %d", *update.NewCustomerName, i+1)
}
}
}
// Update the port assignment
if update.NewCustomerName == nil {
// Remove customer from port
devicePort.PortAssignments[portIndex].CustomerName = nil
} else {
// Assign/update customer on port
devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName
}
// Update counters and backward compatibility fields
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock both device and device_port records
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
// Only ODP devices can have customer assignments
if device.DeviceType != "ODP" {
return fmt.Errorf("customer assignments can only be updated for ODP devices")
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Initialize port assignments if empty
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
// Validate all port numbers first
for _, update := range updates {
if update.PortNumber > device.PortAmount {
return fmt.Errorf("port number %d exceeds device capacity (%d)", update.PortNumber, device.PortAmount)
}
}
// Create a map of final assignments to validate for duplicates
finalAssignments := make(map[int]*string)
// Start with current assignments
for i, assignment := range devicePort.PortAssignments {
finalAssignments[i+1] = assignment.CustomerName
}
// Apply updates
for _, update := range updates {
finalAssignments[update.PortNumber] = update.NewCustomerName
}
// Check for duplicate customer names
customerNames := make(map[string]int) // customer name -> port number
for portNum, customerName := range finalAssignments {
if customerName != nil && *customerName != "" {
if existingPort, exists := customerNames[*customerName]; exists {
return fmt.Errorf("customer %s would be assigned to both port %d and port %d", *customerName, existingPort, portNum)
}
customerNames[*customerName] = portNum
}
}
// Apply all updates
for _, update := range updates {
portIndex := update.PortNumber - 1
devicePort.PortAssignments[portIndex].CustomerName = update.NewCustomerName
}
// Recalculate port_used based on actual assignments
highestUsedPort := 0
customerCount := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
customerCount++
if assignment.PortNumber > highestUsedPort {
highestUsedPort = assignment.PortNumber
}
}
}
// Update port_used to reflect actual usage
if customerCount == 0 {
devicePort.PortUsed = 0
} else {
// Set port_used to the highest port with a customer
// or keep current port_used if it's higher (for reserved ports)
if highestUsedPort > devicePort.PortUsed {
devicePort.PortUsed = highestUsedPort
}
}
// Recalculate port_available
devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed
// Update counters and backward compatibility fields
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Validate port number
if portNumber > device.PortAmount {
return fmt.Errorf("port number %d exceeds device capacity (%d)", portNumber, device.PortAmount)
}
portIndex := portNumber - 1
// Check if port has a customer
if len(devicePort.PortAssignments) <= portIndex ||
devicePort.PortAssignments[portIndex].CustomerName == nil ||
*devicePort.PortAssignments[portIndex].CustomerName == "" {
return fmt.Errorf("port %d does not have a customer assigned", portNumber)
}
// Remove customer from port
devicePort.PortAssignments[portIndex].CustomerName = nil
// Update counters and backward compatibility fields
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock both device and device_port records
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
// Only ODP devices can have port assignments
if device.DeviceType != "ODP" {
return fmt.Errorf("port assignments can only be updated for ODP devices")
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Validate port numbers are within device capacity
for _, assignment := range assignments {
if assignment.PortNumber < 1 || assignment.PortNumber > device.PortAmount {
return fmt.Errorf("port number %d is out of range (1-%d)", assignment.PortNumber, device.PortAmount)
}
}
// Initialize port assignments array if needed
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
// Check for duplicate customer names (if not null)
customerNames := make(map[string]int) // map customer name to port number
for _, assignment := range assignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
if existingPort, exists := customerNames[*assignment.CustomerName]; exists {
return fmt.Errorf("customer %s is assigned to multiple ports (%d and %d)",
*assignment.CustomerName, existingPort, assignment.PortNumber)
}
customerNames[*assignment.CustomerName] = assignment.PortNumber
}
}
// Update port assignments
for _, assignment := range assignments {
portIndex := assignment.PortNumber - 1
if portIndex < len(devicePort.PortAssignments) {
devicePort.PortAssignments[portIndex].PortNumber = assignment.PortNumber
devicePort.PortAssignments[portIndex].CustomerName = assignment.CustomerName
}
}
// Update counters and backward compatibility fields
r.updateDevicePortCounters(&devicePort)
// Calculate port usage based on assignments
portUsed := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
portUsed++
}
}
devicePort.PortUsed = portUsed
devicePort.PortAvailable = device.PortAmount - portUsed
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) Create(device entity.Device) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Create device
if err := tx.Create(&device).Error; err != nil {
return err
}
// Create corresponding device port
devicePort := entity.DevicePort{
ID: uuid.New(),
DeviceID: device.ID,
PortUsed: 0,
PortAvailable: device.PortAmount,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
return tx.Create(&devicePort).Error
})
}
func (r *deviceDetailsRepo) GetAll() ([]entity.DeviceDetails, error) {
var devices []entity.DeviceDetails
err := r.db.
Preload("DevicePort").
Preload("BackbonesStart").
Preload("BackbonesStart.DeviceStart").
Preload("BackbonesStart.DeviceEnd").
Preload("BackbonesEnd").
Preload("BackbonesEnd.DeviceStart").
Preload("BackbonesEnd.DeviceEnd").
Preload("FishbonesStart").
Preload("FishbonesStart.DeviceStart").
Preload("FishbonesStart.DeviceEnd").
Preload("FishbonesStart.Backbone").
Preload("FishbonesEnd").
Preload("FishbonesEnd.DeviceStart").
Preload("FishbonesEnd.DeviceEnd").
Preload("FishbonesEnd.Backbone").
Preload("Towers").
Preload("Towers.Device").
Find(&devices).Error
return devices, err
}
func (r *deviceDetailsRepo) GetByID(id uuid.UUID) (entity.DeviceDetails, error) {
var device entity.DeviceDetails
err := r.db.
Preload("DevicePort").
Preload("BackbonesStart").
Preload("BackbonesStart.DeviceStart").
Preload("BackbonesStart.DeviceEnd").
Preload("BackbonesEnd").
Preload("BackbonesEnd.DeviceStart").
Preload("BackbonesEnd.DeviceEnd").
Preload("FishbonesStart").
Preload("FishbonesStart.DeviceStart").
Preload("FishbonesStart.DeviceEnd").
Preload("FishbonesStart.Backbone").
Preload("FishbonesEnd").
Preload("FishbonesEnd.DeviceStart").
Preload("FishbonesEnd.DeviceEnd").
Preload("FishbonesEnd.Backbone").
Preload("Towers").
Preload("Towers.Device").
Where("id = ?", id).
First(&device).Error
return device, err
}
func (r *deviceDetailsRepo) Update(id uuid.UUID, updates map[string]interface{}) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Update device
if err := tx.Model(&entity.Device{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return err
}
// If port_amount is updated, update device_port
if portAmount, exists := updates["port_amount"]; exists {
if err := r.updatePortAmountCascade(tx, id, portAmount.(int)); err != nil {
return err
}
}
return nil
})
}
func (r *deviceDetailsRepo) updatePortAmountCascade(tx *gorm.DB, deviceID uuid.UUID, newPortAmount int) error {
// Get current port usage
var devicePort entity.DevicePort
if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return err
}
// Check if new port amount is sufficient for current usage
if newPortAmount < devicePort.PortUsed {
return fmt.Errorf("cannot reduce port amount to %d, currently using %d ports", newPortAmount, devicePort.PortUsed)
}
// Special case: if port_amount is set to 0, clear all assignments
if newPortAmount == 0 {
devicePort.PortUsed = 0
devicePort.PortAvailable = 0
devicePort.CustomerCount = 0
devicePort.CustomerNames = []string{}
devicePort.PortAssignments = entity.PortAssignments{}
devicePort.UpdatedAt = time.Now()
} else {
// Calculate new port available
newPortAvailable := newPortAmount - devicePort.PortUsed
// Update port assignments to match new port amount
if len(devicePort.PortAssignments) > 0 {
// Resize port assignments array
newPortAssignments := make(entity.PortAssignments, newPortAmount)
// Copy existing assignments up to the new port amount
for i := 0; i < newPortAmount; i++ {
if i < len(devicePort.PortAssignments) {
newPortAssignments[i] = devicePort.PortAssignments[i]
} else {
newPortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
devicePort.PortAssignments = newPortAssignments
}
devicePort.PortAvailable = newPortAvailable
devicePort.UpdatedAt = time.Now()
}
return tx.Save(&devicePort).Error
}
func (r *deviceDetailsRepo) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
if portUsed > device.PortAmount {
return fmt.Errorf("port_used (%d) cannot exceed port_amount (%d)", portUsed, device.PortAmount)
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Initialize port assignments if empty and we have customers or port_used > 0
if len(devicePort.PortAssignments) == 0 && (len(devicePort.CustomerNames) > 0 || portUsed > 0) {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
var customerName *string
// Map existing customer names to ports
if i < len(devicePort.CustomerNames) && devicePort.CustomerNames[i] != "" {
customerName = &devicePort.CustomerNames[i]
}
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: customerName,
}
}
}
// Update port assignments based on new port_used value
currentCustomerCount := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
currentCustomerCount++
}
}
if portUsed < currentCustomerCount {
// Need to remove some customers (keep first N customers)
customersKept := 0
for i := range devicePort.PortAssignments {
if devicePort.PortAssignments[i].CustomerName != nil && *devicePort.PortAssignments[i].CustomerName != "" {
if customersKept < portUsed {
customersKept++
} else {
devicePort.PortAssignments[i].CustomerName = nil
}
}
}
}
// Update counters
devicePort.PortUsed = portUsed
devicePort.PortAvailable = device.PortAmount - portUsed
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) MigrateCustomerNamesToPortAssignments(devicePort *entity.DevicePort, devicePortAmount int) error {
// Only migrate if PortAssignments is empty but CustomerNames has data
if len(devicePort.PortAssignments) == 0 && len(devicePort.CustomerNames) > 0 {
devicePort.PortAssignments = make(entity.PortAssignments, devicePortAmount)
for i := 0; i < devicePortAmount; i++ {
var customerName *string
if i < len(devicePort.CustomerNames) && devicePort.CustomerNames[i] != "" {
customerName = &devicePort.CustomerNames[i]
}
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: customerName,
}
}
}
return nil
}
// func (r *deviceDetailsRepo) ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error {
// var devicePort entity.DevicePort
// if err := r.db.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
// return err
// }
// if devicePort.Portvailable < requiredPorts {
// return errors.New("insufficient available ports")
// }
// return nil
// }
func (r *deviceDetailsRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) {
var backbones []entity.Backbone
err := r.db.Preload("DeviceStart").Preload("DeviceEnd").
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
Find(&backbones).Error
return backbones, err
}
func (r *deviceDetailsRepo) GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) {
var fishbones []entity.Fishbone
err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
Find(&fishbones).Error
return fishbones, err
}
func (r *deviceDetailsRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) {
var towers []entity.Tower
err := r.db.Where("dev_id = ?", deviceID).Find(&towers).Error
return towers, err
}
func (r *deviceDetailsRepo) GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error) {
var devicePort entity.DevicePort
err = r.db.Where("device_id = ?", deviceID).First(&devicePort).Error
if err != nil {
return 0, 0, err
}
return devicePort.PortUsed, devicePort.PortAvailable, nil
}
func (r *deviceDetailsRepo) Delete(id uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Delete device port first
if err := tx.Where("device_id = ?", id).Delete(&entity.DevicePort{}).Error; err != nil {
return err
}
// Delete device
return tx.Delete(&entity.Device{}, id).Error
})
}
func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock both device and device_port records
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
// Only ODP devices can have customers assigned
if device.DeviceType != "ODP" {
return fmt.Errorf("customers can only be assigned to ODP devices")
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Initialize port assignments if empty
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
// Determine port number to use
var targetPortNumber int
if portNumber != nil {
targetPortNumber = *portNumber
if targetPortNumber < 1 || targetPortNumber > device.PortAmount {
return fmt.Errorf("port number must be between 1 and %d", device.PortAmount)
}
} else {
// Auto-assign to first available port
targetPortNumber = -1
for i, assignment := range devicePort.PortAssignments {
if assignment.CustomerName == nil || *assignment.CustomerName == "" {
targetPortNumber = i + 1
break
}
}
if targetPortNumber == -1 {
return fmt.Errorf("no available ports for customer assignment")
}
}
// Check if the specified port is already occupied
portIndex := targetPortNumber - 1
if devicePort.PortAssignments[portIndex].CustomerName != nil &&
*devicePort.PortAssignments[portIndex].CustomerName != "" {
return fmt.Errorf("port %d is already occupied by %s", targetPortNumber, *devicePort.PortAssignments[portIndex].CustomerName)
}
// Check if customer is already assigned to another port
for i, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName == customerName {
return fmt.Errorf("customer %s is already assigned to port %d", customerName, i+1)
}
}
// Assign customer to port
devicePort.PortAssignments[portIndex].CustomerName = &customerName
// Update both PortAssignments and CustomerNames for backward compatibility
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) RemoveCustomerFromPort(deviceID uuid.UUID, customerName string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Find and remove customer
newCustomerNames := make([]string, 0)
found := false
for _, existing := range devicePort.CustomerNames {
if existing != customerName {
newCustomerNames = append(newCustomerNames, existing)
} else {
found = true
}
}
if !found {
return fmt.Errorf("customer %s is not assigned to this device", customerName)
}
devicePort.CustomerNames = newCustomerNames
devicePort.CustomerCount = len(devicePort.CustomerNames)
devicePort.PortAvailable = devicePort.PortAvailable + 1
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error {
// This method is required by the DeviceDetailsRepo interface
return r.db.Transaction(func(tx *gorm.DB) error {
var devicePort entity.DevicePort
if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Update based on port assignments
r.updateDevicePortCounters(&devicePort)
devicePort.PortUsed = devicePort.CustomerCount
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) updateDevicePortCounters(devicePort *entity.DevicePort) {
// Get device port amount (need to load device if not loaded)
var device entity.Device
if devicePort.Device.ID == uuid.Nil {
r.db.Where("id = ?", devicePort.DeviceID).First(&device)
devicePort.Device = device
} else {
device = devicePort.Device
}
// Count actual customers and find highest used port
customerCount := 0
customerNames := make([]string, 0)
highestCustomerPort := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
customerCount++
customerNames = append(customerNames, *assignment.CustomerName) // Just customer names, not with port numbers
if assignment.PortNumber > highestCustomerPort {
highestCustomerPort = assignment.PortNumber
}
}
}
// Set port_used to the highest port number that has a customer
// This ensures we don't have gaps in port usage
if customerCount == 0 {
devicePort.PortUsed = 0
} else {
devicePort.PortUsed = highestCustomerPort
}
// Calculate port_available
devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed
// Update customer fields
devicePort.CustomerCount = customerCount
devicePort.CustomerNames = customerNames
}
func (r *deviceDetailsRepo) AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Lock both device and device_port records
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
// Only ODP devices can have customers assigned
if device.DeviceType != "ODP" {
return fmt.Errorf("customers can only be assigned to ODP devices")
}
var devicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return fmt.Errorf("device port record not found: %w", err)
}
// Initialize port assignments if empty
if len(devicePort.PortAssignments) == 0 {
devicePort.PortAssignments = make(entity.PortAssignments, device.PortAmount)
for i := 0; i < device.PortAmount; i++ {
devicePort.PortAssignments[i] = entity.PortAssignment{
PortNumber: i + 1,
CustomerName: nil,
}
}
}
// Check if there are enough available ports
availablePorts := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName == nil || *assignment.CustomerName == "" {
availablePorts++
}
}
portsNeeded := len(assignments)
if portsNeeded > availablePorts {
return fmt.Errorf("not enough available ports: need %d, available %d", portsNeeded, availablePorts)
}
// Process each assignment
for _, assignment := range assignments {
var targetPortNumber int
if assignment.PortNumber != nil {
targetPortNumber = *assignment.PortNumber
if targetPortNumber < 1 || targetPortNumber > device.PortAmount {
return fmt.Errorf("port number %d must be between 1 and %d", targetPortNumber, device.PortAmount)
}
} else {
// Auto-assign to first available port
targetPortNumber = -1
for i, portAssignment := range devicePort.PortAssignments {
if portAssignment.CustomerName == nil || *portAssignment.CustomerName == "" {
targetPortNumber = i + 1
break
}
}
if targetPortNumber == -1 {
return fmt.Errorf("no available ports for customer assignment")
}
}
// Check if the specified port is already occupied
portIndex := targetPortNumber - 1
if devicePort.PortAssignments[portIndex].CustomerName != nil &&
*devicePort.PortAssignments[portIndex].CustomerName != "" {
return fmt.Errorf("port %d is already occupied by %s", targetPortNumber, *devicePort.PortAssignments[portIndex].CustomerName)
}
// Check if customer is already assigned to another port
for i, portAssignment := range devicePort.PortAssignments {
if portAssignment.CustomerName != nil && *portAssignment.CustomerName == assignment.CustomerName {
return fmt.Errorf("customer %s is already assigned to port %d", assignment.CustomerName, i+1)
}
}
// Assign customer to port
devicePort.PortAssignments[portIndex].CustomerName = &assignment.CustomerName
}
// Update counters and backward compatibility fields
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}
func (r *deviceDetailsRepo) RecalculatePortUsageAfterBulkUpdate(deviceID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var device entity.Device
if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
var devicePort entity.DevicePort
if err := tx.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return err
}
// Find the highest port number with a customer
highestUsedPort := 0
customerCount := 0
for _, assignment := range devicePort.PortAssignments {
if assignment.CustomerName != nil && *assignment.CustomerName != "" {
customerCount++
if assignment.PortNumber > highestUsedPort {
highestUsedPort = assignment.PortNumber
}
}
}
// Update port usage
devicePort.PortUsed = highestUsedPort
devicePort.PortAvailable = device.PortAmount - devicePort.PortUsed
// Update other counters
r.updateDevicePortCounters(&devicePort)
devicePort.UpdatedAt = time.Now()
return tx.Save(&devicePort).Error
})
}

View File

@ -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
}

View File

@ -1,6 +1,7 @@
package repository
import (
"time"
"users_management/m/model/entity"
"github.com/google/uuid"
@ -27,13 +28,40 @@ func NewDevicesRepo(db *gorm.DB) DevicesRepo {
}
func (r *devicesRepo) Post(device entity.Device) error {
err := r.db.Create(&device).Error
if err != nil {
return err
}
return nil
}
return r.db.Transaction(func(tx *gorm.DB) error {
// Create the device first
if err := tx.Create(&device).Error; err != nil {
return err
}
// Create the corresponding DevicePort record
customerCount := 0
customerNames := make([]string, 0)
// For ODP devices, initialize customer tracking
if device.DeviceType == "ODP" {
// Customer count starts at 0, will be updated when fishbones are connected
customerCount = 0
}
devicePort := entity.DevicePort{
ID: uuid.New(),
DeviceID: device.ID,
PortUsed: 0,
PortAvailable: device.PortAmount,
CustomerCount: customerCount,
CustomerNames: customerNames,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := tx.Create(&devicePort).Error; err != nil {
return err
}
return nil
})
}
func (r *devicesRepo) GetAll() ([]entity.Device, error) {
var devices []entity.Device
err := r.db.Find(&devices).Error

View File

@ -1,6 +1,7 @@
package repository
import (
"log"
"users_management/m/model/entity"
"github.com/google/uuid"
@ -16,6 +17,14 @@ type FishboneRepo interface {
CountFishbone() (map[uuid.UUID]int, error)
CountFishboneByBackboneID(backboneID uuid.UUID) (int, error)
GetAllWithRelations() ([]entity.Fishbone, error)
GetByIDWithRelations(id uuid.UUID) (entity.Fishbone, error)
CheckFishboneExists(id uuid.UUID) (bool, error)
CheckBackboneExists(id uuid.UUID) (bool, error)
CheckDeviceExists(id uuid.UUID) (bool, error)
Delete(id uuid.UUID) error
WithTransaction(fn func(*gorm.DB) error) error
GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error)
}
type fishboneRepo struct {
@ -36,15 +45,32 @@ func (r *fishboneRepo) Post(fishbone entity.Fishbone) error {
return nil
}
func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) {
func (r *fishboneRepo) GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error) {
var fishbones []entity.Fishbone
err := r.db.Preload("DeviceStart").Preload("DeviceEnd").Preload("Backbone").Preload("Backbone.DeviceStart").Preload("Backbone.DeviceEnd").Find(&fishbones).Error
err := r.db.Where("bb_id = ?", backboneID).
Preload("DeviceStart").
Preload("DeviceEnd").
Preload("Backbone").
Find(&fishbones).Error
return fishbones, err
}
func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) {
log.Print("Fetching all fishbones with relations")
var fishbones []entity.Fishbone
err := r.db.Preload("DeviceStart").Preload("DeviceEnd").Preload("Backbone").Find(&fishbones).Error
if err != nil {
return fishbones, err
}
log.Print(fishbones)
return fishbones, nil
}
func (r *fishboneRepo) WithTransaction(fn func(*gorm.DB) error) error {
return r.db.Transaction(fn)
}
func (r *fishboneRepo) Update(id uuid.UUID,updates map[string]interface{}) error {
err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
@ -93,4 +119,38 @@ func (r *fishboneRepo) CountFishboneByBackboneID(backboneID uuid.UUID) (int, err
return 0, err
}
return int(count), nil
}
func (r *fishboneRepo) GetAllWithRelations() ([]entity.Fishbone, error) {
var fishbones []entity.Fishbone
err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").Find(&fishbones).Error
return fishbones, err
}
func (r *fishboneRepo) GetByIDWithRelations(id uuid.UUID) (entity.Fishbone, error) {
var fishbone entity.Fishbone
err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").Where("id = ?", id).First(&fishbone).Error
return fishbone, err
}
func (r *fishboneRepo) CheckFishboneExists(id uuid.UUID) (bool, error) {
var count int64
err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
func (r *fishboneRepo) CheckBackboneExists(id uuid.UUID) (bool, error) {
var count int64
err := r.db.Model(&entity.Backbone{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
func (r *fishboneRepo) CheckDeviceExists(id uuid.UUID) (bool, error) {
var count int64
err := r.db.Model(&entity.Device{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
func (r *fishboneRepo) Delete(id uuid.UUID) error {
return r.db.Where("id = ?", id).Delete(&entity.Fishbone{}).Error
}

View File

@ -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
}

View File

@ -12,6 +12,7 @@ type TowerRepo interface {
GetAll() ([]entity.Tower, error)
Update(id uuid.UUID,updates map[string]interface{}) error
GetByID(id uuid.UUID) (entity.Tower, error)
CheckDeviceExists(deviceID uuid.UUID) (bool, error)
}
type towerRepo struct {
@ -42,6 +43,11 @@ func (r *towerRepo) GetAll() ([]entity.Tower, error) {
return towers, nil
}
func (r *towerRepo) CheckDeviceExists(deviceID uuid.UUID) (bool, error) {
var count int64
err := r.db.Model(&entity.Device{}).Where("id = ?", deviceID).Count(&count).Error
return count > 0, err
}
func (r *towerRepo) Update(id uuid.UUID,updates map[string]interface{}) error {
err := r.db.Model(&entity.Tower{}).Where("id = ?", id).Updates(updates).Error
if err != nil {

View File

@ -1,50 +1,75 @@
package repository
import (
"users_management/m/model/entity"
"users_management/m/model/entity"
"gorm.io/gorm"
"github.com/google/uuid"
"gorm.io/gorm"
)
type UsersRepo interface {
Post(user entity.User) error
GetRoleByDepartment(departmentName string) (entity.Role, error)
GetUserByUsername(username string) (entity.User, error)
Post(user entity.User) error
GetRoleByDepartment(departmentName string) (entity.Role, error)
GetUserByUsername(username string) (entity.User, error)
GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this
CreateUserFromExternal(user entity.User) error // Add this
UpdateUserRole(nomorInduk string, roleID uuid.UUID) error // Add this
GetAllUsers() ([]entity.User, error) // Add this
}
type usersRepo struct {
db *gorm.DB
db *gorm.DB
}
func NewUsersRepo(db *gorm.DB) UsersRepo {
return &usersRepo{
db: db,
}
return &usersRepo{
db: db,
}
}
func (r *usersRepo) Post(user entity.User) error {
err := r.db.Create(&user).Error
if err != nil {
return err
}
return nil
err := r.db.Create(&user).Error
if err != nil {
return err
}
return nil
}
func (r *usersRepo) GetRoleByDepartment(departmentName string) (entity.Role, error) {
var role entity.Role
err := r.db.Where("name = ?", departmentName).First(&role).Error
if err != nil {
return role, err
}
return role, nil
var role entity.Role
err := r.db.Where("name = ?", departmentName).First(&role).Error
if err != nil {
return role, err
}
return role, nil
}
func (r *usersRepo) GetUserByUsername(username string) (entity.User, error) {
var users entity.User
err := r.db.Where("username = ?", username).First(&users).Error
if err != nil {
return users, err
}
return users, nil
var users entity.User
err := r.db.Where("username = ?", username).Preload("Role").First(&users).Error
if err != nil {
return users, err
}
return users, nil
}
// New methods for RBAC
func (r *usersRepo) GetUserByNomorInduk(nomorInduk string) (entity.User, error) {
var user entity.User
err := r.db.Where("nomor_induk = ?", nomorInduk).Preload("Role").First(&user).Error
return user, err
}
func (r *usersRepo) CreateUserFromExternal(user entity.User) error {
return r.db.Create(&user).Error
}
func (r *usersRepo) UpdateUserRole(nomorInduk string, roleID uuid.UUID) error {
return r.db.Model(&entity.User{}).Where("nomor_induk = ?", nomorInduk).Update("role_id", roleID).Error
}
func (r *usersRepo) GetAllUsers() ([]entity.User, error) {
var users []entity.User
err := r.db.Preload("Role").Find(&users).Error
return users, err
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -0,0 +1,101 @@
package usecase
import (
"encoding/json"
"log"
"users_management/m/model/entity"
"users_management/m/repository"
"github.com/google/uuid"
)
type ActivityLogUseCase interface {
LogActivity(userID uuid.UUID, action, resource string, resourceID *string, oldData, newData interface{}, ipAddress, userAgent, details string) error
GetUserLogs(userID uuid.UUID, page, limit int) ([]entity.ActivityLog, int64, error)
GetAllLogs(page, limit int) ([]entity.ActivityLog, int64, error)
GetTeknisinLogs(page, limit int) ([]entity.ActivityLog, int64, error)
}
type activityLogUseCase struct {
activityLogRepo repository.ActivityLogRepo
}
func NewActivityLogUseCase(activityLogRepo repository.ActivityLogRepo) ActivityLogUseCase {
return &activityLogUseCase{
activityLogRepo: activityLogRepo,
}
}
func (uc *activityLogUseCase) LogActivity(userID uuid.UUID, action, resource string, resourceID *string, oldData, newData interface{}, ipAddress, userAgent, details string) error {
var oldDataJSON, newDataJSON *string
if oldData != nil {
oldBytes, err := json.Marshal(oldData)
if err == nil {
oldStr := string(oldBytes)
oldDataJSON = &oldStr
}
}
if newData != nil {
newBytes, err := json.Marshal(newData)
if err == nil {
newStr := string(newBytes)
newDataJSON = &newStr
}
}
var detailsPtr *string
if details != "" {
detailsPtr = &details
}
log := entity.ActivityLog{
ID: uuid.New(),
UserID: userID,
Action: action,
Resource: resource,
ResourceID: resourceID,
OldData: oldDataJSON,
NewData: newDataJSON,
IPAddress: ipAddress,
UserAgent: userAgent,
Details: detailsPtr,
}
return uc.activityLogRepo.Create(log)
}
func (uc *activityLogUseCase) GetUserLogs(userID uuid.UUID, page, limit int) ([]entity.ActivityLog, int64, error) {
offset := (page - 1) * limit
log.Printf("Fetching logs for user %s, page: %d, limit: %d, offset: %d", userID, page, limit, offset)
logs, err := uc.activityLogRepo.GetByUserID(userID, limit, offset)
if err != nil {
return nil, 0, err
}
total, err := uc.activityLogRepo.CountByUserID(userID)
return logs, total, err
}
func (uc *activityLogUseCase) GetAllLogs(page, limit int) ([]entity.ActivityLog, int64, error) {
offset := (page - 1) * limit
logs, err := uc.activityLogRepo.GetAllForAdmins(limit, offset)
if err != nil {
return nil, 0, err
}
total, err := uc.activityLogRepo.CountAll()
return logs, total, err
}
func (uc *activityLogUseCase) GetTeknisinLogs(page, limit int) ([]entity.ActivityLog, int64, error) {
offset := (page - 1) * limit
logs, err := uc.activityLogRepo.GetByUserRole("Teknisi", limit, offset)
if err != nil {
return nil, 0, err
}
total, err := uc.activityLogRepo.CountByUserRole("Teknisi")
return logs, total, err
}

View File

@ -2,6 +2,7 @@ package usecase
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"io/ioutil"
@ -28,13 +29,32 @@ type authUsecase struct {
userRepo repository.UsersRepo
validate *validator.Validate
cfg *config.Config
client *http.Client
}
func NewAuthUsecase(userRepo repository.UsersRepo, cfg *config.Config) AuthUsecase {
var client *http.Client
if cfg.LoginConfig.SkipSSLVerification {
// Create HTTP client with SSL verification disabled
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}
} else {
// Use standard HTTP client with SSL verification enabled
client = &http.Client{
Timeout: 30 * time.Second,
}
}
return &authUsecase{
userRepo: userRepo,
validate: validator.New(),
cfg: cfg,
client: client,
}
}
@ -54,14 +74,16 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err
return "","","", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
// client := &http.Client{}
resp, err := u.client.Do(req)
if err != nil {
return "","","", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
@ -88,7 +110,8 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err
meReq.Header.Set("Authorization", "Bearer "+token)
meReq.Header.Set("Accept", "application/json")
meResp, err := client.Do(meReq)
meResp, err := u.client.Do(meReq)
if err != nil {
return "","","", err
}
@ -109,15 +132,25 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err
return "","","", err
}
departemen := meResponse.Data.Departemen
if departemen != "Teknisi" {
if departemen != "TECHNICAL PRORGAMMER" {
return "","","", errors.New("user is not a technician")
}
role , err := u.userRepo.GetRoleByDepartment(departemen)
if err != nil {
return "","","", err
isUserExist, err := u.userRepo.GetUserByUsername(login.Username)
if isUserExist.ID != uuid.Nil {
// Validate the password
if !utils.CheckPasswordHash(login.Password, isUserExist.Password) {
return "","","", errors.New("incorrect password")
}
if err != nil {
return "","","", err
}
return token, isUserExist.Role.Name, isUserExist.Name, nil
}else if err != nil && err != gorm.ErrRecordNotFound {
return "ERROR WHILE SEARCHING USERNAME","", "", err
}
password, err := utils.HashPassword(login.Password)
@ -125,19 +158,11 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err
return "error while hasing password: ","", "", err
}
isUserExist, err := u.userRepo.GetUserByUsername(login.Username)
if isUserExist.ID != uuid.Nil {
// Validate the password
if !utils.CheckPasswordHash(login.Password, isUserExist.Password) {
return "","","", errors.New("incorrect password")
}
return token, role.Name, isUserExist.Name, nil
}else if err != nil && err != gorm.ErrRecordNotFound {
return "ERROR WHILE SEARCHING USERNAME","", "", err
}
default_role := "Teknisi"
role, err := u.userRepo.GetRoleByDepartment(default_role)
if err != nil {
return "","","", err
}
user := entity.User{
ID: uuid.New(),
@ -148,7 +173,6 @@ func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, err
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = u.userRepo.Post(user)
if err != nil {
return "","","", err
@ -166,8 +190,8 @@ func (u *authUsecase) Logout(token string) error {
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
// client := &http.Client{}
resp, err := u.client.Do(req)
if err != nil {
return err
}

View File

@ -11,6 +11,7 @@ import (
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"gorm.io/gorm"
)
type BackboneUseCase interface {
@ -19,41 +20,107 @@ type BackboneUseCase interface {
GetByID(id uuid.UUID) (res.BackboneResponse, error)
UpdateBackbone(id uuid.UUID, backbone req.UpdateBackboneDTO) error
}
type backboneUseCase struct {
backboneRepo repository.BackboneRepo
fishboneRepo repository.FishboneRepo
validate *validator.Validate
deviceDetailsRepo repository.DeviceDetailsRepo // Add this field
}
func NewBackboneUseCase(backboneRepo repository.BackboneRepo, fishboneRepo repository.FishboneRepo) BackboneUseCase {
return &backboneUseCase{
backboneRepo: backboneRepo,
fishboneRepo: fishboneRepo,
validate: validator.New(),
}
func NewBackboneUseCase(backboneRepo repository.BackboneRepo, fishboneRepo repository.FishboneRepo, deviceDetailsRepo repository.DeviceDetailsRepo) BackboneUseCase {
return &backboneUseCase{
backboneRepo: backboneRepo,
fishboneRepo: fishboneRepo,
deviceDetailsRepo: deviceDetailsRepo, // Initialize the field
validate: validator.New(),
}
}
func (u *backboneUseCase) CreateBackbone(backbone req.BackboneDTO) error {
err := u.validate.Struct(backbone)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
err := u.validate.Struct(backbone)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
newBackbone := entity.Backbone{
ID: uuid.New(),
DeviceStartID: backbone.DeviceStartID,
DeviceEndID: backbone.DeviceEndID,
CoreAmount: backbone.CoreAmount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error {
// Lock device records to prevent race conditions
var startDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", backbone.DeviceStartID).First(&startDevice).Error; err != nil {
return fmt.Errorf("start device not found: %w", err)
}
var endDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", backbone.DeviceEndID).First(&endDevice).Error; err != nil {
return fmt.Errorf("end device not found: %w", err)
}
return u.backboneRepo.Post(newBackbone)
// Validate device types - both devices must be OTB for backbones
if startDevice.DeviceType != "OTB" {
return fmt.Errorf("start device must be of type OTB, got %s", startDevice.DeviceType)
}
if endDevice.DeviceType != "OTB" {
return fmt.Errorf("end device must be of type OTB, got %s", endDevice.DeviceType)
}
// Check port availability with locking
var startDevicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", backbone.DeviceStartID).First(&startDevicePort).Error; err != nil {
return fmt.Errorf("start device port record not found: %w", err)
}
var endDevicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", backbone.DeviceEndID).First(&endDevicePort).Error; err != nil {
return fmt.Errorf("end device port record not found: %w", err)
}
// Validate port availability - each backbone uses 1 port regardless of core amount
if startDevicePort.PortAvailable < 1 {
return fmt.Errorf("start device has no available ports (available: %d, required: 1)",
startDevicePort.PortAvailable)
}
if endDevicePort.PortAvailable < 1 {
return fmt.Errorf("end device has no available ports (available: %d, required: 1)",
endDevicePort.PortAvailable)
}
newBackbone := entity.Backbone{
ID: uuid.New(),
BackboneCode: backbone.BackboneCode,
DeviceStartID: backbone.DeviceStartID,
DeviceEndID: backbone.DeviceEndID,
CoreAmount: backbone.CoreAmount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Create backbone
if err := tx.Create(&newBackbone).Error; err != nil {
return err
}
// Update port usage for both devices
if err := u.updateDevicePortUsageInTx(tx, backbone.DeviceStartID); err != nil {
return fmt.Errorf("failed to update start device port usage: %w", err)
}
if err := u.updateDevicePortUsageInTx(tx, backbone.DeviceEndID); err != nil {
return fmt.Errorf("failed to update end device port usage: %w", err)
}
return nil
})
}
func (u *backboneUseCase) GetAllBackbone() ([]res.BackboneResponse, error) {
backbones, err := u.backboneRepo.GetAll()
if err != nil {
@ -91,29 +158,142 @@ func (u *backboneUseCase) GetByID(id uuid.UUID) (res.BackboneResponse, error) {
}
return backboneResp, nil
}
}
func (u *backboneUseCase) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackboneDTO) error {
err := u.validate.Struct(backbone)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
err := u.validate.Struct(backbone)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
updates := make(map[string]interface{})
return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error {
// Get original backbone for comparison with lock
var originalBackbone entity.Backbone
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", id).First(&originalBackbone).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return fmt.Errorf("backbone not found")
}
return err
}
if backbone.DeviceStartID != nil {
updates["DeviceStartID"] = backbone.DeviceStartID
}
updates := make(map[string]interface{})
devicesToUpdate := make(map[uuid.UUID]bool)
devicesToUpdate[originalBackbone.DeviceStartID] = true
devicesToUpdate[originalBackbone.DeviceEndID] = true
if backbone.DeviceEndID != nil {
updates["DeviceEndID"] = backbone.DeviceEndID
}
// Validate device type changes if devices are being changed
if backbone.DeviceStartID != nil && *backbone.DeviceStartID != originalBackbone.DeviceStartID {
var newStartDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", *backbone.DeviceStartID).First(&newStartDevice).Error; err != nil {
return fmt.Errorf("new start device not found: %w", err)
}
if newStartDevice.DeviceType != "OTB" {
return fmt.Errorf("new start device must be of type OTB, got %s", newStartDevice.DeviceType)
}
updates["dev_start_id"] = *backbone.DeviceStartID
devicesToUpdate[*backbone.DeviceStartID] = true
}
if backbone.CoreAmount != nil {
updates["CoreAmount"] = backbone.CoreAmount
}
if backbone.DeviceEndID != nil && *backbone.DeviceEndID != originalBackbone.DeviceEndID {
var newEndDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", *backbone.DeviceEndID).First(&newEndDevice).Error; err != nil {
return fmt.Errorf("new end device not found: %w", err)
}
if newEndDevice.DeviceType != "OTB" {
return fmt.Errorf("new end device must be of type OTB, got %s", newEndDevice.DeviceType)
}
updates["dev_end_id"] = *backbone.DeviceEndID
devicesToUpdate[*backbone.DeviceEndID] = true
}
updates["UpdatedAt"] = time.Now()
// Handle core amount changes
if backbone.CoreAmount != nil && *backbone.CoreAmount != originalBackbone.CoreAmount {
updates["core_amount"] = *backbone.CoreAmount
}
return u.backboneRepo.Update(id, updates)
}
if len(updates) == 0 {
return fmt.Errorf("no fields to update")
}
updates["updated_at"] = time.Now()
// Update backbone
if err := tx.Model(&entity.Backbone{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return err
}
// Update port usage for all affected devices
for deviceID := range devicesToUpdate {
if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil {
return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err)
}
}
return nil
})
}
func (u *backboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error {
// Get device with lock
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
var portUsed int
var customerCount int
switch device.DeviceType {
case "OTB":
// For OTB: count backbones (each backbone uses 1 port regardless of core amount)
var backboneCount int64
if err := tx.Model(&entity.Backbone{}).
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
Count(&backboneCount).Error; err != nil {
return err
}
portUsed = int(backboneCount)
customerCount = 0 // OTB doesn't serve customers directly
case "closure":
// For closure: count fishbones where this device is the start device
var fishboneCount int64
if err := tx.Model(&entity.Fishbone{}).
Where("dev_start_id = ?", deviceID).
Count(&fishboneCount).Error; err != nil {
return err
}
portUsed = int(fishboneCount)
customerCount = 0 // Closure doesn't serve customers directly
case "ODP":
// For ODP: sum fishbone core amounts where this device is the end device
var totalCores int64
if err := tx.Model(&entity.Fishbone{}).
Where("dev_end_id = ?", deviceID).
Select("COALESCE(SUM(core_amount), 0)").
Scan(&totalCores).Error; err != nil {
return err
}
portUsed = int(totalCores)
customerCount = portUsed // For ODP, customer count equals port_used
}
portAvailable := device.PortAmount - portUsed
if portAvailable < 0 {
portAvailable = 0
}
return tx.Model(&entity.DevicePort{}).
Where("device_id = ?", deviceID).
Updates(map[string]interface{}{
"port_used": portUsed,
"port_available": portAvailable,
"customer_count": customerCount,
"updated_at": gorm.Expr("NOW()"),
}).Error
}

View File

@ -0,0 +1,31 @@
package usecase
import (
"users_management/m/model/entity"
"users_management/m/repository"
)
type CountAssetsUseCase interface {
GetCounts() (entity.CountAssets, error)
}
type countAssetsUseCase struct {
countAssetsRepo repository.CountAssetsRepo
}
func NewCountAssetsUseCase(countAssetsRepo repository.CountAssetsRepo) CountAssetsUseCase {
return &countAssetsUseCase{
countAssetsRepo: countAssetsRepo,
}
}
func (uc *countAssetsUseCase) GetCounts() (entity.CountAssets, error) {
// Update counts before returning
err := uc.countAssetsRepo.UpdateCounts()
if err != nil {
return entity.CountAssets{}, err
}
// Get the updated counts
return uc.countAssetsRepo.Get()
}

View File

@ -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(),
}

297
usecase/device_details.go Normal file
View File

@ -0,0 +1,297 @@
package usecase
import (
"errors"
"fmt"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils/helper"
"users_management/m/utils/service"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
type DeviceDetailsUseCase interface {
CreateDeviceDetails(device req.DeviceDetailsDTO) error
GetAllDeviceDetails() ([]res.DeviceDetailsResponse, error)
GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error)
UpdateDeviceDetails(id uuid.UUID, device req.UpdateDeviceDetailsDTO) error
DeleteDeviceDetails(id uuid.UUID) error
// Port management
// ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error
RecalculatePortUsage(deviceID uuid.UUID) error
AssignCustomerToPort(deviceID uuid.UUID, customerName string,portNumber *int) error
UpdatePortUsage(deviceID uuid.UUID, portUsed int) error
UpdatePortAssignments(deviceID uuid.UUID, portAssignments []req.PortAssignmentDTO) error
AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error
UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error
BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error
RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error
}
type deviceDetailsUseCase struct {
deviceDetailsRepo repository.DeviceDetailsRepo
geocoder service.GeocodingService
validate *validator.Validate
}
func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, geocoder service.GeocodingService) DeviceDetailsUseCase {
return &deviceDetailsUseCase{
deviceDetailsRepo: deviceDetailsRepo,
geocoder: geocoder,
validate: validator.New(),
}
}
func (u *deviceDetailsUseCase) UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error {
// Validate port number
if update.PortNumber < 1 {
return errors.New("port number must be greater than 0")
}
return u.deviceDetailsRepo.UpdateCustomerByPort(deviceID, update)
}
func (u *deviceDetailsUseCase) BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error {
if len(updates) == 0 {
return errors.New("no updates provided")
}
// Validate all updates
portNumbers := make(map[int]bool)
newCustomerNames := make(map[string]bool)
for _, update := range updates {
// Check for duplicate port numbers in request
if portNumbers[update.PortNumber] {
return fmt.Errorf("duplicate port number in request: %d", update.PortNumber)
}
portNumbers[update.PortNumber] = true
// Check for duplicate new customer names in request (ignore nulls)
if update.NewCustomerName != nil && *update.NewCustomerName != "" {
if newCustomerNames[*update.NewCustomerName] {
return fmt.Errorf("duplicate new customer name in request: %s", *update.NewCustomerName)
}
newCustomerNames[*update.NewCustomerName] = true
}
// Validate port number
if update.PortNumber < 1 {
return fmt.Errorf("port number must be greater than 0: %d", update.PortNumber)
}
}
return u.deviceDetailsRepo.BulkUpdateCustomersByPort(deviceID, updates)
}
func (u *deviceDetailsUseCase) RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error {
if portNumber < 1 {
return errors.New("port number must be greater than 0")
}
return u.deviceDetailsRepo.RemoveCustomerByPort(deviceID, portNumber)
}
func (u *deviceDetailsUseCase) AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error {
if len(assignments) == 0 {
return errors.New("no assignments provided")
}
// Validate all assignments first
customerNames := make(map[string]bool)
portNumbers := make(map[int]bool)
for _, assignment := range assignments {
// Check for duplicate customer names in the request
if customerNames[assignment.CustomerName] {
return fmt.Errorf("duplicate customer name in request: %s", assignment.CustomerName)
}
customerNames[assignment.CustomerName] = true
// Check for duplicate port numbers in the request
if assignment.PortNumber != nil {
if portNumbers[*assignment.PortNumber] {
return fmt.Errorf("duplicate port number in request: %d", *assignment.PortNumber)
}
portNumbers[*assignment.PortNumber] = true
}
}
return u.deviceDetailsRepo.AssignMultipleCustomersToPort(deviceID, assignments)
}
func (u *deviceDetailsUseCase) UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error {
// Validate the assignments
if len(assignments) == 0 {
return errors.New("port assignments cannot be empty")
}
// Check for duplicate port numbers
portNumbers := make(map[int]bool)
for _, assignment := range assignments {
if portNumbers[assignment.PortNumber] {
return fmt.Errorf("duplicate port number: %d", assignment.PortNumber)
}
portNumbers[assignment.PortNumber] = true
}
return u.deviceDetailsRepo.UpdatePortAssignments(deviceID, assignments)
}
func (u *deviceDetailsUseCase) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error {
return u.deviceDetailsRepo.UpdatePortUsage(deviceID, portUsed)
}
func (u *deviceDetailsUseCase) CreateDeviceDetails(deviceDTO req.DeviceDetailsDTO) error {
err := u.validate.Struct(deviceDTO)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
newDevice := entity.Device{
ID: uuid.New(),
DeviceCode: deviceDTO.DeviceCode,
DeviceType: entity.DeviceType(deviceDTO.DeviceType),
Longitude: deviceDTO.Longitude,
Latitude: deviceDTO.Latitude,
PortAmount: deviceDTO.PortAmount,
Status: entity.DeviceStatus(deviceDTO.Status),
Province: deviceDTO.Province,
City: deviceDTO.City,
District: deviceDTO.District,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.deviceDetailsRepo.Create(newDevice)
}
func (u *deviceDetailsUseCase) GetAllDeviceDetails() ([]res.DeviceDetailsResponse, error) {
devices, err := u.deviceDetailsRepo.GetAll()
if err != nil {
return nil, err
}
return helper.ConvertToDeviceDetailsResponses(devices, u.geocoder)
}
func (u *deviceDetailsUseCase) GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error) {
device, err := u.deviceDetailsRepo.GetByID(id)
if err != nil {
return res.DeviceDetailsResponse{}, err
}
return helper.ConvertToDeviceDetailsResponse(device, u.geocoder)
}
func (u *deviceDetailsUseCase) UpdateDeviceDetails(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO) error {
err := u.validate.Struct(deviceDTO)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
updates := map[string]interface{}{}
if deviceDTO.DeviceCode != nil {
updates["device_code"] = *deviceDTO.DeviceCode
}
if deviceDTO.DeviceType != nil {
updates["device_type"] = *deviceDTO.DeviceType
}
if deviceDTO.Longitude != nil {
updates["longitude"] = *deviceDTO.Longitude
}
if deviceDTO.Latitude != nil {
updates["latitude"] = *deviceDTO.Latitude
}
if deviceDTO.PortAmount != nil {
// Validate port amount change
if *deviceDTO.PortAmount < 0 {
return fmt.Errorf("port amount cannot be negative")
}
// If not setting to 0, check current usage
if *deviceDTO.PortAmount > 0 {
currentUsed, _, err := u.deviceDetailsRepo.GetPortUsageByDevice(id)
if err != nil {
return err
}
if *deviceDTO.PortAmount < currentUsed {
return fmt.Errorf("cannot reduce port amount to %d, currently using %d ports", *deviceDTO.PortAmount, currentUsed)
}
}
updates["port_amount"] = *deviceDTO.PortAmount
}
if deviceDTO.Status != nil {
updates["status"] = *deviceDTO.Status
}
if deviceDTO.Region != nil {
updates["region"] = *deviceDTO.Region
}
if deviceDTO.Province != nil {
updates["province"] = *deviceDTO.Province
}
if deviceDTO.City != nil {
updates["city"] = *deviceDTO.City
}
if deviceDTO.District != nil {
updates["district"] = *deviceDTO.District
}
if len(updates) == 0 {
return errors.New("no fields to update")
}
updates["updated_at"] = time.Now()
return u.deviceDetailsRepo.Update(id, updates)
}
func (u *deviceDetailsUseCase) DeleteDeviceDetails(id uuid.UUID) error {
// Check if device has connections
backbones, err := u.deviceDetailsRepo.GetBackbonesByDeviceID(id)
if err != nil {
return err
}
if len(backbones) > 0 {
return errors.New("cannot delete device with active backbone connections")
}
fishbones, err := u.deviceDetailsRepo.GetFishbonesByDeviceID(id)
if err != nil {
return err
}
if len(fishbones) > 0 {
return errors.New("cannot delete device with active fishbone connections")
}
towers, err := u.deviceDetailsRepo.GetTowersByDeviceID(id)
if err != nil {
return err
}
if len(towers) > 0 {
return errors.New("cannot delete device with active tower connections")
}
return u.deviceDetailsRepo.Delete(id)
}
func (u *deviceDetailsUseCase) RecalculatePortUsage(deviceID uuid.UUID) error {
return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID)
}
func (u *deviceDetailsUseCase) AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error {
return u.deviceDetailsRepo.AssignCustomerToPort(deviceID, customerName, portNumber)
}

View File

@ -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
}

View File

@ -43,18 +43,26 @@ func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
return fmt.Errorf("validation error: %w", err)
}
newDevice := entity.Device{
ID: uuid.New(),
DeviceCode: device.DeviceCode,
DeviceType: entity.DeviceType(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: entity.DeviceStatus(device.Status),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
if device.DeviceType == "OTB" || device.DeviceType == "ODP" && device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
newDevice := entity.Device{
ID: uuid.New(),
DeviceCode: device.DeviceCode,
DeviceType: entity.DeviceType(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: entity.DeviceStatus(device.Status),
Province: device.Province, // Now nullable
City: device.City, // Now nullable
District: device.District, // Now nullable
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.deviceRepo.Post(newDevice)
}
@ -88,7 +96,6 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
updates := map[string]interface{}{}
if device.DeviceCode != nil {
@ -106,6 +113,21 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e
if device.PortAmount != nil {
updates["PortAmount"] = *device.PortAmount
}
if device.Status != nil {
updates["Status"] = *device.Status
}
if device.Province != nil {
updates["Province"] = *device.Province
}
if device.City != nil {
updates["City"] = *device.City
}
if device.District != nil {
updates["District"] = *device.District
}
if device.DeviceType != nil && (*device.DeviceType == "OTB" || *device.DeviceType == "ODP") && device.PortAmount != nil && *device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
if len(updates) == 0 {
return fmt.Errorf("no update data")
@ -113,6 +135,8 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e
updates["UpdatedAt"] = time.Now()
return u.deviceRepo.Update(id, updates)
}

View File

@ -2,96 +2,317 @@ package usecase
import (
"errors"
"fmt"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils/helper"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"gorm.io/gorm"
)
type FishboneUseCase interface {
CreateFishbone(fishbone req.FishboneDTO) error
GetAllFishbone() ([]entity.Fishbone, error)
GetByID(id uuid.UUID) (entity.Fishbone, error)
UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error
CreateFishbone(fishbone req.FishboneDTO) error
GetAllFishbone() ([]res.FishboneResponse, error)
GetByID(id uuid.UUID) (res.FishboneDetailResponse, error)
UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error
DeleteFishbone(id uuid.UUID) error
GetFishboneStats() (map[string]interface{}, error)
GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error)
}
type fishboneUsecase struct {
fishboneRepo repository.FishboneRepo
validate *validator.Validate
type fishboneUseCase struct {
fishboneRepo repository.FishboneRepo
backboneRepo repository.BackboneRepo
deviceDetailsRepo repository.DeviceDetailsRepo // Add this field
validate *validator.Validate
}
func NewFishboneUseCase(fishboneRepo repository.FishboneRepo) FishboneUseCase {
return &fishboneUsecase{
fishboneRepo: fishboneRepo,
validate: validator.New(),
}
func NewFishboneUseCase(fishboneRepo repository.FishboneRepo, backboneRepo repository.BackboneRepo, deviceDetailsRepo repository.DeviceDetailsRepo) FishboneUseCase {
return &fishboneUseCase{
fishboneRepo: fishboneRepo,
backboneRepo: backboneRepo,
deviceDetailsRepo: deviceDetailsRepo, // Initialize the field
validate: validator.New(),
}
}
func (u *fishboneUsecase) CreateFishbone(fishbone req.FishboneDTO) error {
err := u.validate.Struct(fishbone)
if err != nil {
return err
}
func (u *fishboneUseCase) GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error) {
fishbones, err := u.fishboneRepo.GetByBackboneID(backboneID)
if err != nil {
return nil, err
}
newFishbone := entity.Fishbone{
ID: uuid.New(),
BackboneID: fishbone.BackboneID,
DeviceStartID: fishbone.DeviceStartID,
DeviceEndID: fishbone.DeviceEndID,
CoreAmount: fishbone.CoreAmount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
fishboneResp, err := helper.ConvertToFishboneResponses(fishbones, nil)
if err != nil {
return nil, err
}
return u.fishboneRepo.Post(newFishbone)
return fishboneResp, nil
}
func (u *fishboneUsecase) GetAllFishbone() ([]entity.Fishbone, error) {
fishbones, err := u.fishboneRepo.GetAll()
if err != nil {
return fishbones, err
}
return fishbones, nil
func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error {
err := u.validate.Struct(fishbone)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error {
// Lock device records to prevent race conditions
var startDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", fishbone.DeviceStartID).First(&startDevice).Error; err != nil {
return fmt.Errorf("start device not found: %w", err)
}
var endDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", fishbone.DeviceEndID).First(&endDevice).Error; err != nil {
return fmt.Errorf("end device not found: %w", err)
}
// Validate device types
if startDevice.DeviceType != "closure" {
return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType)
}
if endDevice.DeviceType != "ODP" {
return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType)
}
// Check port availability with locking
var startDevicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", fishbone.DeviceStartID).First(&startDevicePort).Error; err != nil {
return fmt.Errorf("start device port record not found: %w", err)
}
var endDevicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", fishbone.DeviceEndID).First(&endDevicePort).Error; err != nil {
return fmt.Errorf("end device port record not found: %w", err)
}
// Validate port availability
if startDevicePort.PortAvailable < 1 {
return fmt.Errorf("start device has no available ports (available: %d, required: 1)",
startDevicePort.PortAvailable)
}
if endDevicePort.PortAvailable < fishbone.CoreAmount {
return fmt.Errorf("end device has insufficient available ports (available: %d, required: %d)",
endDevicePort.PortAvailable, fishbone.CoreAmount)
}
newFishbone := entity.Fishbone{
ID: uuid.New(),
FishboneCode: fishbone.FishboneCode,
BackboneID: fishbone.BackboneID,
DeviceStartID: fishbone.DeviceStartID,
DeviceEndID: fishbone.DeviceEndID,
CoreAmount: fishbone.CoreAmount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Create fishbone
if err := tx.Create(&newFishbone).Error; err != nil {
return err
}
// Update port usage for both devices
if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceStartID); err != nil {
return fmt.Errorf("failed to update start device port usage: %w", err)
}
if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceEndID); err != nil {
return fmt.Errorf("failed to update end device port usage: %w", err)
}
return nil
})
}
func (u *fishboneUsecase) GetByID(id uuid.UUID) (entity.Fishbone, error) {
fishbone, err := u.fishboneRepo.GetByID(id)
if err != nil {
return fishbone, err
}
return fishbone, nil
func (u *fishboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error {
// Get device with lock
var device entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", deviceID).First(&device).Error; err != nil {
return err
}
var portUsed int
var customerCount int
switch device.DeviceType {
case "closure":
var fishboneCount int64
if err := tx.Model(&entity.Fishbone{}).
Where("dev_start_id = ?", deviceID).
Count(&fishboneCount).Error; err != nil {
return err
}
portUsed = int(fishboneCount)
customerCount = 0
case "ODP":
var totalCores int64
if err := tx.Model(&entity.Fishbone{}).
Where("dev_end_id = ?", deviceID).
Select("COALESCE(SUM(core_amount), 0)").
Scan(&totalCores).Error; err != nil {
return err
}
portUsed = int(totalCores)
customerCount = portUsed // For ODP, customer count equals port_used
}
portAvailable := device.PortAmount - portUsed
if portAvailable < 0 {
portAvailable = 0
}
return tx.Model(&entity.DevicePort{}).
Where("device_id = ?", deviceID).
Updates(map[string]interface{}{
"port_used": portUsed,
"port_available": portAvailable,
"customer_count": customerCount,
"updated_at": gorm.Expr("NOW()"),
}).Error
}
func (u *fishboneUsecase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error {
err := u.validate.Struct(fishbone)
if err != nil {
return err
}
func (u *fishboneUseCase) GetAllFishbone() ([]res.FishboneResponse, error) {
fishbones, err := u.fishboneRepo.GetAll()
if err != nil {
return nil, err
}
updates := make(map[string]interface{})
if fishbone.BackboneID != nil {
updates["BackboneID"] = *fishbone.BackboneID
}
if fishbone.DeviceStartID != nil {
updates["DeviceStartID"] = *fishbone.DeviceStartID
}
if fishbone.DeviceEndID != nil {
updates["DeviceEndID"] = *fishbone.DeviceEndID
}
if fishbone.CoreAmount != nil {
updates["CoreAmount"] = *fishbone.CoreAmount
}
if len(updates) == 0 {
return errors.New("no fields to update")
}
updates["UpdatedAt"] = time.Now()
return u.fishboneRepo.Update(id, updates)
return helper.ConvertToSimpleFishboneResponses(fishbones), nil
}
func (u *fishboneUseCase) GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) {
fishbone, err := u.fishboneRepo.GetByIDWithRelations(id)
if err != nil {
return res.FishboneDetailResponse{}, err
}
// Convert to detailed response using helper
return helper.ConvertToFishboneDetailResponse(fishbone), nil
}
func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error {
err := u.validate.Struct(fishbone)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error {
// Get original fishbone for comparison with lock
var originalFishbone entity.Fishbone
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", id).First(&originalFishbone).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return fmt.Errorf("fishbone not found")
}
return err
}
updates := make(map[string]interface{})
devicesToUpdate := make(map[uuid.UUID]bool)
devicesToUpdate[originalFishbone.DeviceStartID] = true
devicesToUpdate[originalFishbone.DeviceEndID] = true
// Validate device type changes if devices are being changed
if fishbone.DeviceStartID != nil && *fishbone.DeviceStartID != originalFishbone.DeviceStartID {
var newStartDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", *fishbone.DeviceStartID).First(&newStartDevice).Error; err != nil {
return fmt.Errorf("new start device not found: %w", err)
}
if newStartDevice.DeviceType != "closure" {
return fmt.Errorf("new start device must be of type closure, got %s", newStartDevice.DeviceType)
}
updates["dev_start_id"] = *fishbone.DeviceStartID
devicesToUpdate[*fishbone.DeviceStartID] = true
}
if fishbone.DeviceEndID != nil && *fishbone.DeviceEndID != originalFishbone.DeviceEndID {
var newEndDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", *fishbone.DeviceEndID).First(&newEndDevice).Error; err != nil {
return fmt.Errorf("new end device not found: %w", err)
}
if newEndDevice.DeviceType != "ODP" {
return fmt.Errorf("new end device must be of type ODP, got %s", newEndDevice.DeviceType)
}
updates["dev_end_id"] = *fishbone.DeviceEndID
devicesToUpdate[*fishbone.DeviceEndID] = true
}
// Handle core amount changes
if fishbone.CoreAmount != nil && *fishbone.CoreAmount != originalFishbone.CoreAmount {
updates["core_amount"] = *fishbone.CoreAmount
}
if fishbone.BackboneID != nil {
// Validate backbone exists
var backbone entity.Backbone
if err := tx.Where("id = ?", *fishbone.BackboneID).First(&backbone).Error; err != nil {
return fmt.Errorf("backbone not found: %w", err)
}
updates["bb_id"] = *fishbone.BackboneID
}
if fishbone.FishboneCode != nil {
updates["fishbone_code"] = *fishbone.FishboneCode
}
if len(updates) == 0 {
return fmt.Errorf("no fields to update")
}
updates["updated_at"] = time.Now()
// Update fishbone
if err := tx.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return err
}
// Update port usage for all affected devices
for deviceID := range devicesToUpdate {
if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil {
return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err)
}
}
return nil
})
}
func (u *fishboneUseCase) DeleteFishbone(id uuid.UUID) error {
// Check if fishbone exists
exists, err := u.fishboneRepo.CheckFishboneExists(id)
if err != nil {
return err
}
if !exists {
return errors.New("fishbone not found")
}
return u.fishboneRepo.Delete(id)
}
func (u *fishboneUseCase) GetFishboneStats() (map[string]interface{}, error) {
fishbones, err := u.fishboneRepo.GetAll()
if err != nil {
return nil, err
}
return helper.GetFishboneStats(fishbones), nil
}

106
usecase/nearest_device.go Normal file
View File

@ -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
}

View File

@ -2,6 +2,8 @@ package usecase
import (
"errors"
"fmt"
"mime/multipart"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
@ -15,10 +17,10 @@ import (
)
type TowerUseCase interface {
Post(tower req.TowerDTO) error
Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error
GetAll() ([]res.TowerResponse, error)
GetByID(id uuid.UUID) (res.TowerResponse, error)
UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error
UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) error
}
type towerUsecase struct {
@ -35,23 +37,49 @@ func NewTowerUseCase(towerRepo repository.TowerRepo, geocoder service.GeocodingS
}
}
func (u *towerUsecase) Post(tower req.TowerDTO) error {
err := u.validate.Struct(tower)
if err != nil {
return err
}
func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error {
err := u.validate.Struct(tower)
if err != nil {
return err
}
newTower := entity.Tower{
ID: uuid.New(),
DeviceID: tower.DeviceID,
TowerCode: tower.TowerCode,
Longitude: tower.Longitude,
Latitude: tower.Latitude,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Validate that if it's not an external tower, DeviceID must be provided
if tower.ExternalTower != nil && !*tower.ExternalTower && tower.DeviceID == nil {
return fmt.Errorf("device_id is required for internal towers")
}
return u.towerRepo.Post(newTower)
// Validate that if DeviceID is provided, the device exists
if tower.DeviceID != nil {
deviceExists, err := u.towerRepo.CheckDeviceExists(*tower.DeviceID)
if err != nil {
return err
}
if !deviceExists {
return fmt.Errorf("device not found")
}
}
var imageURL string
if imageFile != nil {
imageURL, err = helper.SaveTowerImage(imageFile)
if err != nil {
return err
}
}
newTower := entity.Tower{
ID: uuid.New(),
DeviceID: tower.DeviceID, // Now nullable
TowerCode: tower.TowerCode,
Longitude: tower.Longitude,
Latitude: tower.Latitude,
ImageURL: imageURL,
ExternalTower: tower.ExternalTower, // Now nullable
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.towerRepo.Post(newTower)
}
func (u *towerUsecase) GetAll() ([]res.TowerResponse, error) {
@ -80,7 +108,7 @@ func (u *towerUsecase) GetByID(id uuid.UUID) (res.TowerResponse, error) {
return towerResp, nil
}
func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error {
func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) error {
err := u.validate.Struct(tower)
if err != nil {
return err // Return validation error
@ -99,6 +127,26 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error
if tower.Latitude != nil {
updates["Latitude"] = *tower.Latitude
}
if tower.ExternalTower != nil {
updates["ExternalTower"] = *tower.ExternalTower
}
// Handle image upload
if imageFile != nil {
// Get current tower to delete old image
currentTower, err := u.towerRepo.GetByID(id)
if err == nil && currentTower.ImageURL != "" {
// Delete old image
helper.DeleteTowerImage(currentTower.ImageURL)
}
// Save new image
imageURL, err := helper.SaveTowerImage(imageFile)
if err != nil {
return err
}
updates["ImageURL"] = imageURL
}
// If no fields are updated, return an error
if len(updates) == 0 {
@ -108,4 +156,4 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error
updates["UpdatedAt"] = time.Now()
return u.towerRepo.Update(id, updates)
}
}

View File

@ -1,30 +1,33 @@
package usecase
import (
"users_management/m/model/entity"
"users_management/m/repository"
"time"
"users_management/m/model/entity"
"users_management/m/repository"
"github.com/google/uuid"
"github.com/google/uuid"
)
type UsersUsecase interface {
GetRoleByDepartment(departmentName string) (uuid.UUID, error)
GetUserByUsername(username string) (entity.User, error)
GetRoleByDepartment(departmentName string) (uuid.UUID, error)
GetUserByUsername(username string) (entity.User, error)
GetUserByNomorInduk(nomorInduk string) (entity.User, error) // Add this
CreateUserFromExternal(nomorInduk, name, roleName string) error // Add this
UpdateUserRole(nomorInduk, roleName string) error // Add this
GetAllUsers() ([]entity.User, error) // Add this
}
type usersUsecase struct {
userRepo repository.UsersRepo
userRepo repository.UsersRepo
}
func NewUsersUsecase(userRepo repository.UsersRepo) UsersUsecase {
return &usersUsecase{
userRepo: userRepo,
}
return &usersUsecase{
userRepo: userRepo,
}
}
func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, error) {
role, err := u.userRepo.GetRoleByDepartment(departmentName)
if err != nil {
return uuid.Nil, err
@ -33,10 +36,49 @@ func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, er
}
func (u *usersUsecase) GetUserByUsername(username string) (entity.User, error) {
users, err := u.userRepo.GetUserByUsername(username)
if err != nil {
return users, err
}
return users, nil
users, err := u.userRepo.GetUserByUsername(username)
if err != nil {
return users, err
}
return users, nil
}
// New methods for RBAC
func (u *usersUsecase) GetUserByNomorInduk(nomorInduk string) (entity.User, error) {
return u.userRepo.GetUserByNomorInduk(nomorInduk)
}
func (u *usersUsecase) CreateUserFromExternal(nomorInduk, name, roleName string) error {
// Get role ID
roleID, err := u.GetRoleByDepartment(roleName)
if err != nil {
return err
}
// Create user
user := entity.User{
ID: uuid.New(),
NomorInduk: &nomorInduk,
RoleID: roleID,
Name: name,
Username: nomorInduk, // Use nomor_induk as username
Password: "", // External auth, no local password needed
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.userRepo.CreateUserFromExternal(user)
}
func (u *usersUsecase) UpdateUserRole(nomorInduk, roleName string) error {
roleID, err := u.GetRoleByDepartment(roleName)
if err != nil {
return err
}
return u.userRepo.UpdateUserRole(nomorInduk, roleID)
}
func (u *usersUsecase) GetAllUsers() ([]entity.User, error) {
return u.userRepo.GetAllUsers()
}

View File

@ -0,0 +1,84 @@
package helper
import (
"users_management/m/model/dto/res"
"users_management/m/model/entity"
)
// ConvertToActivityLogResponses converts activity log entities to simple response format for users
func ConvertToActivityLogResponses(logs []entity.ActivityLog) []res.ActivityLogResponse {
var responses []res.ActivityLogResponse
for _, log := range logs {
response := res.ActivityLogResponse{
ID: log.ID,
Username: log.User.Username,
Role: log.User.Role.Name,
Action: log.Action,
Resource: log.Resource,
IPAddress: log.IPAddress,
UserAgent: log.UserAgent,
Timestamp: log.Timestamp,
}
responses = append(responses, response)
}
return responses
}
// ConvertToActivityLogDetailResponses converts activity log entities to detailed response format for admins
func ConvertToActivityLogDetailResponses(logs []entity.ActivityLog) []res.ActivityLogDetailResponse {
var responses []res.ActivityLogDetailResponse
for _, log := range logs {
response := res.ActivityLogDetailResponse{
ID: log.ID,
UserID: log.UserID,
Username: log.User.Username,
Role: log.User.Role.Name,
Action: log.Action,
Resource: log.Resource,
ResourceID: log.ResourceID,
OldData: log.OldData,
NewData: log.NewData,
IPAddress: log.IPAddress,
UserAgent: log.UserAgent,
Timestamp: log.Timestamp,
}
responses = append(responses, response)
}
return responses
}
// ConvertToSingleActivityLogResponse converts single activity log entity to simple response
func ConvertToSingleActivityLogResponse(log entity.ActivityLog) res.ActivityLogResponse {
return res.ActivityLogResponse{
ID: log.ID,
Username: log.User.Username,
Role: log.User.Role.Name,
Action: log.Action,
Resource: log.Resource,
IPAddress: log.IPAddress,
UserAgent: log.UserAgent,
Timestamp: log.Timestamp,
}
}
// ConvertToSingleActivityLogDetailResponse converts single activity log entity to detailed response
func ConvertToSingleActivityLogDetailResponse(log entity.ActivityLog) res.ActivityLogDetailResponse {
return res.ActivityLogDetailResponse{
ID: log.ID,
UserID: log.UserID,
Username: log.User.Username,
Role: log.User.Role.Name,
Action: log.Action,
Resource: log.Resource,
ResourceID: log.ResourceID,
OldData: log.OldData,
NewData: log.NewData,
IPAddress: log.IPAddress,
UserAgent: log.UserAgent,
Timestamp: log.Timestamp,
}
}

View File

@ -16,6 +16,7 @@ func ConvertToBackboneResponses(backbone []entity.Backbone, totalFishbone map[uu
}
backboneResp := res.BackboneResponse {
ID: backbone.ID,
BackboneCode: backbone.BackboneCode,
DevStart: backbone.DeviceStart.DeviceCode,
DevEnd: backbone.DeviceEnd.DeviceCode,
CoreAmount: backbone.CoreAmount,

View File

@ -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,

View File

@ -0,0 +1,230 @@
package helper
import (
"fmt"
"log"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/utils/service"
)
func ConvertToDeviceDetailsResponses(devices []entity.DeviceDetails, geocoder service.GeocodingService) ([]res.DeviceDetailsResponse, error) {
var responses []res.DeviceDetailsResponse
for _, device := range devices {
response, err := ConvertToDeviceDetailsResponse(device, geocoder)
if err != nil {
return nil, err
}
responses = append(responses, response)
}
return responses, nil
}
func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder service.GeocodingService) (res.DeviceDetailsResponse, error) {
// Get address
address := ""
if geocoder != nil {
addr, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude)
if err != nil {
log.Printf("Geocoding error for device %s: %v", device.DeviceCode, err)
address = fmt.Sprintf("Coordinates: %.6f, %.6f", device.Latitude, device.Longitude)
} else {
address = addr
}
}
// Get all backbones connected to this device
allBackbones := device.GetAllBackbones()
backboneInfos := make([]res.BackboneConnectionInfo, 0)
for _, backbone := range allBackbones {
isStartDevice := backbone.DeviceStartID == device.ID
connectedTo := ""
if isStartDevice && backbone.DeviceEnd.DeviceCode != "" {
connectedTo = backbone.DeviceEnd.DeviceCode
} else if !isStartDevice && backbone.DeviceStart.DeviceCode != "" {
connectedTo = backbone.DeviceStart.DeviceCode
}
info := res.BackboneConnectionInfo{
ID: backbone.ID,
BackboneCode: backbone.BackboneCode,
CoreAmount: backbone.CoreAmount,
IsStartDevice: isStartDevice,
ConnectedTo: connectedTo,
}
backboneInfos = append(backboneInfos, info)
}
// Get all fishbones connected to this device
allFishbones := device.GetAllFishbones()
fishboneInfos := make([]res.FishboneConnectionInfo, 0)
for _, fishbone := range allFishbones {
isStartDevice := fishbone.DeviceStartID == device.ID
connectedTo := ""
if isStartDevice && fishbone.DeviceEnd.DeviceCode != "" {
connectedTo = fishbone.DeviceEnd.DeviceCode
} else if !isStartDevice && fishbone.DeviceStart.DeviceCode != "" {
connectedTo = fishbone.DeviceStart.DeviceCode
}
info := res.FishboneConnectionInfo{
ID: fishbone.ID,
FishboneCode: fishbone.FishboneCode,
CoreAmount: fishbone.CoreAmount,
BackboneCode: fishbone.Backbone.BackboneCode,
IsStartDevice: isStartDevice,
ConnectedTo: connectedTo,
}
fishboneInfos = append(fishboneInfos, info)
}
// Convert tower connections - safely handle nullable ExternalTower
towerInfos := make([]res.TowerConnectionDetail, 0)
for _, tower := range device.Towers {
distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude)
// Safely handle nullable ExternalTower field
var externalTower *bool
if tower.ExternalTower != nil {
externalTower = tower.ExternalTower
} // If tower.ExternalTower is nil, externalTower remains nil
// Safely handle nullable ImageURL
var imageURL *string
if tower.ImageURL != "" {
imageURL = &tower.ImageURL
}
info := res.TowerConnectionDetail{
ID: tower.ID,
TowerCode: tower.TowerCode,
Distance: distance,
ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil
ImageURL: imageURL,
}
towerInfos = append(towerInfos, info)
}
// Convert entity.PortAssignmentResponse to res.PortAssignmentResponse
entityPortAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
resPortAssignments := make([]res.PortAssignmentResponse, len(entityPortAssignments))
for i, pa := range entityPortAssignments {
resPortAssignments[i] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
}
}
portAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
// Ensure we show all ports up to PortAmount
portAssignmentMap := make(map[int]res.PortAssignmentResponse)
for _, pa := range portAssignments {
portAssignmentMap[pa.PortNumber] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
}
}
// Fill in missing ports
finalPortAssignments := make([]res.PortAssignmentResponse, 0)
for i := 1; i <= device.PortAmount; i++ {
if pa, exists := portAssignmentMap[i]; exists {
finalPortAssignments = append(finalPortAssignments, pa)
} else {
// Determine if this port should be occupied
isOccupied := false
// Count occupied ports before this one
occupiedBefore := 0
for j := 1; j < i; j++ {
if existingPA, exists := portAssignmentMap[j]; exists && existingPA.IsOccupied {
occupiedBefore++
}
}
// Count total ports with customers
customersCount := 0
for _, pa := range portAssignments {
if pa.CustomerName != nil && *pa.CustomerName != "" {
customersCount++
}
}
// This port should be occupied if we haven't reached port_used yet
if occupiedBefore + customersCount < device.DevicePort.PortUsed {
isOccupied = true
}
finalPortAssignments = append(finalPortAssignments, res.PortAssignmentResponse{
PortNumber: i,
CustomerName: nil,
IsOccupied: isOccupied,
})
}
}
var customerNames []string
// If port_used is 0, return empty port assignments
if device.PortAmount == 0 {
finalPortAssignments = []res.PortAssignmentResponse{}
customerNames = []string{}
} else if device.DevicePort.PortUsed == 0 {
// If port_used is 0, return empty port assignments but keep port_amount structure
finalPortAssignments = []res.PortAssignmentResponse{}
customerNames = []string{}
} else {
// Get port assignments with details
portAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
// Convert to response format
finalPortAssignments = make([]res.PortAssignmentResponse, len(portAssignments))
for i, pa := range portAssignments {
finalPortAssignments[i] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
}
}
// Get customer names
customerNames = device.DevicePort.GetCustomerNamesOnly()
}
response := res.DeviceDetailsResponse{
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Address: address,
Longitude: device.Longitude,
Latitude: device.Latitude,
Status: string(device.Status),
PortAmount: device.PortAmount,
PortUsed: device.DevicePort.PortUsed,
PortAvailable: device.DevicePort.PortAvailable,
CustomerNames: customerNames, // Just customer names
PortAssignments: finalPortAssignments, // Use finalPortAssignments instead of resPortAssignments
Region: device.Region,
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
Backbones: backboneInfos,
Fishbones: fishboneInfos,
Towers: towerInfos,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
return response, nil
}
// ... rest of helper functions remain the same

View File

@ -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
}

View File

@ -0,0 +1,83 @@
package helper
import (
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"github.com/google/uuid"
)
// ConvertToFishboneResponses converts fishbone entities to response DTOs
func ConvertToFishboneResponses(fishbones []entity.Fishbone, totalFishbone map[uuid.UUID]int) ([]res.FishboneResponse, error) {
var responses []res.FishboneResponse
for _, fishbone := range fishbones {
fishboneResp := res.FishboneResponse{
ID: fishbone.ID,
FishboneCode: fishbone.FishboneCode,
BackboneCode: fishbone.Backbone.BackboneCode,
DeviceStart: fishbone.DeviceStart.DeviceCode,
DeviceEnd: fishbone.DeviceEnd.DeviceCode,
CoreAmount: fishbone.CoreAmount,
CreatedAt: fishbone.CreatedAt,
UpdatedAt: fishbone.UpdatedAt,
}
responses = append(responses, fishboneResp)
}
return responses, nil
}
// ConvertToFishboneDetailResponse converts a single fishbone entity to detailed response
func ConvertToFishboneDetailResponse(fishbone entity.Fishbone) res.FishboneDetailResponse {
return res.FishboneDetailResponse{
ID: fishbone.ID,
FishboneCode: fishbone.FishboneCode,
Backbone: res.BackboneInfo{
BackboneCode: fishbone.Backbone.BackboneCode,// Fixed missing field
},
CoreAmount: fishbone.CoreAmount,
CreatedAt: fishbone.CreatedAt,
UpdatedAt: fishbone.UpdatedAt,
}
}
// ConvertToSimpleFishboneResponses converts fishbone entities to simple response format
func ConvertToSimpleFishboneResponses(fishbones []entity.Fishbone) []res.FishboneResponse {
var responses []res.FishboneResponse
for _, fishbone := range fishbones {
fishboneResp := res.FishboneResponse{
ID: fishbone.ID,
FishboneCode: fishbone.FishboneCode,
BackboneCode: fishbone.Backbone.BackboneCode,
DeviceStart: fishbone.DeviceStart.DeviceCode,
DeviceEnd: fishbone.DeviceEnd.DeviceCode,
CoreAmount: fishbone.CoreAmount,
CreatedAt: fishbone.CreatedAt,
UpdatedAt: fishbone.UpdatedAt,
}
responses = append(responses, fishboneResp)
}
return responses
}
// GetFishboneStats calculates statistics for fishbones
func GetFishboneStats(fishbones []entity.Fishbone) map[string]interface{} {
totalFishbones := len(fishbones)
totalCores := 0
backboneCount := make(map[uuid.UUID]int)
for _, fishbone := range fishbones {
totalCores += fishbone.CoreAmount
backboneCount[fishbone.BackboneID]++
}
return map[string]interface{}{
"total_cores": totalCores,
"average_cores": float64(totalCores) / float64(totalFishbones),
"unique_backbones": len(backboneCount),
"backbone_usage": backboneCount,
}
}

View File

@ -0,0 +1,90 @@
package helper
import (
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
)
const (
MaxFileSize = 5 << 20 // 5MB
UploadDir = "./uploads/towers"
)
// SaveTowerImage saves uploaded image and returns the URL
func SaveTowerImage(file *multipart.FileHeader) (string, error) {
// Validate file size
if file.Size > MaxFileSize {
return "", fmt.Errorf("file size exceeds 5MB limit")
}
// Validate file type
allowedTypes := []string{".jpg", ".jpeg", ".png", ".webp"}
ext := strings.ToLower(filepath.Ext(file.Filename))
isAllowed := false
for _, allowedType := range allowedTypes {
if ext == allowedType {
isAllowed = true
break
}
}
if !isAllowed {
return "", fmt.Errorf("file type not allowed. Only jpg, jpeg, png, webp are allowed")
}
// Create upload directory if it doesn't exist
if err := os.MkdirAll(UploadDir, 0755); err != nil {
return "", fmt.Errorf("failed to create upload directory: %v", err)
}
// Generate unique filename
filename := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
filePath := filepath.Join(UploadDir, filename)
// Open uploaded file
src, err := file.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
// Create destination file
dst, err := os.Create(filePath)
if err != nil {
return "", fmt.Errorf("failed to create destination file: %v", err)
}
defer dst.Close()
// Copy file
if _, err := io.Copy(dst, src); err != nil {
return "", fmt.Errorf("failed to copy file: %v", err)
}
// Return URL (adjust based on your server setup)
return fmt.Sprintf("/uploads/towers/%s", filename), nil
}
// DeleteTowerImage deletes an image file
func DeleteTowerImage(imageURL string) error {
if imageURL == "" {
return nil
}
// Extract filename from URL
filename := filepath.Base(imageURL)
filePath := filepath.Join(UploadDir, filename)
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil // File doesn't exist, nothing to delete
}
// Delete file
return os.Remove(filePath)
}

View File

@ -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
}

View File

@ -8,61 +8,70 @@ import (
)
func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingService) ([]res.TowerResponse, error) {
var responses []res.TowerResponse
for _, tower := range towers {
var address string
var responses []res.TowerResponse
for _, tower := range towers {
var address string
if geocoder != nil {
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
if err != nil {
// Log specific geocoding error
log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err)
} else {
address = generatedAddress
}
} else {
log.Println("WARNING: Geocoder is nil")
}
if geocoder != nil {
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
if err != nil {
log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err)
} else {
address = generatedAddress
}
}
towerResp := res.TowerResponse{
ID: tower.ID,
DeviceCode: tower.Device.DeviceCode,
TowerCode: tower.TowerCode,
Longitude: tower.Longitude,
Latitude: tower.Latitude,
Address: address,
CreatedAt: tower.CreatedAt,
}
responses = append(responses, towerResp)
}
return responses, nil
var deviceCode *string
if tower.Device != nil {
deviceCode = &tower.Device.DeviceCode
}
towerResp := res.TowerResponse{
ID: tower.ID,
DeviceCode: deviceCode, // Now nullable
TowerCode: &tower.TowerCode,
Longitude: tower.Longitude,
Latitude: tower.Latitude,
Address: address,
ImageURL: tower.ImageURL,
ExternalTower: tower.ExternalTower, // Now nullable
CreatedAt: tower.CreatedAt,
UpdatedAt: tower.UpdatedAt,
}
responses = append(responses, towerResp)
}
return responses, nil
}
func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingService) (res.TowerResponse, error) {
var address string
var address string
if geocoder != nil {
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
if err != nil {
// Log specific geocoding error
log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err)
} else {
address = generatedAddress
}
} else {
log.Println("WARNING: Geocoder is nil")
}
if geocoder != nil {
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
if err != nil {
log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err)
} else {
address = generatedAddress
}
}
towerResp := res.TowerResponse{
ID: tower.ID,
DeviceCode: tower.Device.DeviceCode,
TowerCode: tower.TowerCode,
Longitude: tower.Longitude,
Latitude: tower.Latitude,
Address: address,
CreatedAt: tower.CreatedAt,
}
return towerResp, nil
var deviceCode *string
if tower.Device != nil {
deviceCode = &tower.Device.DeviceCode
}
towerResp := res.TowerResponse{
ID: tower.ID,
DeviceCode: deviceCode, // Now nullable
TowerCode: &tower.TowerCode,
Longitude: tower.Longitude,
Latitude: tower.Latitude,
Address: address,
ImageURL: tower.ImageURL,
ExternalTower: tower.ExternalTower, // Now nullable
CreatedAt: tower.CreatedAt,
UpdatedAt: tower.UpdatedAt,
}
return towerResp, nil
}

34
utils/insecure_http.go Normal file
View File

@ -0,0 +1,34 @@
package utils
import (
"crypto/tls"
"net/http"
"time"
)
// CreateInsecureHTTPClient creates an HTTP client that skips SSL verification
func CreateInsecureHTTPClient() *http.Client {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}
}
// CreateSecureHTTPClient creates a standard HTTP client with SSL verification
func CreateSecureHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
}
}
// GetHTTPClient returns appropriate client based on environment
func GetHTTPClient(skipSSL bool) *http.Client {
if skipSSL {
return CreateInsecureHTTPClient()
}
return CreateSecureHTTPClient()
}

View File

@ -17,7 +17,7 @@ type nominatimGeocoder struct {
client *http.Client
}
// NewGeocodingService creates a new geocoding service using Nominatim (OpenStreetMap)
func NewGeocodingService() GeocodingService {
client := &http.Client{
Timeout: 10 * time.Second,