This commit is contained in:
areeqakbr 2025-07-15 09:12:00 +07:00
commit 689505fa6a
110 changed files with 12051 additions and 782 deletions

View File

@ -2,8 +2,11 @@ package config
import (
"errors"
"github.com/joho/godotenv"
"os"
"strconv"
"time"
"github.com/joho/godotenv"
)
type DbConfig struct {
@ -13,10 +16,17 @@ type DbConfig struct {
DBPort string
DBName string
}
type AuthConfig struct {
UserAuthEnabled bool
JWTSecret string
TokenExpiry time.Duration
}
type LoginConfig struct {
LoginAPI string
AuthMeAPI string
LogoutAPI string
SkipSSLVerification bool
}
type TokenConfig struct {
@ -38,6 +48,7 @@ type Config struct {
TokenApi
ApiConfig
LoginConfig
AuthConfig
}
func (c *Config) readConfig() error {
@ -67,12 +78,42 @@ 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,
}
userAuthEnabled := true // Default to true for security
if authEnabledStr := os.Getenv("USER_AUTH_ENABLED"); authEnabledStr != "" {
var err error
userAuthEnabled, err = strconv.ParseBool(authEnabledStr)
if err != nil {
return errors.New("invalid USER_AUTH_ENABLED value, must be true or false")
}
}
// Parse token expiry (default to 24 hours)
tokenExpiry := 24 * time.Hour
if expiryStr := os.Getenv("JWT_TOKEN_EXPIRY_HOURS"); expiryStr != "" {
if hours, err := strconv.Atoi(expiryStr); err == nil {
tokenExpiry = time.Duration(hours) * time.Hour
}
}
c.AuthConfig = AuthConfig{
UserAuthEnabled: userAuthEnabled,
JWTSecret: os.Getenv("JWT_SECRET"),
TokenExpiry: tokenExpiry,
}
if c.ApiConfig.ApiPort == "" {
return errors.New("failed to read environment variables")
}

View File

@ -0,0 +1,145 @@
package controller
import (
"net/http"
"strconv"
"users_management/m/config"
"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
cfg *config.Config // Add config field for middleware
}
func NewActivityLogController(activityLogUC usecase.ActivityLogUseCase, rg *gin.RouterGroup, cfg *config.Config) *ActivityLogController {
return &ActivityLogController{
activityLogUC: activityLogUC,
rg: rg,
cfg: cfg, // Initialize config
}
}
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.ConditionalRequireAnyRole(c.cfg, "Admin", "SuperAdmin"), c.getAllLogs)
// Admins and superadmins can see teknisi logs specifically (detailed response)
logs.GET("/teknisi", middleware.ConditionalRequireAnyRole(c.cfg, "Admin", "SuperAdmin"), 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

@ -0,0 +1,136 @@
package controller
import (
"net/http"
"users_management/m/model/dto"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
)
type AuthController struct {
authUC usecase.AuthUsecase
rg *gin.RouterGroup
}
func NewAuthController(authUC usecase.AuthUsecase, rg *gin.RouterGroup) *AuthController {
return &AuthController{
authUC: authUC,
rg: rg,
}
}
func (c *AuthController) Route() {
auth := c.rg.Group("/auth")
{
auth.POST("/login", c.login)
auth.POST("/logout", c.logout)
auth.POST("/validate", c.validateToken)
auth.POST("/test-token-expiry", c.testTokenExpiry)
}
}
func (c *AuthController) login(ctx *gin.Context) {
var loginDTO dto.UserLoginDTO
if err := ctx.ShouldBindJSON(&loginDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
token, role, name, err := c.authUC.Login(loginDTO)
if err != nil {
common.ErrorResponses(ctx, http.StatusUnauthorized, err.Error())
return
}
response := gin.H{
"token": token,
"user": gin.H{
"name": name,
"username": loginDTO.Username,
"role": role,
},
"expires_in": "24h",
}
common.SingleResponses(ctx, "Login successful", response)
}
func (c *AuthController) logout(ctx *gin.Context) {
token := ctx.GetHeader("Authorization")
if token == "" {
common.ErrorResponses(ctx, http.StatusBadRequest, "Authorization token required")
return
}
// Remove "Bearer " prefix
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
err := c.authUC.Logout(token)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Logout successful", nil)
}
func (c *AuthController) validateToken(ctx *gin.Context) {
token := ctx.GetHeader("Authorization")
if token == "" {
common.ErrorResponses(ctx, http.StatusBadRequest, "Authorization token required")
return
}
// Remove "Bearer " prefix
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
username, err := c.authUC.ValidateToken(token)
if err != nil {
common.ErrorResponses(ctx, http.StatusUnauthorized, err.Error())
return
}
response := gin.H{
"valid": true,
"username": username,
}
common.SingleResponses(ctx, "Token is valid", response)
}
func (c *AuthController) testTokenExpiry(ctx *gin.Context) {
token := ctx.GetHeader("Authorization")
if token == "" {
common.ErrorResponses(ctx, http.StatusBadRequest, "Authorization token required")
return
}
// Remove "Bearer " prefix
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
// Test if token is expired
_, err := c.authUC.ValidateToken(token)
if err != nil {
response := gin.H{
"expired": true,
"message": err.Error(),
}
common.SingleResponses(ctx, "Token expiry test result", response)
return
}
response := gin.H{
"expired": false,
"message": "Token is still valid",
}
common.SingleResponses(ctx, "Token expiry test result", response)
}

View File

@ -1,7 +1,9 @@
package controller
import (
"log"
"net/http"
"users_management/m/config"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -10,32 +12,32 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type BackboneController struct {
bu usecase.BackboneUseCase
rg *gin.RouterGroup
bu usecase.BackboneUseCase
rg *gin.RouterGroup
cfg *config.Config
}
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.ConditionalRequireAnyRole(bc.cfg, "Teknisi", "Admin", "Super Admin"), bc.CreateBackbone())
rg.PUT("/:uuid", middleware.ConditionalRequireAnyRole(bc.cfg, "Teknisi", "Admin", "Super Admin"), bc.UpdateBackbone())
}
}
func NewBackboneController(bu usecase.BackboneUseCase, rg *gin.RouterGroup, cfg *config.Config) *BackboneController {
return &BackboneController{
bu: bu,
rg: rg,
cfg: cfg,
}
}
func NewBackboneController(bu usecase.BackboneUseCase, rg *gin.RouterGroup) *BackboneController {
return &BackboneController{
bu: bu,
rg: rg,
}
}
func (bc *BackboneController) GetBackbone() gin.HandlerFunc {
return func(c *gin.Context) {
@ -56,6 +58,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 +66,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

@ -0,0 +1,140 @@
package controller
import (
"net/http"
"time"
"users_management/m/model/entity"
"users_management/m/usecase"
"users_management/m/utils"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type BypassController struct {
userUC usecase.UsersUsecase
rg *gin.RouterGroup
}
func NewBypassController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *BypassController {
return &BypassController{
userUC: userUC,
rg: rg,
}
}
func (c *BypassController) Route() {
bypass := c.rg.Group("/bypass")
{
// Dangerous endpoint - should be secured or removed in production
bypass.POST("/create-super-admin", c.createSuperAdmin)
bypass.GET("/roles", c.getRoles) // Helper to get role IDs
}
}
type CreateSuperAdminRequest struct {
Name string `json:"name" binding:"required"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
NomorInduk string `json:"nomor_induk" binding:"required"`
SecretKey string `json:"secret_key" binding:"required"` // Extra security
}
func (c *BypassController) createSuperAdmin(ctx *gin.Context) {
var req CreateSuperAdminRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
// Simple secret key check (change this in production)
if req.SecretKey != "create-super-admin-secret-2024" {
common.ErrorResponses(ctx, http.StatusForbidden, "Invalid secret key")
return
}
// Check if username already exists
existingUser, err := c.userUC.GetUserByUsernameWithStatus(req.Username)
if err == nil && existingUser.ID != uuid.Nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Username already exists")
return
}
// Check if nomor_induk already exists
if req.NomorInduk != "" {
existingUser, err := c.userUC.GetUserByNomorInduk(req.NomorInduk)
if err == nil && existingUser.ID != uuid.Nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Nomor induk already exists")
return
}
}
// Get or create Super Admin role
superAdminRoleID, err := c.userUC.GetRoleByDepartment("Super Admin")
if err != nil {
// If Super Admin role doesn't exist, try Admin
superAdminRoleID, err = c.userUC.GetRoleByDepartment("Admin")
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, "No admin role found in system")
return
}
}
// Hash password
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, "Failed to hash password")
return
}
// Create super admin user directly (bypass normal registration flow)
user := entity.User{
ID: uuid.New(),
Name: req.Name,
Username: req.Username,
Password: hashedPassword,
NomorInduk: &req.NomorInduk,
RoleID: superAdminRoleID,
Status: entity.UserStatusApproved, // Directly approved
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = c.userUC.CreateSuperAdminBypass(user)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, "Failed to create super admin: "+err.Error())
return
}
response := gin.H{
"message": "Super admin created successfully",
"user": gin.H{
"id": user.ID,
"name": user.Name,
"username": user.Username,
"nomor_induk": user.NomorInduk,
"status": string(user.Status),
},
"warning": "This bypass endpoint should be disabled in production",
}
common.SingleResponses(ctx, "Super admin created", response)
}
func (c *BypassController) getRoles(ctx *gin.Context) {
// Helper endpoint to see available roles
roles := []gin.H{
{"name": "Super Admin", "description": "Full system access"},
{"name": "Admin", "description": "Administrative access"},
{"name": "Teknisi", "description": "Technical user access"},
{"name": "Manager", "description": "Management access"},
}
response := gin.H{
"roles": roles,
"note": "Use the role name to get role ID via GetRoleByDepartment",
}
common.SingleResponses(ctx, "Available roles", response)
}

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,585 @@
package controller
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"strings"
"users_management/m/config"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
"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
cfg *config.Config // Add config field for middleware
}
func NewDeviceDetailsController(deviceDetailsUC usecase.DeviceDetailsUseCase, rg *gin.RouterGroup, cfg *config.Config) *DeviceDetailsController {
return &DeviceDetailsController{
deviceDetailsUC: deviceDetailsUC,
rg: rg,
cfg: cfg,
}
}
func (c *DeviceDetailsController) Route() {
deviceDetails := c.rg.Group("/device-details")
deviceDetails.Use(middleware.ConditionalRequireAnyRole(c.cfg, "Teknisi", "Admin", "Super Admin"))
{
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)
// delete images by filename
deviceDetails.DELETE("/:id/images/:filename", c.deleteDeviceImage)
deviceDetails.GET("/without-towers", c.getDevicesWithoutTowers)
deviceDetails.GET("/without-connections", c.getDevicesWithoutConnections)
}
}
func (c *DeviceDetailsController) getDevicesWithoutConnections(ctx *gin.Context) {
// Get device types from query parameters (optional)
deviceTypesParam := ctx.Query("device_types")
var deviceTypes []string
if deviceTypesParam != "" {
// Split by comma if multiple types provided
deviceTypes = strings.Split(deviceTypesParam, ",")
// Trim whitespace and normalize case
for i, dt := range deviceTypes {
trimmed := strings.TrimSpace(dt)
if strings.ToUpper(trimmed) == "OTB" {
deviceTypes[i] = "OTB"
} else if strings.ToLower(trimmed) == "closure" {
deviceTypes[i] = "closure"
} else {
deviceTypes[i] = trimmed
}
}
}
devices, err := c.deviceDetailsUC.GetDevicesWithoutConnections(deviceTypes)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
// Categorize devices by type for better response structure
closureDevices := make([]res.DeviceDetailsResponse, 0)
otbDevices := make([]res.DeviceDetailsResponse, 0)
for _, device := range devices {
if device.DeviceType == "closure" {
closureDevices = append(closureDevices, device)
} else if device.DeviceType == "OTB" {
otbDevices = append(otbDevices, device)
}
}
response := gin.H{
"devices": devices,
"total": len(devices),
"breakdown": gin.H{
"closure": gin.H{
"count": len(closureDevices),
"devices": closureDevices,
},
"otb": gin.H{
"count": len(otbDevices),
"devices": otbDevices,
},
},
"filter": gin.H{
"device_types": deviceTypes,
"criteria": "without_fishbones_and_backbones",
},
}
common.SingleResponses(ctx, "Devices without connections retrieved successfully", response)
}
func (c *DeviceDetailsController) getDevicesWithoutTowers(ctx *gin.Context) {
// Get device types from query parameters (optional)
deviceTypesParam := ctx.Query("device_types")
var deviceTypes []string
if deviceTypesParam != "" {
// Split by comma if multiple types provided
deviceTypes = strings.Split(deviceTypesParam, ",")
// Trim whitespace
for i, dt := range deviceTypes {
deviceTypes[i] = strings.TrimSpace(strings.ToUpper(dt))
}
}
devices, err := c.deviceDetailsUC.GetDevicesWithoutTowers(deviceTypes)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
response := gin.H{
"devices": devices,
"total": len(devices),
"filter": gin.H{
"device_types": deviceTypes,
"without_towers": true,
},
}
common.SingleResponses(ctx, "Devices without towers retrieved successfully", response)
}
func (c *DeviceDetailsController) deleteDeviceImage(ctx *gin.Context) {
// Parse device ID
id := ctx.Param("id")
deviceID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
// Get filename from URL parameter
filename := ctx.Param("filename")
if filename == "" {
common.ErrorResponses(ctx, http.StatusBadRequest, "Filename is required")
return
}
// Validate filename format (basic security check)
if strings.Contains(filename, "..") || strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid filename format")
return
}
// Call use case to delete the image
err = c.deviceDetailsUC.DeleteDeviceImage(deviceID, filename)
if err != nil {
if err.Error() == "device not found" {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
if err.Error() == "image not found in device" {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, fmt.Sprintf("Image %s deleted successfully", filename), nil)
}
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
}
contentType := ctx.GetHeader("Content-Type")
// Handle JSON request (no images)
if strings.Contains(contentType, "application/json") {
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)
return
}
// Handle multipart form request
err = ctx.Request.ParseMultipartForm(50 << 20) // 50MB for multiple images
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Failed to parse multipart form")
return
}
// Create update DTO from form data
deviceUpdateDTO := req.UpdateDeviceDetailsDTO{}
if deviceCode := ctx.PostForm("device_code"); deviceCode != "" {
deviceUpdateDTO.DeviceCode = &deviceCode
}
if deviceType := ctx.PostForm("device_type"); deviceType != "" {
deviceUpdateDTO.DeviceType = &deviceType
}
if longitudeStr := ctx.PostForm("longitude"); longitudeStr != "" {
if longitude, err := strconv.ParseFloat(longitudeStr, 64); err == nil {
deviceUpdateDTO.Longitude = &longitude
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude format")
return
}
}
if latitudeStr := ctx.PostForm("latitude"); latitudeStr != "" {
if latitude, err := strconv.ParseFloat(latitudeStr, 64); err == nil {
deviceUpdateDTO.Latitude = &latitude
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude format")
return
}
}
if portAmountStr := ctx.PostForm("port_amount"); portAmountStr != "" {
if portAmount, err := strconv.Atoi(portAmountStr); err == nil {
deviceUpdateDTO.PortAmount = &portAmount
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid port amount format")
return
}
}
if status := ctx.PostForm("status"); status != "" {
deviceUpdateDTO.Status = &status
}
if region := ctx.PostForm("region"); region != "" {
deviceUpdateDTO.Region = &region
}
if province := ctx.PostForm("province"); province != "" {
deviceUpdateDTO.Province = &province
}
if city := ctx.PostForm("city"); city != "" {
deviceUpdateDTO.City = &city
}
if district := ctx.PostForm("district"); district != "" {
deviceUpdateDTO.District = &district
}
// Handle TowerID
if towerIDStr := ctx.PostForm("tower_id"); towerIDStr != "" {
if towerID, err := uuid.Parse(towerIDStr); err == nil {
deviceUpdateDTO.TowerID = &towerID
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid tower ID format")
return
}
}
if oltIDStr := ctx.PostForm("olt_id"); oltIDStr != "" {
if oltID, err := uuid.Parse(oltIDStr); err == nil {
deviceUpdateDTO.OLTID = &oltID
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID format")
return
}
}
// Get multiple image files
form := ctx.Request.MultipartForm
imageFiles := form.File["images"] // Support multiple images
// Also support single image upload for backward compatibility
if len(imageFiles) == 0 {
if singleImage, err := ctx.FormFile("image"); err == nil {
imageFiles = []*multipart.FileHeader{singleImage}
}
}
// Handle replace_images flag
replaceImages := ctx.PostForm("replace_images") == "true"
err = c.deviceDetailsUC.UpdateDeviceDetailsWithMultipleImages(deviceID, deviceUpdateDTO, imageFiles, replaceImages)
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

@ -1,7 +1,13 @@
package controller
import (
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"strconv"
"strings"
"users_management/m/config"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -14,55 +20,269 @@ import (
type DeviceController struct {
du usecase.DeviceUseCase
rg *gin.RouterGroup
cfg *config.Config
}
func NewDeviceController(du usecase.DeviceUseCase, rg *gin.RouterGroup, cfg *config.Config) *DeviceController {
return &DeviceController{
du: du,
rg: rg,
cfg: cfg,
}
}
func (dc *DeviceController) Route() {
rg := dc.rg.Group("/devices")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.RateLimitMiddleware())
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.ConditionalRequireAnyRole(dc.cfg,"Teknisi", "Admin", "Super Admin"))
{
rg.POST("", dc.CreateDevice())
rg.GET("", dc.GetAllDevices())
rg.GET("/:uuid", dc.GetDeviceByID())
rg.PUT("/:uuid", dc.UpdateDevice())
rg.POST("/bulk-upload-images", dc.BulkUploadImages())
}
}
func NewDeviceController(du usecase.DeviceUseCase, rg *gin.RouterGroup) *DeviceController {
return &DeviceController{
du: du,
rg: rg,
}
}
func (dc *DeviceController) BulkUploadImages() gin.HandlerFunc {
return func(c *gin.Context) {
// Parse multipart form
err := c.Request.ParseMultipartForm(100 << 20) // 100MB for bulk upload
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
return
}
// Get device data from form
deviceDataStr := c.PostForm("devices")
if deviceDataStr == "" {
common.ErrorResponses(c, http.StatusBadRequest, "devices data is required")
return
}
// Parse device data
var devicesData []req.BulkDeviceImageUploadDTO
if err := json.Unmarshal([]byte(deviceDataStr), &devicesData); err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid devices data format")
return
}
// Get all image files
form := c.Request.MultipartForm
allFiles := form.File["images"]
if len(allFiles) == 0 {
common.ErrorResponses(c, http.StatusBadRequest, "No image files provided")
return
}
// Parse file distribution from form data
fileDistributionStr := c.PostForm("file_distribution")
var fileDistribution []int
if fileDistributionStr != "" {
if err := json.Unmarshal([]byte(fileDistributionStr), &fileDistribution); err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid file_distribution format")
return
}
} else {
// Default: distribute files evenly
filesPerDevice := len(allFiles) / len(devicesData)
remainder := len(allFiles) % len(devicesData)
fileDistribution = make([]int, len(devicesData))
for i := range fileDistribution {
fileDistribution[i] = filesPerDevice
if i < remainder {
fileDistribution[i]++
}
}
}
// Validate file distribution
if len(fileDistribution) != len(devicesData) {
common.ErrorResponses(c, http.StatusBadRequest,
fmt.Sprintf("File distribution count (%d) must match device count (%d)",
len(fileDistribution), len(devicesData)))
return
}
totalExpectedFiles := 0
for _, count := range fileDistribution {
totalExpectedFiles += count
}
if totalExpectedFiles != len(allFiles) {
common.ErrorResponses(c, http.StatusBadRequest,
fmt.Sprintf("Total files (%d) must match file distribution sum (%d)",
len(allFiles), totalExpectedFiles))
return
}
// Call use case
err = dc.du.BulkUploadImagesMultiple(devicesData, allFiles, fileDistribution)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
totalImages := len(allFiles)
common.SingleResponses(c, fmt.Sprintf("%d images uploaded successfully for %d devices", totalImages, len(devicesData)), nil)
}
}
func (dc *DeviceController) CreateDevice() gin.HandlerFunc {
return func(c *gin.Context) {
var deviceDTO req.DeviceDTO
err := c.ShouldBindJSON(&deviceDTO)
return func(c *gin.Context) {
contentType := c.GetHeader("Content-Type")
// Handle JSON request (no images)
if strings.Contains(contentType, "application/json") {
var deviceDTO req.DeviceDTO
err := c.ShouldBindJSON(&deviceDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
err = dc.du.CreateDevice(deviceDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
err = dc.du.CreateDevice(deviceDTO)
common.SingleResponses(c, "Device has been created", nil)
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
// Parse multipart form
err := c.Request.ParseMultipartForm(50 << 20) // 50MB for multiple images
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
return
}
common.SingleResponses(c, "Device has been created", nil)
}
// Extract form data for device
deviceCode := c.PostForm("device_code")
deviceType := c.PostForm("device_type")
longitudeStr := c.PostForm("longitude")
latitudeStr := c.PostForm("latitude")
portAmountStr := c.PostForm("port_amount")
status := c.PostForm("status")
province := c.PostForm("province")
city := c.PostForm("city")
district := c.PostForm("district")
// Validate required fields
if deviceCode == "" || deviceType == "" || longitudeStr == "" || latitudeStr == "" || status == "" {
common.ErrorResponses(c, http.StatusBadRequest, "Missing required fields")
return
}
// 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
}
// Parse port amount
portAmount := 0
if portAmountStr != "" {
portAmount, err = strconv.Atoi(portAmountStr)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid port amount")
return
}
}
var towerID *uuid.UUID
if towerIDStr := c.PostForm("tower_id"); towerIDStr != "" {
if parsedTowerID, err := uuid.Parse(towerIDStr); err == nil {
towerID = &parsedTowerID
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid tower ID format")
return
}
}
// Handle OLTID in form data
var oltID *uuid.UUID
if oltIDStr := c.PostForm("olt_id"); oltIDStr != "" {
if parsedOLTID, err := uuid.Parse(oltIDStr); err == nil {
oltID = &parsedOLTID
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid OLT ID format")
return
}
}
// Create DTO
deviceDTO := req.DeviceDTO{
DeviceCode: deviceCode,
DeviceType: deviceType,
Longitude: longitude,
Latitude: latitude,
PortAmount: portAmount,
Status: status,
TowerID: towerID, // Add TowerID
OLTID: oltID, // Add OLTID
}
// Handle optional string fields
if province != "" {
deviceDTO.Province = &province
}
if city != "" {
deviceDTO.City = &city
}
if district != "" {
deviceDTO.District = &district
}
// Get multiple image files
form := c.Request.MultipartForm
imageFiles := form.File["images"] // Multiple images
// Also support single image upload for backward compatibility
if len(imageFiles) == 0 {
if singleImage, err := c.FormFile("image"); err == nil {
imageFiles = []*multipart.FileHeader{singleImage}
}
}
err = dc.du.CreateDeviceWithMultipleImages(deviceDTO, imageFiles)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Device has been created", nil)
}
}
func (dc *DeviceController) GetAllDevices() gin.HandlerFunc {
return func(c *gin.Context) {
deviceType := c.Query("type")
if deviceType != "" {
deviceResp, err := dc.du.GetByType(deviceType)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Device type not found")
return
}
common.SingleResponses(c, "Success", deviceResp)
return
}
devices, err := dc.du.GetAllDevices()
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
common.ErrorResponses(c, http.StatusBadRequest, "something went wrong")
return
}
@ -75,12 +295,12 @@ func (dc *DeviceController) GetDeviceByID() gin.HandlerFunc {
id := c.Param("uuid")
uuid, err := uuid.Parse(id)
if err != nil{
common.ErrorResponses(c, http.StatusBadGateway, err.Error())
common.ErrorResponses(c, http.StatusBadGateway,"Invalid UUID")
return
}
device, err := dc.du.GetByID(uuid)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
common.ErrorResponses(c, http.StatusBadRequest, "Device not found")
return
}
@ -89,29 +309,128 @@ func (dc *DeviceController) GetDeviceByID() gin.HandlerFunc {
}
func (dc *DeviceController) UpdateDevice() 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")
deviceUUID, err := uuid.Parse(id) // Change variable name from 'uuid' to 'deviceUUID'
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID")
return
}
var deviceDTO req.UpdateDeviceDTO
err = c.ShouldBindJSON(&deviceDTO)
contentType := c.GetHeader("Content-Type")
// Handle JSON request (no images)
if strings.Contains(contentType, "application/json") {
var deviceDTO req.UpdateDeviceDTO
err = c.ShouldBindJSON(&deviceDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid request")
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
err = dc.du.UpdateDevice(deviceUUID, deviceDTO) // Use the new variable name
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error()) // Also fix error message
return
}
err = dc.du.UpdateDevice(uuid, deviceDTO)
common.SingleResponses(c, "Device has been updated", nil)
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
// Handle multipart form request
err = c.Request.ParseMultipartForm(50 << 20) // 50MB for multiple images
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
return
}
common.SingleResponses(c, "Device has been updated", nil)
}
// Create update DTO from form data
deviceUpdateDTO := req.UpdateDeviceDTO{}
if deviceCode := c.PostForm("device_code"); deviceCode != "" {
deviceUpdateDTO.DeviceCode = &deviceCode
}
if deviceType := c.PostForm("device_type"); deviceType != "" {
deviceUpdateDTO.DeviceType = &deviceType
}
if longitudeStr := c.PostForm("longitude"); longitudeStr != "" {
if longitude, err := strconv.ParseFloat(longitudeStr, 64); err == nil {
deviceUpdateDTO.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 {
deviceUpdateDTO.Latitude = &latitude
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid latitude format")
return
}
}
if portAmountStr := c.PostForm("port_amount"); portAmountStr != "" {
if portAmount, err := strconv.Atoi(portAmountStr); err == nil {
deviceUpdateDTO.PortAmount = &portAmount
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid port amount format")
return
}
}
if status := c.PostForm("status"); status != "" {
deviceUpdateDTO.Status = &status
}
if province := c.PostForm("province"); province != "" {
deviceUpdateDTO.Province = &province
}
if city := c.PostForm("city"); city != "" {
deviceUpdateDTO.City = &city
}
if district := c.PostForm("district"); district != "" {
deviceUpdateDTO.District = &district
}
// Handle TowerID in form data
if towerIDStr := c.PostForm("tower_id"); towerIDStr != "" {
if towerID, err := uuid.Parse(towerIDStr); err == nil {
deviceUpdateDTO.TowerID = &towerID
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid tower ID format")
return
}
}
// Handle OLTID in form data
if oltIDStr := c.PostForm("olt_id"); oltIDStr != "" {
if oltID, err := uuid.Parse(oltIDStr); err == nil {
deviceUpdateDTO.OLTID = &oltID
} else {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid OLT ID format")
return
}
}
// Get multiple image files
form := c.Request.MultipartForm
imageFiles := form.File["images"] // Support multiple images
// Also support single image upload for backward compatibility
if len(imageFiles) == 0 {
if singleImage, err := c.FormFile("image"); err == nil {
imageFiles = []*multipart.FileHeader{singleImage}
}
}
// Handle replace_images flag
replaceImages := c.PostForm("replace_images") == "true"
err = dc.du.UpdateDeviceWithMultipleImages(deviceUUID, deviceUpdateDTO, imageFiles, replaceImages) // Use the new variable name
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Device has been updated", nil)
}
}

View File

@ -2,6 +2,7 @@ package controller
import (
"net/http"
"users_management/m/config"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -14,105 +15,175 @@ import (
type FishboneController struct {
fu usecase.FishboneUseCase
rg *gin.RouterGroup
cfg *config.Config
}
func NewFishboneController(fu usecase.FishboneUseCase, rg *gin.RouterGroup, cfg *config.Config) *FishboneController {
return &FishboneController{
fu: fu,
rg: rg,
cfg: cfg,
}
}
func (fc *FishboneController) Route() {
rg := fc.rg.Group("/fishbone")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.RateLimitMiddleware())
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.ConditionalRequireAnyRole(fc.cfg,"Teknisi", "Admin", "Super Admin"))
// 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())
}
}
func NewFishboneController(fu usecase.FishboneUseCase, rg *gin.RouterGroup) *FishboneController {
return &FishboneController{
fu: fu,
rg: rg,
}
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) {
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,174 @@
package controller
import (
"net/http"
"strconv"
"users_management/m/config"
"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
cfg *config.Config
}
func NewNearestDeviceController(nearestDeviceUC usecase.NearestDeviceUseCase, rg *gin.RouterGroup, cfg *config.Config) *NearestDeviceController {
return &NearestDeviceController{
nearestDeviceUC: nearestDeviceUC,
rg: rg,
cfg: cfg,
}
}
func (c *NearestDeviceController) Route() {
nearestDevices := c.rg.Group("/nearest-devices")
nearestDevices.Use(middleware.ConditionalRequireAnyRole(c.cfg,"Teknisi", "Admin", "Super Admin"))
{
nearestDevices.POST("/search", c.getNearestDevices)
nearestDevices.GET("/:id", c.getNearestDeviceByID)
nearestDevices.POST("/towers/search", c.getNearestTowers)
nearestDevices.GET("/towers/:id", c.getNearestTowerByID)
}
}
func (c *NearestDeviceController) getNearestTowers(ctx *gin.Context) {
var request req.NearestTowerDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
towers, err := c.nearestDeviceUC.GetNearestTowers(request)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
response := gin.H{
"towers": towers,
"total": len(towers),
"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 towers retrieved successfully", response)
}
func (c *NearestDeviceController) getNearestTowerByID(ctx *gin.Context) {
id := ctx.Param("id")
towerID, err := uuid.Parse(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid tower 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
}
tower, err := c.nearestDeviceUC.GetNearestTowerByID(towerID, userLat, userLng)
if err != nil {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
common.SingleResponses(ctx, "Tower details retrieved successfully", tower)
}
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

@ -0,0 +1,229 @@
package controller
import (
"net/http"
"strconv"
"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 OLTController struct {
oltUC usecase.OLTUsecase
rg *gin.RouterGroup
}
func NewOLTController(oltUC usecase.OLTUsecase, rg *gin.RouterGroup) *OLTController {
return &OLTController{
oltUC: oltUC,
rg: rg,
}
}
func (c *OLTController) Route() {
oltGroup := c.rg.Group("/olt")
{
oltGroup.POST("", c.createOLT)
oltGroup.GET("", c.getAllOLTs)
oltGroup.GET("/:id", c.getOLTByID)
oltGroup.PUT("/:id", c.updateOLT)
oltGroup.DELETE("/:id", c.deleteOLT)
oltGroup.POST("/:id/assign-device", c.assignDeviceToOLT)
oltGroup.DELETE("/unassign-device/:deviceId", c.unassignDeviceFromOLT)
oltGroup.GET("/:id/devices", c.getDevicesByOLT)
}
}
func (c *OLTController) createOLT(ctx *gin.Context) {
var oltDTO req.OLTDTO
if err := ctx.ShouldBindJSON(&oltDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err := c.oltUC.CreateOLT(oltDTO)
if err != nil {
if err.Error() == "OLT name already exists" {
common.ErrorResponses(ctx, http.StatusConflict, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "OLT created successfully", gin.H{
"olt_name": oltDTO.OLTName,
})
}
func (c *OLTController) getAllOLTs(ctx *gin.Context) {
// Pagination parameters
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
olts, err := c.oltUC.GetAllOLTs()
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
response := gin.H{
"olts": olts,
"total": len(olts),
"page": page,
}
common.SingleResponses(ctx, "OLTs retrieved successfully", response)
}
func (c *OLTController) getOLTByID(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
olt, err := c.oltUC.GetOLTByID(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusNotFound, "OLT not found")
return
}
common.SingleResponses(ctx, "OLT details retrieved successfully", olt)
}
func (c *OLTController) updateOLT(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
var updateDTO req.UpdateOLTDTO
if err := ctx.ShouldBindJSON(&updateDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.oltUC.UpdateOLT(id, updateDTO)
if err != nil {
switch err.Error() {
case "OLT not found":
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
case "OLT name already exists":
common.ErrorResponses(ctx, http.StatusConflict, err.Error())
default:
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
}
return
}
common.SingleResponses(ctx, "OLT updated successfully", gin.H{
"olt_id": id,
})
}
func (c *OLTController) deleteOLT(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
err = c.oltUC.DeleteOLT(id)
if err != nil {
if err.Error() == "OLT not found" {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "OLT deleted successfully", gin.H{
"olt_id": id,
})
}
func (c *OLTController) assignDeviceToOLT(ctx *gin.Context) {
oltIDStr := ctx.Param("id")
oltID, err := uuid.Parse(oltIDStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
var assignDTO req.AssignDeviceToOLTDTO
if err := ctx.ShouldBindJSON(&assignDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.oltUC.AssignDeviceToOLT(oltID, assignDTO.DeviceID)
if err != nil {
if err.Error() == "only ODP devices can be assigned to OLT" {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Device assigned to OLT successfully", gin.H{
"olt_id": oltID,
"device_id": assignDTO.DeviceID,
})
}
func (c *OLTController) unassignDeviceFromOLT(ctx *gin.Context) {
deviceIDStr := ctx.Param("deviceId")
deviceID, err := uuid.Parse(deviceIDStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
err = c.oltUC.UnassignDeviceFromOLT(deviceID)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Device unassigned from OLT successfully", gin.H{
"device_id": deviceID,
})
}
func (c *OLTController) getDevicesByOLT(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
devices, err := c.oltUC.GetDevicesByOLT(id)
if err != nil {
if err.Error() == "OLT not found" {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
response := gin.H{
"devices": devices,
"total": len(devices),
}
common.SingleResponses(ctx, "OLT devices retrieved successfully", response)
}

View File

@ -1,7 +1,13 @@
package controller
import (
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"strconv"
"strings"
"users_management/m/config"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -14,28 +20,81 @@ import (
type TowerController struct {
tu usecase.TowerUseCase
rg *gin.RouterGroup
cfg *config.Config
}
func (tc *TowerController) Route() {
rg := tc.rg.Group("/tower")
rg.Use(middleware.AuthMiddleware())
rg.Use(middleware.RateLimitMiddleware())
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.ConditionalRequireAnyRole(tc.cfg,"Teknisi", "Admin","Super Admin")) // Apply middleware to all routes
{
rg.GET("", tc.GetTower())
rg.POST("", tc.CreateTower())
rg.GET("/:uuid", tc.GetTowerByID())
rg.PUT("/:uuid", tc.UpdateTower())
rg.POST("/bulk-upload-images", tc.BulkUploadImages())
}
}
func NewTowerController(tu usecase.TowerUseCase, rg *gin.RouterGroup) *TowerController {
func NewTowerController(tu usecase.TowerUseCase, rg *gin.RouterGroup, cfg *config.Config) *TowerController {
return &TowerController{
tu: tu,
rg: rg,
cfg: cfg,
}
}
func (tc *TowerController) BulkUploadImages() gin.HandlerFunc {
return func(c *gin.Context) {
// Parse multipart form
err := c.Request.ParseMultipartForm(50 << 20) // 50MB max for bulk
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
return
}
// Get tower data from form
towerDataStr := c.PostForm("towers")
if towerDataStr == "" {
common.ErrorResponses(c, http.StatusBadRequest, "towers data is required")
return
}
// Parse tower data
var towersData []req.BulkTowerImageUploadDTO
if err := json.Unmarshal([]byte(towerDataStr), &towersData); err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Invalid towers data format")
return
}
// Get image files
form := c.Request.MultipartForm
files := form.File["images"]
if len(files) == 0 {
common.ErrorResponses(c, http.StatusBadRequest, "No image files provided")
return
}
if len(files) != len(towersData) {
common.ErrorResponses(c, http.StatusBadRequest,
fmt.Sprintf("Number of images (%d) must match number of towers (%d)",
len(files), len(towersData)))
return
}
// Call use case
err = tc.tu.BulkUploadImages(towersData, files)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, fmt.Sprintf("%d tower images uploaded successfully", len(towersData)), nil)
}
}
func (tc *TowerController) GetTower() gin.HandlerFunc {
return func(c *gin.Context) {
towers, err := tc.tu.GetAll()
@ -50,24 +109,110 @@ 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 (no images)
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 files 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(50 << 20) // Increase to 50MB for multiple images
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 multiple image files
form := c.Request.MultipartForm
imageFiles := form.File["images"] // Changed from "image" to "images" to support multiple
// Also support single image upload for backward compatibility
if len(imageFiles) == 0 {
if singleImage, err := c.FormFile("image"); err == nil {
imageFiles = []*multipart.FileHeader{singleImage}
}
}
err = tc.tu.PostWithMultipleImages(towerDTO, imageFiles)
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 +235,113 @@ 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 (no images)
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 files for JSON requests
err := tc.tu.UpdateTowerWithMultipleImages(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
err = c.Request.ParseMultipartForm(50 << 20) // Increase to 50MB for multiple images
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 multiple image files
form := c.Request.MultipartForm
imageFiles := form.File["images"] // Support multiple images
// Also support single image upload for backward compatibility
if len(imageFiles) == 0 {
if singleImage, err := c.FormFile("image"); err == nil {
imageFiles = []*multipart.FileHeader{singleImage}
}
}
// Handle replace_images flag - if true, replace all images; if false, append to existing
replaceImages := c.PostForm("replace_images") == "true"
err = tc.tu.UpdateTowerWithMultipleImages(tower_uuid, towerUpdateDTO, imageFiles, replaceImages)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Tower has been updated", nil)
}
}

View File

@ -0,0 +1,263 @@
package controller
import (
"errors"
"net/http"
"users_management/m/middleware"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type UserRoleManagementController struct {
userUC usecase.UsersUsecase
rg *gin.RouterGroup
}
func NewUserRoleManagementController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *UserRoleManagementController {
return &UserRoleManagementController{
userUC: userUC,
rg: rg,
}
}
func (c *UserRoleManagementController) Route() {
userRole := c.rg.Group("/user-role-management")
userRole.Use(middleware.RequireAdminRole()) // Only Admin and Super Admin can access
{
userRole.PUT("/update/:id", c.updateUserRoleByID)
userRole.PUT("/update-by-username", c.updateUserRoleByUsername)
userRole.GET("/users", c.getAllUsersWithRoles)
userRole.GET("/available-roles", c.getAvailableRoles)
}
}
type UpdateUserRoleByIDRequest struct {
NewRole string `json:"new_role" binding:"required,oneof=Teknisi Admin"`
}
type UpdateUserRoleByUsernameRequest struct {
Username string `json:"username" binding:"required"`
NewRole string `json:"new_role" binding:"required,oneof=Teknisi Admin"`
}
func (c *UserRoleManagementController) updateUserRoleByID(ctx *gin.Context) {
// Get user ID from URL parameter
userIDStr := ctx.Param("id")
userID, err := uuid.Parse(userIDStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID format")
return
}
var req UpdateUserRoleByIDRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
// Get current user role to check permissions
currentUserRole, exists := ctx.Get("userRole")
if !exists {
common.ErrorResponses(ctx, http.StatusUnauthorized, "User role not found")
return
}
currentUserRoleStr, ok := currentUserRole.(string)
if !ok {
common.ErrorResponses(ctx, http.StatusUnauthorized, "Invalid user role format")
return
}
// Get target user to check current role and prevent invalid operations
targetUser, err := c.userUC.GetUserByID(userID)
if err != nil {
common.ErrorResponses(ctx, http.StatusNotFound, "User not found")
return
}
// Validate role change permissions
if err := c.validateRoleChange(currentUserRoleStr, targetUser.Role.Name, req.NewRole); err != nil {
common.ErrorResponses(ctx, http.StatusForbidden, err.Error())
return
}
// Update user role
err = c.userUC.UpdateUserRoleByID(userID, req.NewRole)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, "Failed to update user role: "+err.Error())
return
}
// Get updated user info
updatedUser, err := c.userUC.GetUserByID(userID)
if err != nil {
// Role was updated but couldn't fetch updated info
common.SingleResponses(ctx, "User role updated successfully", gin.H{
"user_id": userID,
"new_role": req.NewRole,
})
return
}
response := gin.H{
"user_id": updatedUser.ID,
"username": updatedUser.Username,
"name": updatedUser.Name,
"previous_role": targetUser.Role.Name,
"new_role": updatedUser.Role.Name,
"updated_by": ctx.GetString("userName"),
}
common.SingleResponses(ctx, "User role updated successfully", response)
}
func (c *UserRoleManagementController) updateUserRoleByUsername(ctx *gin.Context) {
var req UpdateUserRoleByUsernameRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
// Get current user role to check permissions
currentUserRole, exists := ctx.Get("userRole")
if !exists {
common.ErrorResponses(ctx, http.StatusUnauthorized, "User role not found")
return
}
currentUserRoleStr, ok := currentUserRole.(string)
if !ok {
common.ErrorResponses(ctx, http.StatusUnauthorized, "Invalid user role format")
return
}
// Get target user
targetUser, err := c.userUC.GetUserByUsernameWithStatus(req.Username)
if err != nil {
common.ErrorResponses(ctx, http.StatusNotFound, "User not found")
return
}
// Validate role change permissions
if err := c.validateRoleChange(currentUserRoleStr, targetUser.Role.Name, req.NewRole); err != nil {
common.ErrorResponses(ctx, http.StatusForbidden, err.Error())
return
}
// Update user role by username
err = c.userUC.UpdateUserRoleByUsername(req.Username, req.NewRole)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, "Failed to update user role: "+err.Error())
return
}
// Get updated user info
updatedUser, err := c.userUC.GetUserByUsernameWithStatus(req.Username)
if err != nil {
// Role was updated but couldn't fetch updated info
common.SingleResponses(ctx, "User role updated successfully", gin.H{
"username": req.Username,
"new_role": req.NewRole,
})
return
}
response := gin.H{
"user_id": updatedUser.ID,
"username": updatedUser.Username,
"name": updatedUser.Name,
"previous_role": targetUser.Role.Name,
"new_role": updatedUser.Role.Name,
"updated_by": ctx.GetString("userName"),
}
common.SingleResponses(ctx, "User role updated successfully", response)
}
func (c *UserRoleManagementController) getAllUsersWithRoles(ctx *gin.Context) {
users, err := c.userUC.GetAllUsers()
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, "Failed to get users: "+err.Error())
return
}
// Filter and format response
var userList []gin.H
for _, user := range users {
// Only show approved users
if user.Status == "approved" {
userInfo := gin.H{
"id": user.ID,
"username": user.Username,
"name": user.Name,
"nomor_induk": user.NomorInduk,
"role": user.Role.Name,
"status": user.Status,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
}
userList = append(userList, userInfo)
}
}
response := gin.H{
"users": userList,
"total": len(userList),
}
common.SingleResponses(ctx, "Users retrieved successfully", response)
}
func (c *UserRoleManagementController) getAvailableRoles(ctx *gin.Context) {
roles := []gin.H{
{
"name": "Teknisi",
"description": "Technical user with limited administrative access",
"can_be_assigned_by": []string{"Admin", "Super Admin"},
},
{
"name": "Admin",
"description": "Administrative user with full system access except user management",
"can_be_assigned_by": []string{"Admin", "Super Admin"},
},
}
currentUserRole := ctx.GetString("userRole")
response := gin.H{
"available_roles": roles,
"your_role": currentUserRole,
"note": "You can update users between Teknisi and Admin roles only",
}
common.SingleResponses(ctx, "Available roles for assignment", response)
}
// Helper function to validate role change permissions
func (c *UserRoleManagementController) validateRoleChange(currentUserRole, targetCurrentRole, newRole string) error {
// Super Admin can change any role to Teknisi or Admin
if currentUserRole == "Super Admin" {
return nil
}
// Admin can change roles but with some restrictions
if currentUserRole == "Admin" {
// Admin cannot change Super Admin roles
if targetCurrentRole == "Super Admin" {
return errors.New("cannot modify Super Admin users")
}
// Admin cannot assign Super Admin role
if newRole == "Super Admin" {
return errors.New("cannot assign Super Admin role")
}
return nil
}
// Only Admin and Super Admin can change roles
return errors.New("insufficient permissions to change user roles")
}

View File

@ -0,0 +1,128 @@
package controller
import (
"net/http"
"users_management/m/model/dto"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type UserRegistrationController struct {
userUC usecase.UsersUsecase
rg *gin.RouterGroup
}
func NewUserRegistrationController(userUC usecase.UsersUsecase, rg *gin.RouterGroup) *UserRegistrationController {
return &UserRegistrationController{
userUC: userUC,
rg: rg,
}
}
func (c *UserRegistrationController) Route() {
registration := c.rg.Group("/user-registration")
{
// Public registration endpoint
registration.POST("/register", c.registerUser)
}
adminRegistration := c.rg.Group("/user-registration")
adminRegistration.GET("/pending", c.getPendingUsers)
adminRegistration.PUT("/approve/:id", c.approveUser)
adminRegistration.PUT("/reject/:id", c.rejectUser)
}
func (c *UserRegistrationController) RegisterUser(ctx *gin.Context) {
c.registerUser(ctx)
}
func (c *UserRegistrationController) GetPendingUsers(ctx *gin.Context) {
c.getPendingUsers(ctx)
}
func (c *UserRegistrationController) ApproveUser(ctx *gin.Context) {
c.approveUser(ctx)
}
func (c *UserRegistrationController) RejectUser(ctx *gin.Context) {
c.rejectUser(ctx)
}
func (c *UserRegistrationController) registerUser(ctx *gin.Context) {
var registerDTO dto.UserRegisterDTO
if err := ctx.ShouldBindJSON(&registerDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err := c.userUC.RegisterUser(registerDTO)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
response := gin.H{
"message": "Registration successful. Your account is pending approval from an administrator.",
"status": "pending",
"next_steps": "Please wait for an administrator to approve your account before you can log in.",
}
common.SingleResponses(ctx, "User registered successfully", response)
}
func (c *UserRegistrationController) getPendingUsers(ctx *gin.Context) {
users, err := c.userUC.GetPendingUsers()
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
response := gin.H{
"users": users,
"total": len(users),
}
common.SingleResponses(ctx, "Pending users retrieved successfully", response)
}
func (c *UserRegistrationController) approveUser(ctx *gin.Context) {
userID, err := uuid.Parse(ctx.Param("id"))
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID")
return
}
err = c.userUC.ApproveUser(userID)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "User approved successfully", gin.H{
"user_id": userID,
"status": "approved",
})
}
func (c *UserRegistrationController) rejectUser(ctx *gin.Context) {
userID, err := uuid.Parse(ctx.Param("id"))
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid user ID")
return
}
err = c.userUC.RejectUser(userID)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(ctx, "User rejected successfully", gin.H{
"user_id": userID,
"status": "rejected",
})
}

View File

@ -19,7 +19,7 @@ type UsersController struct {
func (uc *UsersController) Route() {
rg:= uc.rg.Group("/users")
rg.Use(middleware.CORSMiddleware())
rg.Use(middleware.RateLoginMiddleware())
// rg.Use(middleware.RateLoginMiddleware())
rg.OPTIONS("/login", func(c *gin.Context) {
c.Status(http.StatusNoContent)
})

View File

@ -15,6 +15,7 @@ type Server struct {
ucManager manager.UsecaseManager
engine *gin.Engine
host string
cfg *config.Config
}
func NewServer() *Server {
@ -31,6 +32,10 @@ func NewServer() *Server {
repoManager := manager.NewRepositoryManager(infraManager)
ucManager := manager.NewUsecaseManager(repoManager, cfg)
engine := gin.Default()
engine.Use(middleware.CORSMiddleware())
engine.Use(middleware.ConditionalActivityLoggingMiddleware(ucManager.NewActivityLogUsecase(), cfg))
engine.Use(common.LoggingToFile())
host := fmt.Sprintf(":%s", cfg.ApiPort)
@ -38,22 +43,61 @@ func NewServer() *Server {
ucManager: ucManager,
engine: engine,
host: host,
cfg: cfg,
}
}
func (s *Server) setupController() {
rg := s.engine.Group("/api/v1")
controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route()
rg.Use(middleware.AuthMiddleware())
s.engine.Static("/uploads", "./uploads")
rg := s.engine.Group("/api/v1")
// Add bypass controller FIRST - before any auth middleware
controller.NewBypassController(s.ucManager.NewUserUsecase(), rg).Route()
// Add auth controller (public endpoints)
controller.NewAuthController(s.ucManager.NewAuthUsecase(), rg).Route()
publicRegistration := rg.Group("/user-registration")
{
publicRegistration.POST("/register", controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).RegisterUser)
}
// Add user registration controller (public endpoints)
// controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), rg).Route()
// Now apply auth middleware to protected routes
protected := rg.Use(middleware.ConditionalAuthMiddleware(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), s.cfg)).(*gin.RouterGroup)
{
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()
}
protectedRegistration := protected.Group("/user-registration")
{
protectedRegistration.GET("/pending", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).GetPendingUsers)
protectedRegistration.PUT("/approve/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).ApproveUser)
protectedRegistration.PUT("/reject/:id", middleware.RequireAdminRole(), controller.NewUserRegistrationController(s.ucManager.NewUserUsecase(), protected).RejectUser)
}
controller.NewUserRoleManagementController(s.ucManager.NewUserUsecase(), protected).Route()
controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(), protected).Route()
controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), protected).Route()
controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), protected, s.cfg).Route()
controller.NewBackboneController(s.ucManager.NewBackboneUsecase(), protected, s.cfg).Route()
controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), protected, s.cfg).Route()
controller.NewTowerController(s.ucManager.NewTowerUsecase(), protected, s.cfg).Route()
controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), protected).Route()
controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), protected, s.cfg).Route()
controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), protected).Route()
controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), protected, s.cfg).Route()
controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), protected, s.cfg).Route()
controller.NewOLTController(s.ucManager.NewOLTUsecase(), protected).Route()
}
}
func (s *Server) Run() {
s.setupController()
if err := s.engine.Run(s.host); err != nil {

View File

@ -1,5 +1,5 @@
# Use the official Golang image
FROM golang:1.23.1
FROM golang:1.23.1-alpine
# Set the working directory inside the container
WORKDIR /app

BIN
dev-nam Executable file

Binary file not shown.

6
go.mod
View File

@ -5,8 +5,11 @@ go 1.23.1
require (
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.24.0
github.com/golang-jwt/jwt/v4 v4.5.2
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 +18,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 +41,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
)

4
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=
@ -28,6 +26,8 @@ github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

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,31 @@ 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{},
&entity.OLT{},
)
}
func NewInfraManager(cfg *config.Config) (InfraManager, error) {
im := &infraManager{
cfg: cfg,

View File

@ -10,6 +10,14 @@ type RepositoryManager interface {
NewTowerRepository() repository.TowerRepo
NewDevicePortRepository() repository.DevicePortRepo
NewCountAssetsRepository() repository.CountAssetsRepo
NewActivityLogRepository() repository.ActivityLogRepo
NewDeviceInspectionRepository() repository.DeviceInspectionRepo
NewNearestDeviceRepository() repository.NearestDeviceRepo
NewDeviceDetailsRepository() repository.DeviceDetailsRepo
NewOLTRepo() repository.OLTRepo
}
type repositoryManager struct {
@ -22,6 +30,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 +59,20 @@ 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())
}
func (rm *repositoryManager) NewOLTRepo() repository.OLTRepo {
return repository.NewOLTRepo(rm.infra.Conn())
}

View File

@ -3,6 +3,7 @@ package manager
import (
"users_management/m/config"
"users_management/m/usecase"
"users_management/m/utils/service"
)
type UsecaseManager interface {
@ -15,15 +16,44 @@ type UsecaseManager interface {
NewTowerUsecase() usecase.TowerUseCase
NewDevicePortUsecase() usecase.DevicePortUseCase
NewCountAssetsUsecase() usecase.CountAssetsUseCase
NewActivityLogUsecase() usecase.ActivityLogUseCase
NewDeviceInspectionUsecase() usecase.DeviceInspectionUseCase
NewNearestDeviceUsecase() usecase.NearestDeviceUseCase
NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase
NewOLTUsecase() usecase.OLTUsecase
}
type usecaseManager struct {
repo RepositoryManager
geocoder service.GeocodingService
cfg *config.Config
}
func NewUsecaseManager(repo RepositoryManager, cfg *config.Config) UsecaseManager {
return &usecaseManager{repo: repo,cfg: cfg }
basegeoCoder := service.NewGeocodingService()
cachedGeocoder := service.NewCachedGeocodingService(basegeoCoder)
return &usecaseManager{repo: repo,cfg: cfg , geocoder: cachedGeocoder}
}
func (um *usecaseManager) NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase {
return usecase.NewDeviceDetailsUseCase(
um.repo.NewDeviceDetailsRepository(),
um.repo.NewOLTRepo(),
um.geocoder,
)
}
func (um *usecaseManager) NewNearestDeviceUsecase() usecase.NearestDeviceUseCase {
return usecase.NewNearestDeviceUseCase(
um.repo.NewNearestDeviceRepository(),
um.geocoder,
)
}
func (um *usecaseManager) NewUserUsecase() usecase.UsersUsecase {
@ -35,21 +65,51 @@ func (um *usecaseManager) NewAuthUsecase() usecase.AuthUsecase {
}
func (um *usecaseManager) NewDeviceUsecase() usecase.DeviceUseCase {
return usecase.NewDeviceUseCase(um.repo.NewDeviceRepository())
return usecase.NewDeviceUseCase(um.repo.NewDeviceRepository(), um.repo.NewOLTRepo(), um.geocoder)
}
func (um *usecaseManager) NewBackboneUsecase() usecase.BackboneUseCase {
return usecase.NewBackboneUseCase(um.repo.NewBackboneRepository())
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 {
return usecase.NewTowerUseCase(um.repo.NewTowerRepository())
return usecase.NewTowerUseCase(um.repo.NewTowerRepository(), um.geocoder)
}
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
)
}
func (um *usecaseManager) NewOLTUsecase() usecase.OLTUsecase {
return usecase.NewOLTUsecase(um.repo.NewOLTRepo(), um.repo.NewDeviceDetailsRepository(), service.NewGeocodingService())
}

View File

@ -0,0 +1,155 @@
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"users_management/m/config"
"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)
}
// ConditionalActivityLoggingMiddleware that respects auth config
func ConditionalActivityLoggingMiddleware(activityLogUC usecase.ActivityLogUseCase, cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
// If auth is disabled, skip activity logging
if !cfg.AuthConfig.UserAuthEnabled {
c.Next()
return
}
// If auth is enabled, run normal activity logging
ActivityLoggingMiddleware(activityLogUC)(c)
}
}
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,95 @@
package middleware
import (
"encoding/json"
"net/http"
"strings"
"log"
"net/http"
"strings"
"users_management/m/model/dto/res"
"users_management/m/utils/common"
"users_management/m/config"
"users_management/m/model/entity"
"users_management/m/usecase"
"users_management/m/utils"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
// ConditionalAuthMiddleware checks config to decide whether to apply auth
func ConditionalAuthMiddleware(userUC usecase.UsersUsecase, authUC usecase.AuthUsecase, cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
// If auth is disabled, skip authentication but set default values
if !cfg.AuthConfig.UserAuthEnabled {
log.Println("Authentication disabled - skipping auth middleware")
// Set default values for when auth is disabled
c.Set("userID", uuid.New()) // Generate a dummy UUID
c.Set("userName", "system")
c.Set("userRole", "Admin") // Default role when auth is disabled
c.Set("token", "no-auth-mode")
c.Next()
return
}
if token == "" {
common.ErrorResponses(c, http.StatusUnauthorized, "authorization token required")
c.Abort()
return
}
// If auth is enabled, run the local auth middleware
LocalAuthMiddleware(userUC, authUC)(c)
}
}
token = strings.TrimPrefix(token, "Bearer ")
// LocalAuthMiddleware handles local JWT authentication
func LocalAuthMiddleware(userUC usecase.UsersUsecase, authUC usecase.AuthUsecase) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
c.Set("token", token)
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 {
common.ErrorResponses(c, http.StatusInternalServerError, err.Error())
if token == "" {
common.ErrorResponses(c, http.StatusUnauthorized, "Authorization token required")
c.Abort()
return
}
c.Set("userID", authResponse.Data.NomorInduk)
token = strings.TrimPrefix(token, "Bearer ")
c.Set("token", token)
c.Next()
// Validate JWT token locally
_, username, err := utils.ValidateJWT(token)
if err != nil {
log.Printf("JWT validation error: %v", err)
common.ErrorResponses(c, http.StatusUnauthorized, "Invalid or expired token")
c.Abort()
return
}
}
}
// Get user from local database
user, err := userUC.GetUserByUsername(username)
if err != nil {
log.Printf("User not found: %s", username)
common.ErrorResponses(c, http.StatusUnauthorized, "User not found")
c.Abort()
return
}
// Check if user is approved
switch user.Status {
case entity.UserStatusPending:
common.ErrorResponses(c, http.StatusForbidden, "Your account is pending approval from an administrator. Please wait for approval before accessing the system.")
c.Abort()
return
case entity.UserStatusRejected:
common.ErrorResponses(c, http.StatusForbidden, "Your account has been rejected. Please contact an administrator for more information.")
c.Abort()
return
case entity.UserStatusApproved:
// User is approved, continue with normal flow
c.Set("userID", user.ID)
c.Set("userName", user.Username)
c.Set("userRole", user.Role.Name)
default:
common.ErrorResponses(c, http.StatusForbidden, "Your account status is invalid. Please contact an administrator.")
c.Abort()
return
}
c.Next()
}
}

View File

@ -1,23 +1,66 @@
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",
"http://103.110.8.103:80",// Add production URL
"http://103.110.8.103",
// Allow OPTIONS method to pass through
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
"http://nam.winteraccess.id",
c.Next()
}
}
}
// 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
}
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()
}
}

189
middleware/rbac.go Normal file
View File

@ -0,0 +1,189 @@
package middleware
import (
"net/http"
"users_management/m/config"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
)
// ConditionalRequireRole middleware that respects auth config
func ConditionalRequireRole(cfg *config.Config, allowedRoles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
// If auth is disabled, allow all requests
if !cfg.AuthConfig.UserAuthEnabled {
c.Next()
return
}
// If auth is enabled, check roles normally
RequireRole(allowedRoles...)(c)
}
}
// ConditionalRequireAnyRole middleware that respects auth config
func ConditionalRequireAnyRole(cfg *config.Config, roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
// If auth is disabled, allow all requests
if !cfg.AuthConfig.UserAuthEnabled {
c.Next()
return
}
// If auth is enabled, check roles normally
RequireAnyRole(roles...)(c)
}
}
// 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", "Super Admin")
}
// RequireSuperAdminRole middleware for superadmin-only access
func RequireSuperAdminRole() gin.HandlerFunc {
return RequireRole("Super Admin")
}
// 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,63 @@
package req
import (
"users_management/m/model/entity"
"github.com/google/uuid"
)
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"`
TowerID *uuid.UUID `json:"tower_id,omitempty"`
OLTID *uuid.UUID `json:"olt_id,omitempty"`
}
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"`
TowerID *uuid.UUID `json:"tower_id,omitempty"`
OLTID *uuid.UUID `json:"olt_id,omitempty"`
}
type AssignMultipleCustomersDTO struct {
CustomerName string `json:"customer_name" binding:"required"`
PortNumber *int `json:"port_number,omitempty"`
Status *entity.PortStatus `json:"status,omitempty"` // Add status field
Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field
}
type UpdateCustomerByPortDTO struct {
PortNumber int `json:"port_number,omitempty"`
NewCustomerName *string `json:"new_customer_name,omitempty"`
Status *entity.PortStatus `json:"status,omitempty"` // Add status field
Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field
}
type BulkUpdateCustomersByPortDTO struct {
Updates []UpdateCustomerByPortDTO `json:"updates" binding:"required"`
}
type RemoveCustomerByPortDTO struct {
PortNumber int `json:"port_number" binding:"required,min=1"`
}
type DeleteImageDTO struct {
Filename string `json:"filename" validate:"required"`
}

View File

@ -1,6 +1,10 @@
package req
import "github.com/google/uuid"
import (
"users_management/m/model/entity"
"github.com/google/uuid"
)
type DevicePort struct {
DeviceID uuid.UUID `json:"device_id"`
@ -10,4 +14,27 @@ 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
Status *entity.PortStatus `json:"status,omitempty"` // Add status field
Bandwidth *string `json:"bandwidth,omitempty"` // Add bandwidth field
}
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"`
Status *entity.PortStatus `json:"status,omitempty" validate:"omitempty,portstatus"`
Bandwidth *string `json:"bandwidth,omitempty" validate:"omitempty,min=1"`
}
type UpdatePortAssignmentsDTO struct {
PortAssignments []PortAssignmentDTO `json:"port_assignments" binding:"required"`
}

View File

@ -1,13 +1,19 @@
package req
import "github.com/google/uuid"
type DeviceDTO struct {
DeviceCode string `json:"device_code" validate:"required"`
DeviceType string `json:"device_type" validate:"required"`
Longitude float64 `json:"longitude" validate:"required"`
Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"`
PortAmount int `json:"port_amount" 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"`
TowerID *uuid.UUID `json:"tower_id,omitempty"`
OLTID *uuid.UUID `json:"olt_id,omitempty"`
}
type UpdateDeviceDTO struct {
@ -15,6 +21,19 @@ type UpdateDeviceDTO struct {
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"`
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"`
TowerID *uuid.UUID `json:"tower_id,omitempty"`
OLTID *uuid.UUID `json:"olt_id,omitempty"`
}
type BulkDeviceImageUploadDTO struct {
DeviceID uuid.UUID `json:"device_id" validate:"required"`
}
type BulkDeviceImagesDTO struct {
Devices []BulkDeviceImageUploadDTO `json:"devices" validate:"required,min=1"`
}

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

@ -0,0 +1,11 @@
package req
type NearestTowerDTO 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"`
}

19
model/dto/req/olt.go Normal file
View File

@ -0,0 +1,19 @@
package req
import "github.com/google/uuid"
type OLTDTO struct {
OLTName string `json:"olt_name" validate:"required,min=3,max=100"`
}
type UpdateOLTDTO struct {
OLTName *string `json:"olt_name,omitempty" validate:"omitempty,min=3,max=100"`
}
type AssignDeviceToOLTDTO struct {
DeviceID uuid.UUID `json:"device_id" validate:"required"`
}
type UnassignDeviceFromOLTDTO struct {
DeviceID uuid.UUID `json:"device_id" validate:"required"`
}

View File

@ -3,15 +3,29 @@ 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"`
}
// Add to model/dto/req/tower_dto.go
type BulkTowerImageUploadDTO struct {
TowerID uuid.UUID `json:"tower_id" validate:"required"`
}
type BulkTowerImagesDTO struct {
Towers []BulkTowerImageUploadDTO `json:"towers" validate:"required,min=1"`
}

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

@ -0,0 +1,20 @@
package res
import (
"time"
"github.com/google/uuid"
)
type BackboneResponse struct {
ID uuid.UUID `json:"id"`
BackboneCode string `json:"backbone_code"`
DevStart string `json:"dev_start"`
DevEnd string `json:"dev_end"`
DeviceStartID uuid.UUID `json:"device_start_id"`
DeviceEndID uuid.UUID `json:"device_end_id"`
CoreAmount int `json:"core_amount"`
TotalFishbone int `json:"total_fishbone"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -0,0 +1,9 @@
package res
import "github.com/google/uuid"
type DeviceTypeResponse struct {
ID uuid.UUID `json:"id"`
DeviceType string `json:"device_type"`
DeviceCode string `json:"device_code"`
}

View File

@ -0,0 +1,61 @@
package res
import (
"time"
"users_management/m/model/entity"
"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"`
ImageURLs []string `json:"image_urls"`
TowerID *uuid.UUID `json:"tower_id,omitempty"` // Add TowerID
OLT *OLTInfo `json:"olt"` // Add OLT name
// 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 OLTInfo struct {
OLTID uuid.UUID `json:"id"`
OLTName string `json:"name"`
}
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"`
ImageURLs []string `json:"image_urls"` // Store multiple images as JSONB
}
type PortAssignmentResponse struct {
PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"`
IsOccupied bool `json:"is_occupied"`
Status entity.PortStatus `json:"status"` // Add status field
Bandwidth *string `json:"bandwidth"` // Add bandwidth field
}

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

@ -0,0 +1,26 @@
package res
import (
"time"
"github.com/google/uuid"
)
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"`
PortAmount int `json:"port_amount"`
Status string `json:"status"`
Address string `json:"address"`
Region *string `json:"region"`
Province *string `json:"province"`
City *string `json:"city"`
District *string `json:"district"`
ImageURL *string `json:"image_url"` // Primary image
ImageURLs []string `json:"image_urls"` // All images
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -0,0 +1,36 @@
package res
import (
"time"
"github.com/google/uuid"
)
type FishboneResponse struct {
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"`
DeviceStartID uuid.UUID `json:"device_start_id"`
DeviceEndID uuid.UUID `json:"device_end_id"`
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"`
DeviceStartID uuid.UUID `json:"device_start_id"`
DeviceEndID uuid.UUID `json:"device_end_id"`
BackboneCode string `json:"backbone_code"`
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,82 @@
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"`
ImageURL *string `json:"image_url"`
// 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"`
ImageURLs []string `json:"image_urls"`
// 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

@ -0,0 +1,54 @@
package res
import (
"time"
"github.com/google/uuid"
)
// Simplified response for list
type NearestTowerResponse struct {
ID uuid.UUID `json:"id"`
TowerCode string `json:"tower_code"`
Distance float64 `json:"distance_km"`
Address string `json:"address"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
ImageURL *string `json:"image_url,omitempty"`
ExternalTower *bool `json:"external_tower,omitempty"`
DeviceCode *string `json:"device_code,omitempty"`
Province *string `json:"province,omitempty"`
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// Detailed response for single tower
type NearestTowerDetailResponse struct {
ID uuid.UUID `json:"id"`
TowerCode string `json:"tower_code"`
Distance float64 `json:"distance_km"`
Address string `json:"address"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
ImageURL *string `json:"image_url,omitempty"`
ImageURLs []string `json:"image_urls,omitempty"`
ExternalTower *bool `json:"external_tower,omitempty"`
// Connected device information
Device *DeviceConnectionInfo `json:"device,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type DeviceConnectionInfo struct {
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType string `json:"device_type"`
Distance float64 `json:"distance_from_towers_to_device_km"` // Distance from tower to device
Address string `json:"address"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Status string `json:"status"`
PortAmount int `json:"port_amount"`
}

39
model/dto/res/olt.go Normal file
View File

@ -0,0 +1,39 @@
package res
import (
"time"
"github.com/google/uuid"
)
type OLTResponse struct {
ID uuid.UUID `json:"id"`
OLTName string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type OLTDetailResponse struct {
ID uuid.UUID `json:"id"`
OLTName string `json:"name"`
DeviceCount int `json:"device_count"`
Devices []DeviceDetailsResponse `json:"devices"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type OLTDeviceResponse struct {
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType string `json:"device_type"`
Address string `json:"address"`
Province *string `json:"province,omitempty"`
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
Status string `json:"status"`
PortAmount int `json:"port_amount"`
PortUsed int `json:"port_used"`
ImageURL *string `json:"image_url,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -0,0 +1,20 @@
package res
import (
"time"
"github.com/google/uuid"
)
type TowerResponse struct {
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"`
ImageURLs []string `json:"image_urls"` // Store multiple images as JSONB
ExternalTower *bool `json:"external_tower"` // Make nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -1,13 +1,39 @@
package dto
import (
"time"
"github.com/google/uuid"
)
// UserStatus defines the possible status values for a user
type UserStatus string
const (
UserStatusApproved UserStatus = "approved"
UserStatusRejected UserStatus = "rejected"
UserStatusPending UserStatus = "pending"
)
type UserRegisterDTO struct {
Name string `json:"name" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
RoleID string `json:"role_id" validate:"required,uuid"`
Name string `json:"name" validate:"required,min=2,max=100,alphaspace"`
Username string `json:"username" validate:"required,min=3,max=50,alphanumunderscore,nospace"`
Password string `json:"password" validate:"required,min=6,max=100"`
NomorInduk string `json:"nomor_induk" validate:"required,min=3,max=50,alphanum"`
}
type UserLoginDTO struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
type PendingUserResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
NomorInduk *string `json:"nomor_induk"`
RoleName string `json:"role_name"`
Status UserStatus `json:"status"`
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,99 @@
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"`
ImageURLs StringSlice `json:"image_urls" gorm:"type:jsonb"`
OLTID *uuid.UUID `json:"olt_id,omitempty" gorm:"type:uuid"`
TowerID *uuid.UUID `json:"tower_id,omitempty" gorm:"type:uuid"` // Add TowerID field
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relationships
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"`
OLT *OLT `json:"olt,omitempty" gorm:"foreignKey:OLTID;references:ID"`
Tower *Tower `json:"tower,omitempty" gorm:"foreignKey:TowerID;references:ID"` // Add Tower relationship
Towers []Tower `json:"towers" gorm:"foreignKey:DeviceID;references:ID"`
}
func (d *DeviceDetails) GetAllImageURLs() []string {
var allImages []string
// Add main image URL if exists
if d.ImageURL != nil && *d.ImageURL != "" {
allImages = append(allImages, *d.ImageURL)
}
// Add additional images
for _, img := range d.ImageURLs {
if img != "" {
// Avoid duplicates
isDuplicate := false
for _, existingImg := range allImages {
if existingImg == img {
isDuplicate = true
break
}
}
if !isDuplicate {
allImages = append(allImages, img)
}
}
}
return allImages
}
func (d *DeviceDetails) SetMultipleImages(imageURLs []string) {
if len(imageURLs) == 0 {
return
}
// Set primary image
primaryImage := imageURLs[0]
d.ImageURL = &primaryImage
// Set all images
d.ImageURLs = StringSlice(imageURLs)
}
// 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
}
func (DeviceDetails) TableName() string {
return "devices" // Use same table as Device entity
}

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,299 @@
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 PortStatus string
Device Device `gorm:"foreignKey:DeviceID"`
const (
PortStatusOn PortStatus = "on"
PortStatusOff PortStatus = "off"
PortStatusDyingGasp PortStatus = "dyingGasp"
PortStatusLOS PortStatus = "los"
)
type StringSlice []string
func (s StringSlice) Value() (driver.Value, error) {
if len(s) == 0 {
return "[]", nil
}
return json.Marshal(s)
}
func (s *StringSlice) Scan(value interface{}) error {
if value == nil {
*s = StringSlice{}
return nil
}
var bytes []byte
switch v := value.(type) {
case []byte: // []byte and []uint8 are the same type in Go
bytes = v
case string:
bytes = []byte(v)
default:
return fmt.Errorf("cannot scan %T into StringSlice", value)
}
// Handle empty string case
if len(bytes) == 0 {
*s = StringSlice{}
return nil
}
return json.Unmarshal(bytes, s)
}
type PortAssignment struct {
PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"` // Nullable for empty ports
Status PortStatus `json:"status"` // Add status field
Bandwidth *string `json:"bandwidth"` // Add bandwidth field (nullable)
}
type PortAssignmentResponse struct {
PortNumber int `json:"port_number"`
CustomerName *string `json:"customer_name"`
IsOccupied bool `json:"is_occupied"`
Status PortStatus `json:"status"` // Add status field
Bandwidth *string `json:"bandwidth"` // Add bandwidth field
}
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:
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 {
if dp.Device.DeviceType == "OTB" {
return []string{} // OTB devices do not have port assignments
}
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 {
if dp.Device.DeviceType == "OTB" {
return []string{} // OTB devices do not have port assignments
}
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.Device.DeviceType == "OTB" {
return []PortAssignmentResponse{}
}
if dp.PortUsed == 0 {
return []PortAssignmentResponse{}
}
var result []PortAssignmentResponse
// Step 1: Create a map of all port assignments
portMap := make(map[int]*PortAssignment)
maxPort := 0
if len(dp.PortAssignments) > 0 {
for _, assignment := range dp.PortAssignments {
portMap[assignment.PortNumber] = &assignment
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 != "" || portNum <= dp.PortUsed {
var customerName *string
if name != "" {
customerName = &name
}
assignment := PortAssignment{
PortNumber: portNum,
CustomerName: customerName,
Status: PortStatusOn, // Default status
Bandwidth: nil, // Default bandwidth
}
portMap[portNum] = &assignment
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
portsWithCustomers := make([]int, 0)
for portNum, assignment := range portMap {
if assignment.CustomerName != nil && *assignment.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
var status PortStatus = PortStatusOff // Default status for unoccupied ports
var bandwidth *string
if assignment := portMap[i]; assignment != nil {
customerName = assignment.CustomerName
status = assignment.Status
bandwidth = assignment.Bandwidth
} else if occupiedPorts[i] {
// Port is occupied but no assignment data - use defaults
status = PortStatusOn
}
result = append(result, PortAssignmentResponse{
PortNumber: i,
CustomerName: customerName,
IsOccupied: occupiedPorts[i],
Status: status,
Bandwidth: bandwidth,
})
}
return result
}
func (dp *DevicePort) GetCustomerNamesOnly() []string {
if dp.Device.DeviceType == "OTB" {
return []string{} // OTB devices do not have port assignments
}
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,79 @@
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"`
ImageURLs StringSlice `json:"image_urls" gorm:"type:jsonb"` // Store multiple images as JSONB
OLTID *uuid.UUID `json:"olt_id,omitempty" gorm:"type:uuid"` // Add this field
TowerID *uuid.UUID `json:"tower_id,omitempty" gorm:"type:uuid"` // Add TowerID field
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
OLT *OLT `json:"olt,omitempty" gorm:"foreignKey:OLTID;references:ID"`
Tower *Tower `json:"tower,omitempty" gorm:"foreignKey:TowerID;references:ID"`
}
func (d *Device) GetAllImageURLs() []string {
var allImages []string
// Add main image URL if exists
if d.ImageURL != nil && *d.ImageURL != "" {
allImages = append(allImages, *d.ImageURL)
}
// Add additional images
for _, img := range d.ImageURLs {
if img != "" && (d.ImageURL == nil || img != *d.ImageURL) { // Avoid duplicates
allImages = append(allImages, img)
}
}
return allImages
}
// Set multiple images (first one becomes primary)
func (d *Device) SetMultipleImages(imageURLs []string) {
if len(imageURLs) == 0 {
return
}
// Set primary image
primaryImage := imageURLs[0]
d.ImageURL = &primaryImage
// Set all images
d.ImageURLs = StringSlice(imageURLs)
}
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"
}

20
model/entity/olt.go Normal file
View File

@ -0,0 +1,20 @@
package entity
import (
"time"
"github.com/google/uuid"
)
type OLT struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
OLTName string `json:"olt_name" gorm:"unique;not null"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relationships - One OLT can have many devices
Devices []Device `json:"devices,omitempty" gorm:"foreignKey:OLTID;references:ID"`
}
func (OLT) TableName() string {
return "olts"
}

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,74 @@
// Update model/entity/tower.go
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"` // Keep for backward compatibility
ImageURLs StringSlice `json:"image_urls" gorm:"type:jsonb"` // Multiple images
ExternalTower *bool `json:"external_tower,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relationships
Device Device `json:"device,omitempty" gorm:"foreignKey:DeviceID"`
}
Device Device `gorm:"foreignKey:DeviceID"`
type TowerWithDistance struct {
ID uuid.UUID `json:"id"`
TowerCode string `json:"tower_code"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
ImageURL *string `json:"image_url"`
ExternalTower *bool `json:"external_tower"`
DevID *uuid.UUID `json:"dev_id"`
DeviceCode *string `json:"device_code"`
Province *string `json:"province"`
City *string `json:"city"`
District *string `json:"district"`
Distance float64 `json:"distance"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Helper method to get all image URLs
func (t *Tower) GetAllImageURLs() []string {
var allImages []string
// Add main image URL if exists
if t.ImageURL != "" {
allImages = append(allImages, t.ImageURL)
}
// Add additional images
for _, img := range t.ImageURLs {
if img != "" && img != t.ImageURL { // Avoid duplicates
allImages = append(allImages, img)
}
}
return allImages
}
// Set multiple images (first one becomes primary)
func (t *Tower) SetMultipleImages(imageURLs []string) {
if len(imageURLs) == 0 {
return
}
// Set primary image
t.ImageURL = imageURLs[0]
// Set all images
t.ImageURLs = StringSlice(imageURLs)
}
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

@ -1,18 +1,33 @@
package entity
import (
"time"
"time"
"github.com/google/uuid"
"github.com/google/uuid"
)
type UserStatus string
const (
UserStatusPending UserStatus = "pending"
UserStatusApproved UserStatus = "approved"
UserStatusRejected UserStatus = "rejected"
)
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"`
RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"`
Role Role `json:"role" gorm:"foreignKey:RoleID"`
Name string `json:"name" gorm:"unique"`
Username string `json:"username" gorm:"unique"`
Password string `json:"password"`
Status UserStatus `json:"status" gorm:"type:varchar(20);default:'pending'"`
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

@ -11,8 +11,8 @@ type BackboneRepo interface {
Post(backbone entity.Backbone) error
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 {
@ -59,3 +59,7 @@ func (r *backboneRepo) GetByID(id uuid.UUID) (entity.Backbone, error) {
}
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
}

1124
repository/device_details.go Normal file

File diff suppressed because it is too large Load Diff

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,8 @@
package repository
import (
"fmt"
"time"
"users_management/m/model/entity"
"github.com/google/uuid"
@ -13,6 +15,11 @@ type DevicesRepo interface {
Update(id uuid.UUID,updates map[string]interface{}) error
GetByID(id uuid.UUID) (entity.Device, error)
GetByType(deviceType string) ([]entity.Device, error)
BulkUpdateImages(updates map[uuid.UUID]string) error
BulkUpdateImagesMultiple(updates map[uuid.UUID][]string) error
SyncTowerLocationWithDevice(deviceID uuid.UUID) error
ValidateTowerExists(towerID uuid.UUID) (bool, error)
}
type devicesRepo struct {
@ -25,14 +32,130 @@ 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
func (r *devicesRepo) SyncTowerLocationWithDevice(deviceID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Get device details
var device entity.Device
if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil {
return fmt.Errorf("device not found: %w", err)
}
// Update tower location if device has a tower assigned
if device.TowerID != nil {
if err := tx.Model(&entity.Tower{}).
Where("id = ?", *device.TowerID).
Updates(map[string]interface{}{
"longitude": device.Longitude,
"latitude": device.Latitude,
"updated_at": time.Now(),
}).Error; err != nil {
return fmt.Errorf("failed to sync tower location: %w", err)
}
}
// Also update any towers that reference this device via dev_id
if err := tx.Model(&entity.Tower{}).
Where("dev_id = ?", deviceID).
Updates(map[string]interface{}{
"longitude": device.Longitude,
"latitude": device.Latitude,
"updated_at": time.Now(),
}).Error; err != nil {
return fmt.Errorf("failed to sync related towers location: %w", err)
}
return nil
})
}
func (r *devicesRepo) ValidateTowerExists(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 *devicesRepo) BulkUpdateImages(updates map[uuid.UUID]string) error {
multipleUpdates := make(map[uuid.UUID][]string)
for deviceID, imageURL := range updates {
multipleUpdates[deviceID] = []string{imageURL}
}
return r.BulkUpdateImagesMultiple(multipleUpdates)
}
func (r *devicesRepo) BulkUpdateImagesMultiple(updates map[uuid.UUID][]string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
for deviceID, imageURLs := range updates {
updateFields := map[string]interface{}{
"updated_at": time.Now(),
}
if len(imageURLs) > 0 {
// Set primary image (first one)
updateFields["image_url"] = imageURLs[0]
// Set all images using StringSlice
updateFields["image_urls"] = entity.StringSlice(imageURLs)
}
if err := tx.Model(&entity.Device{}).
Where("id = ?", deviceID).
Updates(updateFields).Error; err != nil {
return fmt.Errorf("failed to update device %s: %w", deviceID, err)
}
}
return nil
})
}
func (r *devicesRepo) Post(device entity.Device) error {
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
}
// Sync tower location with device location
if device.TowerID != nil {
if err := tx.Model(&entity.Tower{}).
Where("id = ?", *device.TowerID).
Updates(map[string]interface{}{
"longitude": device.Longitude,
"latitude": device.Latitude,
"updated_at": time.Now(),
}).Error; err != nil {
return fmt.Errorf("failed to sync tower location: %w", err)
}
}
return nil
})
}
func (r *devicesRepo) GetAll() ([]entity.Device, error) {
var devices []entity.Device
err := r.db.Find(&devices).Error
@ -41,12 +164,44 @@ func (r *devicesRepo) GetAll() ([]entity.Device, error) {
}
return devices, nil
}
func (r *devicesRepo) Update(id uuid.UUID,updates map[string]interface{}) error {
err := r.db.Model(&entity.Device{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
return err
}
return nil
func (r *devicesRepo) Update(id uuid.UUID, updates map[string]interface{}) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Validate tower exists if TowerID is being updated
if towerID, exists := updates["TowerID"]; exists && towerID != nil {
towerUUID := towerID.(uuid.UUID)
var towerExists bool
var err error
if towerExists, err = r.ValidateTowerExists(towerUUID); err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", towerUUID.String())
}
}
// Update device
if err := tx.Model(&entity.Device{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return err
}
// If location is updated, sync with tower
if _, hasLng := updates["Longitude"]; hasLng {
if _, hasLat := updates["Latitude"]; hasLat {
if err := r.SyncTowerLocationWithDevice(id); err != nil {
return err
}
}
}
// If TowerID is updated, sync location
if _, exists := updates["TowerID"]; exists {
if err := r.SyncTowerLocationWithDevice(id); err != nil {
return err
}
}
return nil
})
}
func (r *devicesRepo) GetByID(id uuid.UUID) (entity.Device, error) {
@ -57,3 +212,12 @@ func (r *devicesRepo) GetByID(id uuid.UUID) (entity.Device, error) {
}
return device, nil
}
func (r *devicesRepo) GetByType(deviceType string) ([]entity.Device, error) {
var devices []entity.Device
err := r.db.Where("device_type = ?", deviceType).Find(&devices).Error
if err != nil {
return devices, err
}
return devices, nil
}

View File

@ -13,6 +13,17 @@ type FishboneRepo interface {
Update(id uuid.UUID,updates map[string]interface{}) error
GetByID(id uuid.UUID) (entity.Fishbone, error)
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 {
@ -33,15 +44,30 @@ func (r *fishboneRepo) Post(fishbone entity.Fishbone) error {
return nil
}
func (r *fishboneRepo) GetByBackboneID(backboneID uuid.UUID) ([]entity.Fishbone, error) {
var fishbones []entity.Fishbone
err := r.db.Where("bb_id = ?", backboneID).
Preload("DeviceStart").
Preload("DeviceEnd").
Preload("Backbone").
Find(&fishbones).Error
return fishbones, err
}
func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) {
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.Preload("DeviceStart").Preload("DeviceEnd").Preload("Backbone").Find(&fishbones).Error
if err != nil {
return fishbones, err
}
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 {
@ -57,4 +83,71 @@ func (r *fishboneRepo) GetByID(id uuid.UUID) (entity.Fishbone, error) {
return fishbone, err
}
return fishbone, nil
}
func (r *fishboneRepo) CountFishbone() (map[uuid.UUID]int, error) {
type Result struct {
BackboneID uuid.UUID `gorm:"column:bb_id"`
Count int `gorm:"column:count"`
}
var results []Result
err := r.db.Model(&entity.Fishbone{}).
Select("bb_id, count(*) as count").
Group("bb_id").
Scan(&results).Error
if err != nil {
return nil, err
}
countMap := make(map[uuid.UUID]int)
for _, result := range results {
countMap[result.BackboneID] = result.Count
}
return countMap, nil
}
func (r *fishboneRepo) CountFishboneByBackboneID(backboneID uuid.UUID) (int, error) {
var count int64
err := r.db.Model(&entity.Fishbone{}).Where("bb_id = ?", backboneID).Count(&count).Error
if err != nil {
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,159 @@
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)
GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error)
GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error)
}
type nearestDeviceRepo struct {
db *gorm.DB
}
func NewNearestDeviceRepo(db *gorm.DB) NearestDeviceRepo {
return &nearestDeviceRepo{
db: db,
}
}
func (r *nearestDeviceRepo) GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error) {
var towers []entity.TowerWithDistance
// Build the subquery first - join with devices to get location filters
subQuery := r.db.Table("towers").
Select(`towers.id, towers.tower_code, towers.longitude, towers.latitude,
towers.image_url, towers.external_tower, towers.dev_id, towers.created_at, towers.updated_at,
devices.device_code, devices.province, devices.city, devices.district,
(6371 * acos(cos(radians(?)) * cos(radians(towers.latitude)) * cos(radians(towers.longitude) - radians(?)) + sin(radians(?)) * sin(radians(towers.latitude)))) AS distance`,
latitude, longitude, latitude).
Joins("LEFT JOIN devices ON towers.dev_id = devices.id")
// Apply location filters to subquery
if province != nil && *province != "" {
subQuery = subQuery.Where("devices.province = ?", *province)
}
if city != nil && *city != "" {
subQuery = subQuery.Where("devices.city = ?", *city)
}
if district != nil && *district != "" {
subQuery = subQuery.Where("devices.district = ?", *district)
}
// Use the subquery in the main query
err := r.db.Table("(?) as towers_with_distance", subQuery).
Where("distance <= ?", radius).
Order("distance ASC").
Limit(limit).
Scan(&towers).Error
return towers, err
}
func (r *nearestDeviceRepo) GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error) {
var tower entity.Tower
err := r.db.Preload("Device").Where("id = ?", id).First(&tower).Error
return tower, err
}
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
}

127
repository/olt_repo.go Normal file
View File

@ -0,0 +1,127 @@
package repository
import (
"fmt"
"users_management/m/model/entity"
"users_management/m/model/dto/req"
"github.com/google/uuid"
"gorm.io/gorm"
)
type OLTRepo interface {
Create(olt entity.OLT) error
GetAll() ([]entity.OLT, error)
GetByID(id uuid.UUID) (entity.OLT, error)
GetByIDWithDevices(id uuid.UUID) (entity.OLT, error)
Update(id uuid.UUID, updateDTO req.UpdateOLTDTO) error
Delete(id uuid.UUID) error
AssignDeviceToOLT(oltID, deviceID uuid.UUID) error
UnassignDeviceFromOLT(deviceID uuid.UUID) error
GetByName(name string) (entity.OLT, error)
GetDevicesByOLTID(oltID uuid.UUID) ([]entity.Device, error)
}
type oltRepo struct {
db *gorm.DB
}
func NewOLTRepo(db *gorm.DB) OLTRepo {
return &oltRepo{db: db}
}
func (r *oltRepo) Create(olt entity.OLT) error {
return r.db.Create(&olt).Error
}
func (r *oltRepo) GetAll() ([]entity.OLT, error) {
var olts []entity.OLT
err := r.db.Order("created_at DESC").Find(&olts).Error
return olts, err
}
func (r *oltRepo) GetByID(id uuid.UUID) (entity.OLT, error) {
var olt entity.OLT
err := r.db.Where("id = ?", id).First(&olt).Error
return olt, err
}
func (r *oltRepo) GetByIDWithDevices(id uuid.UUID) (entity.OLT, error) {
var olt entity.OLT
err := r.db.Preload("Devices").
Preload("Devices.DevicePort").
Where("id = ?", id).
First(&olt).Error
return olt, err
}
func (r *oltRepo) Update(id uuid.UUID, updateDTO req.UpdateOLTDTO) error {
updates := make(map[string]interface{})
if updateDTO.OLTName != nil {
updates["olt_name"] = *updateDTO.OLTName
}
if len(updates) == 0 {
return fmt.Errorf("no fields to update")
}
return r.db.Model(&entity.OLT{}).Where("id = ?", id).Updates(updates).Error
}
func (r *oltRepo) Delete(id uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// First, unassign all devices from this OLT
if err := tx.Model(&entity.Device{}).
Where("olt_id = ?", id).
Update("olt_id", nil).Error; err != nil {
return err
}
// Then delete the OLT
return tx.Where("id = ?", id).Delete(&entity.OLT{}).Error
})
}
func (r *oltRepo) AssignDeviceToOLT(oltID, deviceID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Check if OLT exists
var olt entity.OLT
if err := tx.Where("id = ?", oltID).First(&olt).Error; err != nil {
return fmt.Errorf("OLT not found: %w", err)
}
// Check if device exists and is ODP type
var device entity.Device
if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil {
return fmt.Errorf("device not found: %w", err)
}
if device.DeviceType != entity.ODP {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
// Assign device to OLT
return tx.Model(&device).Update("olt_id", oltID).Error
})
}
func (r *oltRepo) UnassignDeviceFromOLT(deviceID uuid.UUID) error {
return r.db.Model(&entity.Device{}).
Where("id = ?", deviceID).
Update("olt_id", nil).Error
}
func (r *oltRepo) GetByName(name string) (entity.OLT, error) {
var olt entity.OLT
err := r.db.Where("olt_name = ?", name).First(&olt).Error
return olt, err
}
func (r *oltRepo) GetDevicesByOLTID(oltID uuid.UUID) ([]entity.Device, error) {
var devices []entity.Device
err := r.db.Preload("DevicePort").
Where("olt_id = ? AND device_type = ?", oltID, entity.ODP).
Find(&devices).Error
return devices, err
}

View File

@ -1,6 +1,8 @@
package repository
import (
"fmt"
"time"
"users_management/m/model/entity"
"github.com/google/uuid"
@ -12,6 +14,9 @@ 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)
BulkUpdateImages(updates map[uuid.UUID]string) error
GetDeviceLocation(deviceID uuid.UUID) (longitude, latitude float64, err error)
}
type towerRepo struct {
@ -24,6 +29,15 @@ func NewTowerRepo(db *gorm.DB) TowerRepo {
}
}
func (r *towerRepo) GetDeviceLocation(deviceID uuid.UUID) (longitude, latitude float64, err error) {
var device entity.Device
err = r.db.Select("longitude, latitude").Where("id = ?", deviceID).First(&device).Error
if err != nil {
return 0, 0, fmt.Errorf("device not found: %w", err)
}
return device.Longitude, device.Latitude, nil
}
func (r *towerRepo) Post(tower entity.Tower) error {
err := r.db.Create(&tower).Error
if err != nil {
@ -34,13 +48,19 @@ func (r *towerRepo) Post(tower entity.Tower) error {
func (r *towerRepo) GetAll() ([]entity.Tower, error) {
var towers []entity.Tower
err := r.db.Preload("Device").Find(&towers).Error
if err != nil {
return towers, err
// Chain debug methods
result := r.db.Preload("Device").Find(&towers)
if result.Error != nil {
return nil, result.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 {
@ -56,4 +76,20 @@ func (r *towerRepo) GetByID(id uuid.UUID) (entity.Tower, error) {
return tower, err
}
return tower, nil
}
func (r *towerRepo) BulkUpdateImages(updates map[uuid.UUID]string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
for towerID, imageURL := range updates {
if err := tx.Model(&entity.Tower{}).
Where("id = ?", towerID).
Updates(map[string]interface{}{
"image_url": imageURL,
"updated_at": time.Now(),
}).Error; err != nil {
return fmt.Errorf("failed to update tower %s: %w", towerID, err)
}
}
return nil
})
}

View File

@ -3,48 +3,127 @@ package repository
import (
"users_management/m/model/entity"
"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)
CreateUserFromExternal(user entity.User) error
UpdateUserRole(nomorInduk string, roleID uuid.UUID) error
GetAllUsers() ([]entity.User, error)
RegisterUser(user entity.User) error
GetPendingUsers() ([]entity.User, error)
UpdateUserStatus(userID uuid.UUID, status entity.UserStatus) error
GetUserByID(userID uuid.UUID) (entity.User, error)
GetUserByUsernameWithStatus(username string) (entity.User, error)
CreateSuperAdminBypass(user entity.User) error
UpdateUserRoleByID(userID uuid.UUID, roleID uuid.UUID) error
UpdateUserRoleByUsername(username string, roleID uuid.UUID) error
GetUserByName(name string) (entity.User, error)
}
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) GetUserByName(name string) (entity.User, error) {
var user entity.User
// Use LOWER() function for case-insensitive comparison
err := r.db.Where("LOWER(name) = LOWER(?)", name).Preload("Role").First(&user).Error
return user, err
}
func (r *usersRepo) UpdateUserRoleByID(userID uuid.UUID, roleID uuid.UUID) error {
return r.db.Model(&entity.User{}).Where("id = ?", userID).Update("role_id", roleID).Error
}
func (r *usersRepo) UpdateUserRoleByUsername(username string, roleID uuid.UUID) error {
return r.db.Model(&entity.User{}).Where("username = ?", username).Update("role_id", roleID).Error
}
func (r *usersRepo) CreateSuperAdminBypass(user entity.User) error {
return r.db.Create(&user).Error
}
func (r *usersRepo) RegisterUser(user entity.User) error {
return r.db.Create(&user).Error
}
func (r *usersRepo) GetPendingUsers() ([]entity.User, error) {
var users []entity.User
err := r.db.Where("status = ?", entity.UserStatusPending).Preload("Role").Find(&users).Error
return users, err
}
func (r *usersRepo) UpdateUserStatus(userID uuid.UUID, status entity.UserStatus) error {
return r.db.Model(&entity.User{}).Where("id = ?", userID).Update("status", status).Error
}
func (r *usersRepo) GetUserByID(userID uuid.UUID) (entity.User, error) {
var user entity.User
err := r.db.Where("id = ?", userID).Preload("Role").First(&user).Error
return user, err
}
func (r *usersRepo) GetUserByUsernameWithStatus(username string) (entity.User, error) {
var user entity.User
err := r.db.Where("username = ?", username).Preload("Role").First(&user).Error
return user, err
}
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,99 @@
package usecase
import (
"encoding/json"
"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
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

@ -1,181 +1,95 @@
package usecase
import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"time"
"users_management/m/config"
"users_management/m/model/dto"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils"
"errors"
"users_management/m/config"
"users_management/m/model/dto"
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/go-playground/validator/v10"
)
type AuthUsecase interface {
Login(login dto.UserLoginDTO) (string,string, string ,error)
Logout(token string) error
Login(login dto.UserLoginDTO) (string, string, string, error)
Logout(token string) error
ValidateToken(token string) (string, error)
}
type authUsecase struct {
userRepo repository.UsersRepo
validate *validator.Validate
cfg *config.Config
validate *validator.Validate
}
func NewAuthUsecase(userRepo repository.UsersRepo, cfg *config.Config) AuthUsecase {
return &authUsecase{
userRepo: userRepo,
validate: validator.New(),
cfg: cfg,
validate: validator.New(),
}
}
func (u *authUsecase) Login(login dto.UserLoginDTO) (string, string, string, error) {
err := u.validate.Struct(login)
// Validate input
if err := u.validate.Struct(login); err != nil {
return "", "", "", err
}
// Get user from database (only approved users can login)
user, err := u.userRepo.GetUserByUsername(login.Username)
if err != nil {
return "","","", err
return "", "", "", errors.New("invalid username or password")
}
payload, err := json.Marshal(login)
if err != nil {
return "","","", err
}
req, err := http.NewRequest("POST", u.cfg.LoginAPI, bytes.NewBuffer(payload))
if err != nil {
return "","","", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "","","", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "","","", errors.New("wrong password or username")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "","","", err
}
var authResponse res.AuthResponses
err = json.Unmarshal(body, &authResponse)
if err != nil {
return "","","", err
}
token := authResponse.Token
meReq, err := http.NewRequest("POST", u.cfg.AuthMeAPI, nil)
if err != nil {
return "","","", err
}
meReq.Header.Set("Authorization", "Bearer "+token)
meReq.Header.Set("Accept", "application/json")
meResp, err := client.Do(meReq)
if err != nil {
return "","","", err
}
defer meResp.Body.Close()
if meResp.StatusCode != http.StatusOK {
return "","","", errors.New("failed to validate token: " + meResp.Status)
}
meBody, err := ioutil.ReadAll(meResp.Body)
if err != nil {
return "","","", err
}
var meResponse res.AuthMeResponse
err = json.Unmarshal(meBody, &meResponse)
if err != nil {
return "","","", err
}
departemen := meResponse.Data.Departemen
if departemen != "Teknisi" {
return "","","", errors.New("user is not a technician")
}
role , err := u.userRepo.GetRoleByDepartment(departemen)
if err != nil {
return "","","", err
}
password, err := utils.HashPassword(login.Password)
if err != nil {
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")
// Check user status
if user.Status != entity.UserStatusApproved {
switch user.Status {
case entity.UserStatusPending:
return "", "", "", errors.New("your account is pending approval from an administrator")
case entity.UserStatusRejected:
return "", "", "", errors.New("your account has been rejected. Please contact an administrator")
default:
return "", "", "", errors.New("your account is not active")
}
}
return token, role.Name, isUserExist.Name, nil
}else if err != nil && err != gorm.ErrRecordNotFound {
return "ERROR WHILE SEARCHING USERNAME","", "", err
}
// Verify password
if !utils.CheckPasswordHash(login.Password, user.Password) {
return "", "", "", errors.New("invalid username or password")
}
user := entity.User{
ID: uuid.New(),
Name: meResponse.Data.Nama,
Username: login.Username,
Password: password,
RoleID: role.Id,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = u.userRepo.Post(user)
if err != nil {
return "","","", err
}
// Generate JWT token
token, err := utils.GenerateJWT(user.ID.String(), user.Username, user.Role.Name)
if err != nil {
return "", "", "", err
}
return token, user.Role.Name, user.Name, nil
}
func (u *authUsecase) Logout(token string) error {
req, err := http.NewRequest("POST", u.cfg.LogoutAPI, nil)
if err != nil {
return err
}
// For JWT, we could implement a blacklist mechanism here
// For now, we'll just return success since JWT tokens expire naturally
return nil
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("failed to logout: " + resp.Status)
}
return nil
func (u *authUsecase) ValidateToken(token string) (string, error) {
_, username, err := utils.ValidateJWT(token)
if err != nil {
return "", err
}
// Verify user still exists and is approved
user, err := u.userRepo.GetUserByUsername(username)
if err != nil {
return "", errors.New("user not found")
}
if user.Status != entity.UserStatusApproved {
return "", errors.New("user account is not active")
}
return username, nil
}

View File

@ -4,89 +4,296 @@ import (
"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 BackboneUseCase interface {
CreateBackbone(backbone req.BackboneDTO) error
GetAllBackbone() ([]entity.Backbone, error)
GetAllBackbone() ([]res.BackboneResponse, error)
GetByID(id uuid.UUID) (entity.Backbone, error)
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) BackboneUseCase {
return &backboneUseCase{
backboneRepo: backboneRepo,
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 && startDevice.DeviceType == "OTB" {
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() ([]entity.Backbone, error) {
func (u *backboneUseCase) GetAllBackbone() ([]res.BackboneResponse, error) {
backbones, err := u.backboneRepo.GetAll()
if err != nil {
return backbones, err
return nil, err
}
return backbones, nil
totalFishbone,err := u.fishboneRepo.CountFishbone()
if err != nil {
return nil, err
}
backboneResp, err := helper.ConvertToBackboneResponses(backbones,totalFishbone)
if err != nil {
return nil, err
}
return backboneResp, nil
}
func (u *backboneUseCase) GetByID(id uuid.UUID) (entity.Backbone, error) {
func (u *backboneUseCase) GetByID(id uuid.UUID) (res.BackboneResponse, error) {
backbone, err := u.backboneRepo.GetByID(id)
if err != nil {
return backbone, err
return res.BackboneResponse{}, err
}
return backbone, nil
}
fishboneCount, err := u.fishboneRepo.CountFishboneByBackboneID(backbone.ID)
if err != nil {
return res.BackboneResponse{}, err
}
backboneResp, err := helper.ConvertToBackboneRespId(backbone,fishboneCount)
if err != nil {
return res.BackboneResponse{}, err
}
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(),
}

617
usecase/device_details.go Normal file
View File

@ -0,0 +1,617 @@
package usecase
import (
"errors"
"fmt"
"log"
"mime/multipart"
"strings"
"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
UpdateDeviceDetailsWithMultipleImages(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error
DeleteDeviceImage(deviceID uuid.UUID, filename string) error
GetDevicesWithoutTowers(deviceTypes []string) ([]res.DeviceDetailsResponse, error)
GetDevicesWithoutConnections(deviceTypes []string) ([]res.DeviceDetailsResponse, error)
ValidateOLTExists(oltID uuid.UUID) (bool, error)
}
type deviceDetailsUseCase struct {
deviceDetailsRepo repository.DeviceDetailsRepo
oltRepo repository.OLTRepo
geocoder service.GeocodingService
validate *validator.Validate
}
func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, oltRepo repository.OLTRepo,geocoder service.GeocodingService) DeviceDetailsUseCase {
return &deviceDetailsUseCase{
deviceDetailsRepo: deviceDetailsRepo,
oltRepo: oltRepo,
geocoder: geocoder,
validate: validator.New(),
}
}
func (u *deviceDetailsUseCase) ValidateOLTExists(oltID uuid.UUID) (bool, error) {
_, err := u.oltRepo.GetByID(oltID)
if err != nil {
return false, nil
}
return true, nil
}
func (u *deviceDetailsUseCase) GetDevicesWithoutConnections(deviceTypes []string) ([]res.DeviceDetailsResponse, error) {
// Validate device types and convert to uppercase
validTypes := map[string]bool{"CLOSURE": true, "OTB": true}
for i, deviceType := range deviceTypes {
upperDeviceType := strings.ToUpper(deviceType)
if !validTypes[upperDeviceType] {
return nil, fmt.Errorf("invalid device type: %s. Only CLOSURE and OTB are allowed", deviceType)
}
// Replace the original value in the slice with uppercase version for consistency
deviceTypes[i] = upperDeviceType
}
// If no device types provided, default to closure and OTB
if len(deviceTypes) == 0 {
deviceTypes = []string{"CLOSURE", "OTB"}
}
log.Printf("Fetching devices without connections for types: %v", deviceTypes)
devices, err := u.deviceDetailsRepo.GetDevicesWithoutConnections(deviceTypes)
if err != nil {
return nil, err
}
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder)
if err != nil {
return nil, err
}
return responses, nil
}
func (u *deviceDetailsUseCase) DeleteDeviceImage(deviceID uuid.UUID, filename string) error {
// Get current device
currentDevice, err := u.deviceDetailsRepo.GetByID(deviceID)
if err != nil {
return fmt.Errorf("device not found")
}
// Get all current image URLs
allImageURLs := currentDevice.GetAllImageURLs()
// Find the image URL that contains the filename
var imageURLToDelete string
var updatedImageURLs []string
for _, imageURL := range allImageURLs {
if strings.Contains(imageURL, filename) {
imageURLToDelete = imageURL
} else {
updatedImageURLs = append(updatedImageURLs, imageURL)
}
}
// Check if image was found
if imageURLToDelete == "" {
return fmt.Errorf("image not found in device")
}
// Don't allow deleting the last image if it's the primary image
if len(allImageURLs) == 1 && currentDevice.ImageURL != nil && *currentDevice.ImageURL == imageURLToDelete {
return fmt.Errorf("cannot delete the only remaining image")
}
// Prepare updates
updates := map[string]interface{}{
"updated_at": time.Now(),
}
if len(updatedImageURLs) == 0 {
// No images left
updates["image_url"] = nil
updates["image_urls"] = entity.StringSlice([]string{})
} else {
// Update primary image if it was deleted
if currentDevice.ImageURL != nil && *currentDevice.ImageURL == imageURLToDelete {
updates["image_url"] = updatedImageURLs[0] // Set first remaining image as primary
}
// Update all images
updates["image_urls"] = entity.StringSlice(updatedImageURLs)
}
// Update database
err = u.deviceDetailsRepo.Update(deviceID, updates)
if err != nil {
return fmt.Errorf("failed to update device in database: %w", err)
}
// Delete the actual file from filesystem
err = helper.DeleteDeviceImage(imageURLToDelete)
if err != nil {
// Log error but don't fail the request since database was already updated
fmt.Printf("Warning: Failed to delete image file %s: %v\n", imageURLToDelete, err)
}
return nil
}
func (u *deviceDetailsUseCase) GetDevicesWithoutTowers(deviceTypes []string) ([]res.DeviceDetailsResponse, error) {
// Validate device types
validTypes := map[string]bool{"ODP": true, "OTB": true}
for _, deviceType := range deviceTypes {
if !validTypes[deviceType] {
return nil, fmt.Errorf("invalid device type: %s. Only ODP and OTB are allowed", deviceType)
}
}
// If no device types provided, default to ODP and OTB
if len(deviceTypes) == 0 {
deviceTypes = []string{"ODP", "OTB"}
}
devices, err := u.deviceDetailsRepo.GetDevicesWithoutTowers(deviceTypes)
if err != nil {
return nil, err
}
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder)
if err != nil {
return nil, err
}
return responses, nil
}
func (u *deviceDetailsUseCase) UpdateDeviceDetailsWithMultipleImages(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) 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 {
if *deviceDTO.PortAmount < 0 {
return fmt.Errorf("port amount cannot be negative")
}
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 deviceDTO.TowerID != nil {
towerExists, err := u.deviceDetailsRepo.ValidateTowerExists(*deviceDTO.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", deviceDTO.TowerID.String())
}
updates["tower_id"] = *deviceDTO.TowerID
}
// Handle OLTID validation and update
if deviceDTO.OLTID!= nil {
// Get current device to check type
currentDevice, err := u.deviceDetailsRepo.GetByID(id)
if err != nil {
return fmt.Errorf("device not found: %w", err)
}
// Only ODP devices can be assigned to OLT
deviceType := string(currentDevice.DeviceType)
if deviceDTO.DeviceType != nil {
deviceType = *deviceDTO.DeviceType
}
if deviceType != "ODP" {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
oltExists, err := u.ValidateOLTExists(*deviceDTO.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", deviceDTO.OLTID.String())
}
updates["olt_id"] = *deviceDTO.OLTID
}
// Handle multiple image uploads
if len(imageFiles) > 0 {
// Get current device to handle existing images
currentDevice, err := u.deviceDetailsRepo.GetByID(id)
if err != nil {
return err
}
// Save new images
newImageURLs, err := helper.SaveDeviceImagesBulk(imageFiles)
if err != nil {
return err
}
var finalImageURLs []string
shouldReplace := len(replaceImages) > 0 && replaceImages[0]
if shouldReplace {
// Replace all images - delete old ones
for _, oldImageURL := range currentDevice.GetAllImageURLs() {
helper.DeleteDeviceImage(oldImageURL)
}
finalImageURLs = newImageURLs
} else {
// Append to existing images
existingImages := currentDevice.GetAllImageURLs()
finalImageURLs = append(existingImages, newImageURLs...)
}
// Update primary image (first image in the final list)
if len(finalImageURLs) > 0 && finalImageURLs[0] != "" {
updates["image_url"] = finalImageURLs[0]
}
// Update all images using image_urls column (not additional_images)
updates["image_urls"] = entity.StringSlice(finalImageURLs)
}
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) 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)
}
// Validate tower exists if TowerID is provided
if deviceDTO.TowerID != nil {
towerExists, err := u.deviceDetailsRepo.ValidateTowerExists(*deviceDTO.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", deviceDTO.TowerID.String())
}
}
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,
TowerID: deviceDTO.TowerID, // Add TowerID
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 deviceDTO.TowerID != nil {
// Validate tower exists
towerExists, err := u.deviceDetailsRepo.ValidateTowerExists(*deviceDTO.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", deviceDTO.TowerID.String())
}
updates["tower_id"] = *deviceDTO.TowerID
}
if deviceDTO.OLTID != nil {
// Validate OLT exists
oltExists, err := u.ValidateOLTExists(*deviceDTO.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", deviceDTO.OLTID.String())
}
updates["olt_id"] = *deviceDTO.OLTID
}
log.Printf("Updating device %s with fields: %+v", id, updates)
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

@ -2,10 +2,14 @@ package usecase
import (
"fmt"
"mime/multipart"
"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"
@ -13,59 +17,335 @@ import (
type DeviceUseCase interface {
CreateDevice(device req.DeviceDTO) error
GetAllDevices() ([]entity.Device, error)
GetAllDevices() ([]res.DeviceResponse, error)
GetByID(id uuid.UUID) (entity.Device, error)
GetByID(id uuid.UUID) (res.DeviceResponse, error)
UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) error
GetByType(deviceType string) ([]res.DeviceTypeResponse, error)
BulkUploadImages(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader) error
CreateDeviceWithMultipleImages(device req.DeviceDTO, imageFiles []*multipart.FileHeader) error
UpdateDeviceWithMultipleImages(id uuid.UUID, device req.UpdateDeviceDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error
BulkUploadImagesMultiple(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader, fileDistribution []int) error
ValidateTowerExists(towerID uuid.UUID) (bool, error)
ValidateOLTExists(oltID uuid.UUID) (bool, error)
}
type deviceUseCase struct {
deviceRepo repository.DevicesRepo
oltRepo repository.OLTRepo
validate *validator.Validate
geocoder service.GeocodingService
}
func NewDeviceUseCase(deviceRepo repository.DevicesRepo) DeviceUseCase {
func NewDeviceUseCase(deviceRepo repository.DevicesRepo,oltRepo repository.OLTRepo ,geocoder service.GeocodingService) DeviceUseCase {
return &deviceUseCase{
deviceRepo: deviceRepo,
oltRepo: oltRepo,
geocoder: geocoder,
validate: validator.New(),
}
}
func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
err := u.validate.Struct(device)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
func (u *deviceUseCase) ValidateOLTExists(oltID uuid.UUID) (bool, error) {
_, err := u.oltRepo.GetByID(oltID)
if err != nil {
return false, nil
}
return true, nil
}
func (u *deviceUseCase) ValidateTowerExists(towerID uuid.UUID) (bool, error) {
return u.deviceRepo.ValidateTowerExists(towerID)
}
func (u *deviceUseCase) CreateDeviceWithMultipleImages(device req.DeviceDTO, imageFiles []*multipart.FileHeader) error {
err := u.validate.Struct(device)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
if device.DeviceType == "OTB" || device.DeviceType == "ODP" {
if device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
}
if device.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*device.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", device.TowerID.String())
}
}
if device.OLTID != nil {
// Only ODP devices can be assigned to OLT
if device.DeviceType != "ODP" {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
oltExists, err := u.ValidateOLTExists(*device.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", device.OLTID.String())
}
}
var imageURLs []string
var primaryImageURL string
if len(imageFiles) > 0 {
// Save all images
imageURLs, err = helper.SaveDeviceImagesBulk(imageFiles)
if err != nil {
return err
}
// First image becomes primary
if len(imageURLs) > 0 && imageURLs[0] != "" {
primaryImageURL = imageURLs[0]
}
}
newDevice := entity.Device{
ID: uuid.New(),
ID: uuid.New(),
DeviceCode: device.DeviceCode,
DeviceType: entity.DeviceType(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: entity.DeviceStatus(device.Status),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Status: entity.DeviceStatus(device.Status),
Province: device.Province,
City: device.City,
District: device.District,
TowerID: device.TowerID, // Add TowerID field
OLTID: device.OLTID,
ImageURL: &primaryImageURL, // Primary image as pointer
ImageURLs: entity.StringSlice(imageURLs), // All images
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.deviceRepo.Post(newDevice)
return u.deviceRepo.Post(newDevice)
}
func (u *deviceUseCase) GetAllDevices() ([]entity.Device, error) {
func (u *deviceUseCase) UpdateDeviceWithMultipleImages(id uuid.UUID, device req.UpdateDeviceDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error {
err := u.validate.Struct(device)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
updates := map[string]interface{}{}
if device.DeviceCode != nil {
updates["DeviceCode"] = *device.DeviceCode
}
if device.DeviceType != nil {
updates["DeviceType"] = *device.DeviceType
}
if device.Longitude != nil {
updates["Longitude"] = *device.Longitude
}
if device.Latitude != nil {
updates["Latitude"] = *device.Latitude
}
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
}
// Handle TowerID validation and update
if device.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*device.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", device.TowerID.String())
}
updates["TowerID"] = *device.TowerID
}
// Handle OLTID validation and update
if device.OLTID != nil {
// Get current device to check type
currentDevice, err := u.deviceRepo.GetByID(id)
if err != nil {
return fmt.Errorf("device not found: %w", err)
}
// Only ODP devices can be assigned to OLT
deviceType := string(currentDevice.DeviceType)
if device.DeviceType != nil {
deviceType = *device.DeviceType
}
if deviceType != "ODP" {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
oltExists, err := u.ValidateOLTExists(*device.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", device.OLTID.String())
}
updates["OLTID"] = *device.OLTID
}
// Handle multiple image uploads
if len(imageFiles) > 0 {
// Get current device to handle existing images
currentDevice, err := u.deviceRepo.GetByID(id)
if err != nil {
return err
}
// Save new images
newImageURLs, err := helper.SaveDeviceImagesBulk(imageFiles)
if err != nil {
return err
}
var finalImageURLs []string
shouldReplace := len(replaceImages) > 0 && replaceImages[0]
if shouldReplace {
// Replace all images - delete old ones
for _, oldImageURL := range currentDevice.GetAllImageURLs() {
helper.DeleteDeviceImage(oldImageURL)
}
finalImageURLs = newImageURLs
} else {
// Append to existing images
existingImages := currentDevice.GetAllImageURLs()
finalImageURLs = append(existingImages, newImageURLs...)
}
// Update primary image (first image in the final list)
if len(finalImageURLs) > 0 && finalImageURLs[0] != "" {
updates["ImageURL"] = finalImageURLs[0]
}
// Update all images
updates["ImageURLs"] = entity.StringSlice(finalImageURLs)
}
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")
}
updates["UpdatedAt"] = time.Now()
return u.deviceRepo.Update(id, updates)
}
func (u *deviceUseCase) BulkUploadImagesMultiple(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader, fileDistribution []int) error {
if len(devices) == 0 {
return fmt.Errorf("no devices provided")
}
if len(fileDistribution) != len(devices) {
return fmt.Errorf("file distribution count must match device count")
}
// Validate all device IDs exist
for i, device := range devices {
_, err := u.deviceRepo.GetByID(device.DeviceID)
if err != nil {
return fmt.Errorf("device %d with ID %s not found", i, device.DeviceID)
}
}
// Save all images
imageURLs, err := helper.SaveDeviceImagesBulk(imageFiles)
if err != nil {
return fmt.Errorf("failed to save images: %w", err)
}
// Distribute images to devices
updates := make(map[uuid.UUID][]string)
fileIndex := 0
for i, device := range devices {
imageCount := fileDistribution[i]
deviceImages := make([]string, 0)
for j := 0; j < imageCount && fileIndex < len(imageURLs); j++ {
if imageURLs[fileIndex] != "" {
deviceImages = append(deviceImages, imageURLs[fileIndex])
}
fileIndex++
}
if len(deviceImages) > 0 {
updates[device.DeviceID] = deviceImages
}
}
// Update database
return u.deviceRepo.BulkUpdateImagesMultiple(updates)
}
// Keep old methods for backward compatibility
func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
return u.CreateDeviceWithMultipleImages(device, nil)
}
func (u *deviceUseCase) GetAllDevices() ([]res.DeviceResponse, error) {
devices, err := u.deviceRepo.GetAll()
if err != nil {
return devices, err
return []res.DeviceResponse{}, err
}
return devices, nil
devicesResponse,err := helper.ConvertToDeviceResponse(devices, u.geocoder)
if err != nil {
return []res.DeviceResponse{}, err
}
return devicesResponse, nil
}
func (u *deviceUseCase) GetByID(id uuid.UUID) (entity.Device, error) {
func (u *deviceUseCase) BulkUploadImages(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader) error {
// Default to one image per device
fileDistribution := make([]int, len(devices))
for i := range fileDistribution {
fileDistribution[i] = 1
}
return u.BulkUploadImagesMultiple(devices, imageFiles, fileDistribution)
}
func (u *deviceUseCase) GetByID(id uuid.UUID) (res.DeviceResponse, error) {
device, err := u.deviceRepo.GetByID(id)
if err != nil {
return device, err
return res.DeviceResponse{}, err
}
return device, nil
deviceResp, err := helper.ConvertToDeviceResponseId(device, u.geocoder)
if err != nil {
return res.DeviceResponse{}, err
}
return deviceResp, nil
}
func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) error {
@ -73,7 +353,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 {
@ -91,6 +370,62 @@ 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
}
// Handle TowerID validation and update
if device.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*device.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", device.TowerID.String())
}
updates["TowerID"] = *device.TowerID
}
// Handle OLTID validation and update
if device.OLTID != nil {
// Get current device to check type
currentDevice, err := u.deviceRepo.GetByID(id)
if err != nil {
return fmt.Errorf("device not found: %w", err)
}
// Only ODP devices can be assigned to OLT
deviceType := string(currentDevice.DeviceType)
if device.DeviceType != nil {
deviceType = *device.DeviceType
}
if deviceType != "ODP" {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
oltExists, err := u.ValidateOLTExists(*device.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", device.OLTID.String())
}
updates["OLTID"] = *device.OLTID
}
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")
@ -98,5 +433,16 @@ func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) e
updates["UpdatedAt"] = time.Now()
return u.deviceRepo.Update(id, updates)
}
func (u *deviceUseCase) GetByType(deviceType string) ([]res.DeviceTypeResponse, error) {
devices, err := u.deviceRepo.GetByType(deviceType)
if err != nil {
return nil, err
}
deviceTypeResponses := helper.ConvertToDeviceTypeResponse(devices)
return deviceTypeResponses, nil
}

View File

@ -2,95 +2,318 @@ package usecase
import (
"errors"
"fmt"
"strings"
"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 strings.ToLower(string(startDevice.DeviceType)) != "closure" {
return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType)
}
if strings.ToUpper(string(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 && startDevice.DeviceType == "ODP" {
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
}

163
usecase/nearest_device.go Normal file
View File

@ -0,0 +1,163 @@
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)
GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, error)
GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, 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) GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, 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 towers
}
towers, err := u.nearestDeviceRepo.GetNearestTowers(
request.Longitude,
request.Latitude,
radius,
limit,
request.Province,
request.City,
request.District,
)
if err != nil {
return nil, err
}
responses, err := helper.ConvertToNearestTowerResponses(towers, u.geocoder)
if err != nil {
return nil, err
}
return responses, nil
}
func (u *nearestDeviceUseCase) GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, error) {
tower, err := u.nearestDeviceRepo.GetTowerByIDWithConnections(id)
if err != nil {
return res.NearestTowerDetailResponse{}, err
}
// Calculate distance
distance := calculateDistance(userLat, userLng, tower.Latitude, tower.Longitude)
response, err := helper.ConvertToNearestTowerDetailResponse(tower, distance, u.nearestDeviceRepo, u.geocoder)
if err != nil {
return res.NearestTowerDetailResponse{}, err
}
return response, nil
}
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
}

234
usecase/olt_usecase.go Normal file
View File

@ -0,0 +1,234 @@
package usecase
import (
"errors"
"fmt"
"log"
"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 OLTUsecase interface {
CreateOLT(oltDTO req.OLTDTO) error
GetAllOLTs() ([]res.OLTResponse, error)
GetOLTByID(id uuid.UUID) (res.OLTDetailResponse, error)
UpdateOLT(id uuid.UUID, updateDTO req.UpdateOLTDTO) error
DeleteOLT(id uuid.UUID) error
AssignDeviceToOLT(oltID, deviceID uuid.UUID) error
UnassignDeviceFromOLT(deviceID uuid.UUID) error
GetDevicesByOLT(oltID uuid.UUID) ([]res.OLTDeviceResponse, error)
}
type oltUsecase struct {
oltRepo repository.OLTRepo
deviceRepo repository.DeviceDetailsRepo
validate *validator.Validate
geocoder service.GeocodingService
}
func NewOLTUsecase(oltRepo repository.OLTRepo, deviceRepo repository.DeviceDetailsRepo, geocoder service.GeocodingService) OLTUsecase {
return &oltUsecase{
oltRepo: oltRepo,
deviceRepo: deviceRepo,
validate: validator.New(),
geocoder: geocoder,
}
}
func (u *oltUsecase) CreateOLT(oltDTO req.OLTDTO) error {
if err := u.validate.Struct(oltDTO); err != nil {
return fmt.Errorf("validation error: %w", err)
}
// Check if OLT name already exists
existingOLT, err := u.oltRepo.GetByName(oltDTO.OLTName)
if err == nil && existingOLT.ID != uuid.Nil {
return errors.New("OLT name already exists")
}
olt := entity.OLT{
ID: uuid.New(),
OLTName: oltDTO.OLTName,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.oltRepo.Create(olt)
}
func (u *oltUsecase) GetAllOLTs() ([]res.OLTResponse, error) {
olts, err := u.oltRepo.GetAll()
if err != nil {
return nil, err
}
var responses []res.OLTResponse
for _, olt := range olts {
response := res.OLTResponse{
ID: olt.ID,
OLTName: olt.OLTName,
CreatedAt: olt.CreatedAt,
UpdatedAt: olt.UpdatedAt,
}
responses = append(responses, response)
}
return responses, nil
}
func (u *oltUsecase) GetOLTByID(id uuid.UUID) (res.OLTDetailResponse, error) {
// First, check if OLT exists using the basic GetByID method
olt, err := u.oltRepo.GetByID(id)
if err != nil {
log.Printf("OLT not found: %v", err)
return res.OLTDetailResponse{}, errors.New("OLT not found")
}
log.Printf("OLT found: %+v", olt)
// Try to get OLT with devices, but fallback to basic info if it fails
oltWithDevices, err := u.oltRepo.GetByIDWithDevices(id)
if err != nil {
log.Printf("Error getting OLT with devices, using basic OLT info: %v", err)
// Use the basic OLT info we already retrieved
oltWithDevices = olt
oltWithDevices.Devices = []entity.Device{} // Ensure devices is empty slice, not nil
}
log.Printf("OLT retrieved with devices: %+v", oltWithDevices)
// Convert devices to device details responses
var deviceResponses []res.DeviceDetailsResponse
for _, device := range oltWithDevices.Devices {
log.Printf("Processing device: %s", device.DeviceCode)
// Get the device details using the existing method which properly loads DevicePort
deviceDetails, err := u.deviceRepo.GetByID(device.ID)
if err != nil {
log.Printf("Error getting device details for %s: %v", device.DeviceCode, err)
continue
}
deviceResponse, err := helper.ConvertToDeviceDetailsResponse(deviceDetails, u.geocoder)
if err != nil {
log.Printf("Error converting device response for %s: %v", device.DeviceCode, err)
continue
}
deviceResponses = append(deviceResponses, deviceResponse)
}
// Initialize empty slice if nil to ensure JSON response shows empty array instead of null
if deviceResponses == nil {
deviceResponses = []res.DeviceDetailsResponse{}
}
response := res.OLTDetailResponse{
ID: oltWithDevices.ID,
OLTName: oltWithDevices.OLTName,
DeviceCount: len(deviceResponses),
Devices: deviceResponses,
CreatedAt: oltWithDevices.CreatedAt,
UpdatedAt: oltWithDevices.UpdatedAt,
}
return response, nil
}
func (u *oltUsecase) UpdateOLT(id uuid.UUID, updateDTO req.UpdateOLTDTO) error {
if err := u.validate.Struct(updateDTO); err != nil {
return fmt.Errorf("validation error: %w", err)
}
// Check if OLT exists
_, err := u.oltRepo.GetByID(id)
if err != nil {
return errors.New("OLT not found")
}
// Check if new name already exists (if name is being updated)
if updateDTO.OLTName != nil {
existingOLT, err := u.oltRepo.GetByName(*updateDTO.OLTName)
if err == nil && existingOLT.ID != id {
return errors.New("OLT name already exists")
}
}
return u.oltRepo.Update(id, updateDTO)
}
func (u *oltUsecase) DeleteOLT(id uuid.UUID) error {
// Check if OLT exists
_, err := u.oltRepo.GetByID(id)
if err != nil {
return errors.New("OLT not found")
}
return u.oltRepo.Delete(id)
}
func (u *oltUsecase) AssignDeviceToOLT(oltID, deviceID uuid.UUID) error {
return u.oltRepo.AssignDeviceToOLT(oltID, deviceID)
}
func (u *oltUsecase) UnassignDeviceFromOLT(deviceID uuid.UUID) error {
return u.oltRepo.UnassignDeviceFromOLT(deviceID)
}
func (u *oltUsecase) GetDevicesByOLT(oltID uuid.UUID) ([]res.OLTDeviceResponse, error) {
// Check if OLT exists
_, err := u.oltRepo.GetByID(oltID)
if err != nil {
return nil, errors.New("OLT not found")
}
devices, err := u.oltRepo.GetDevicesByOLTID(oltID)
if err != nil {
return nil, err
}
var responses []res.OLTDeviceResponse
for _, device := range devices {
// Get address using geocoder
address := ""
if u.geocoder != nil {
addr, err := u.geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude)
if err == nil {
address = addr
}
}
// Get port usage from device port repository
portUsed := 0
portUsageInfo, _, err := u.deviceRepo.GetPortUsageByDevice(device.ID)
if err == nil {
portUsed = portUsageInfo
}
response := res.OLTDeviceResponse{
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Address: address,
Province: device.Province,
City: device.City,
District: device.District,
Status: string(device.Status),
PortAmount: device.PortAmount,
PortUsed: portUsed,
ImageURL: device.ImageURL,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
responses = append(responses, response)
}
return responses, nil
}

View File

@ -2,76 +2,100 @@ package usecase
import (
"errors"
"fmt"
"mime/multipart"
"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 TowerUseCase interface {
Post(tower req.TowerDTO) error
GetAll() ([]entity.Tower, error)
GetByID(id uuid.UUID) (entity.Tower, error)
UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) 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, imageFile *multipart.FileHeader) error
BulkUploadImages(towers []req.BulkTowerImageUploadDTO, imageFiles []*multipart.FileHeader) error
PostWithMultipleImages(tower req.TowerDTO, imageFiles []*multipart.FileHeader) error
UpdateTowerWithMultipleImages(id uuid.UUID, tower req.UpdateTowerDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error
}
type towerUsecase struct {
towerRepo repository.TowerRepo
validate *validator.Validate
geocoder service.GeocodingService
}
func NewTowerUseCase(towerRepo repository.TowerRepo) TowerUseCase {
func NewTowerUseCase(towerRepo repository.TowerRepo, geocoder service.GeocodingService) TowerUseCase {
return &towerUsecase{
towerRepo: towerRepo,
geocoder: geocoder,
validate: validator.New(),
}
}
func (u *towerUsecase) Post(tower req.TowerDTO) 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(),
}
return u.towerRepo.Post(newTower)
}
func (u *towerUsecase) GetAll() ([]entity.Tower, error) {
towers, err := u.towerRepo.GetAll()
if err != nil {
return towers, err
}
return towers, nil
}
func (u *towerUsecase) GetByID(id uuid.UUID) (entity.Tower, error) {
tower, err := u.towerRepo.GetByID(id)
if err != nil {
return tower, err
}
return tower, nil
}
func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error {
err := u.validate.Struct(tower)
func (u *towerUsecase) PostWithMultipleImages(tower req.TowerDTO, imageFiles []*multipart.FileHeader) error {
err := u.validate.Struct(tower)
if err != nil {
return err // Return validation error
return err
}
updates := make(map[string]interface{})
// 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 imageURLs []string
var primaryImageURL string
if len(imageFiles) > 0 {
// Save all images
imageURLs, err = helper.SaveTowerImagesBulk(imageFiles)
if err != nil {
return err
}
// First image becomes primary
if len(imageURLs) > 0 && imageURLs[0] != "" {
primaryImageURL = imageURLs[0]
}
}
newTower := entity.Tower{
ID: uuid.New(),
DeviceID: tower.DeviceID,
TowerCode: tower.TowerCode,
Longitude: tower.Longitude,
Latitude: tower.Latitude,
ImageURL: primaryImageURL, // Primary image
ImageURLs: entity.StringSlice(imageURLs), // All images
ExternalTower: tower.ExternalTower,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.towerRepo.Post(newTower)
}
func (u *towerUsecase) UpdateTowerWithMultipleImages(id uuid.UUID, tower req.UpdateTowerDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error {
err := u.validate.Struct(tower)
if err != nil {
return err
}
updates := make(map[string]interface{})
if tower.DeviceID != nil {
updates["DeviceID"] = *tower.DeviceID
@ -85,6 +109,50 @@ 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
}
if tower.ImageURL != nil {
updates["ImageURL"] = *tower.ImageURL
}
// Handle multiple image uploads
if len(imageFiles) > 0 {
// Get current tower to handle existing images
currentTower, err := u.towerRepo.GetByID(id)
if err != nil {
return err
}
// Save new images
newImageURLs, err := helper.SaveTowerImagesBulk(imageFiles)
if err != nil {
return err
}
var finalImageURLs []string
shouldReplace := len(replaceImages) > 0 && replaceImages[0]
if shouldReplace {
// Replace all images - delete old ones
for _, oldImageURL := range currentTower.GetAllImageURLs() {
helper.DeleteTowerImage(oldImageURL)
}
finalImageURLs = newImageURLs
} else {
// Append to existing images
existingImages := currentTower.GetAllImageURLs()
finalImageURLs = append(existingImages, newImageURLs...)
}
// Update primary image (first image in the final list)
if len(finalImageURLs) > 0 && finalImageURLs[0] != "" {
updates["ImageURL"] = finalImageURLs[0]
}
// Update all images
updates["ImageURLs"] = entity.StringSlice(finalImageURLs)
}
// If no fields are updated, return an error
if len(updates) == 0 {
@ -94,4 +162,183 @@ func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO) error
updates["UpdatedAt"] = time.Now()
return u.towerRepo.Update(id, updates)
}
}
func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error {
err := u.validate.Struct(tower)
if err != nil {
return err
}
// 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")
}
// 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) {
towers, err := u.towerRepo.GetAll()
if err != nil {
return nil, err
}
towerResp, err := helper.ConvertToTowerResponses(towers,u.geocoder)
if err != nil {
return nil, err
}
return towerResp, nil
}
func (u *towerUsecase) GetByID(id uuid.UUID) (res.TowerResponse, error) {
tower, err := u.towerRepo.GetByID(id)
if err != nil {
return res.TowerResponse{}, err
}
towerResp, err := helper.ConvertToTowerIDResponses(tower,u.geocoder)
if err != nil {
return res.TowerResponse{}, err
}
return towerResp, nil
}
func (u *towerUsecase) BulkUploadImages(towers []req.BulkTowerImageUploadDTO, imageFiles []*multipart.FileHeader) error {
// Validate input
if len(towers) != len(imageFiles) {
return fmt.Errorf("number of towers (%d) must match number of image files (%d)", len(towers), len(imageFiles))
}
if len(towers) == 0 {
return fmt.Errorf("no towers provided")
}
// Validate all tower IDs exist
for i, tower := range towers {
_, err := u.towerRepo.GetByID(tower.TowerID)
if err != nil {
return fmt.Errorf("tower %d with ID %s not found", i, tower.TowerID)
}
}
// Save all images
imageURLs, err := helper.SaveTowerImagesBulk(imageFiles)
if err != nil {
return fmt.Errorf("failed to save images: %w", err)
}
// Create update map
updates := make(map[uuid.UUID]string)
for i, tower := range towers {
if i < len(imageURLs) && imageURLs[i] != "" {
updates[tower.TowerID] = imageURLs[i]
}
}
// Update database
return u.towerRepo.BulkUpdateImages(updates)
}
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
}
updates := make(map[string]interface{})
if tower.DeviceID != nil {
updates["DeviceID"] = *tower.DeviceID
}
if tower.TowerCode != nil {
updates["TowerCode"] = *tower.TowerCode
}
// IMPORTANT: Ignore longitude/latitude updates if tower is assigned to a device
currentTower, err := u.towerRepo.GetByID(id)
if err != nil {
return err
}
// If tower has a device assigned, ignore location updates
if currentTower.DeviceID == nil {
// Only allow location updates for external towers (not assigned to devices)
if tower.Longitude != nil {
updates["Longitude"] = *tower.Longitude
}
if tower.Latitude != nil {
updates["Latitude"] = *tower.Latitude
}
}
// If DeviceID is being assigned, sync location with device
if tower.DeviceID != nil && currentTower.DeviceID == nil {
// Get device location using repository method
longitude, latitude, err := u.towerRepo.GetDeviceLocation(*tower.DeviceID)
if err != nil {
return fmt.Errorf("failed to get device location: %w", err)
}
updates["Longitude"] = longitude
updates["Latitude"] = latitude
}
if tower.ExternalTower != nil {
updates["ExternalTower"] = *tower.ExternalTower
}
// Handle image upload
if imageFile != nil {
// Get current tower to delete old image
if 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 {
return errors.New("no fields to update")
}
updates["UpdatedAt"] = time.Now()
return u.towerRepo.Update(id, updates)
}

View File

@ -1,30 +1,213 @@
package usecase
import (
"errors"
"time"
"users_management/m/model/dto"
"users_management/m/model/entity"
"users_management/m/repository"
"users_management/m/utils"
"users_management/m/utils/validation"
"github.com/go-playground/validator/v10"
"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)
CreateUserFromExternal(nomorInduk, name, roleName string) error
UpdateUserRole(nomorInduk, roleName string) error
GetAllUsers() ([]entity.User, error)
RegisterUser(registerDTO dto.UserRegisterDTO) error
GetPendingUsers() ([]dto.PendingUserResponse, error)
ApproveUser(userID uuid.UUID) error
RejectUser(userID uuid.UUID) error
GetUserByUsernameWithStatus(username string) (entity.User, error)
CreateSuperAdminBypass(user entity.User) error
UpdateUserRoleByID(userID uuid.UUID, newRoleName string) error
UpdateUserRoleByUsername(username, newRoleName string) error
GetUserByID(userID uuid.UUID) (entity.User, error)
GetUserByName(name string) (entity.User, error)
}
type usersUsecase struct {
userRepo repository.UsersRepo
userRepo repository.UsersRepo
validate *validator.Validate
}
func NewUsersUsecase(userRepo repository.UsersRepo) UsersUsecase {
return &usersUsecase{
userRepo: userRepo,
}
validate := validator.New()
validation.RegisterCustomValidators(validate)
return &usersUsecase{
userRepo: userRepo,
validate: validate,
}
}
func (u *usersUsecase) GetUserByName(name string) (entity.User, error) {
return u.userRepo.GetUserByName(name)
}
func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, error) {
func (u *usersUsecase) CreateSuperAdminBypass(user entity.User) error {
return u.userRepo.CreateSuperAdminBypass(user)
}
// Add these implementations to the usersUsecase struct
func (u *usersUsecase) UpdateUserRoleByID(userID uuid.UUID, newRoleName string) error {
// Get the new role ID
newRoleID, err := u.GetRoleByDepartment(newRoleName)
if err != nil {
return errors.New("invalid role name: " + err.Error())
}
// Check if user exists
_, err = u.userRepo.GetUserByID(userID)
if err != nil {
return errors.New("user not found")
}
// Update user role
return u.userRepo.UpdateUserRoleByID(userID, newRoleID)
}
func (u *usersUsecase) UpdateUserRoleByUsername(username, newRoleName string) error {
// Get the new role ID
newRoleID, err := u.GetRoleByDepartment(newRoleName)
if err != nil {
return errors.New("invalid role name: " + err.Error())
}
// Check if user exists
_, err = u.userRepo.GetUserByUsernameWithStatus(username)
if err != nil {
return errors.New("user not found")
}
// Update user role by username
return u.userRepo.UpdateUserRoleByUsername(username, newRoleID)
}
func (u *usersUsecase) GetUserByID(userID uuid.UUID) (entity.User, error) {
return u.userRepo.GetUserByID(userID)
}
func (u *usersUsecase) RegisterUser(registerDTO dto.UserRegisterDTO) error {
// Validate input
if err := u.validate.Struct(registerDTO); err != nil {
return err
}
// Check if username already exists
existingUser, err := u.userRepo.GetUserByUsernameWithStatus(registerDTO.Username)
if err == nil && existingUser.ID != uuid.Nil {
return errors.New("username already exists")
}
// Check if nomor_induk already exists
if registerDTO.NomorInduk != "" {
existingUser, err := u.userRepo.GetUserByNomorInduk(registerDTO.NomorInduk)
if err == nil && existingUser.ID != uuid.Nil {
return errors.New("nomor induk already exists")
}
}
existingUserByName, err := u.userRepo.GetUserByName(registerDTO.Name)
if err == nil && existingUserByName.ID != uuid.Nil {
return errors.New("name already exists, please use a different name")
}
// Hash password
hashedPassword, err := utils.HashPassword(registerDTO.Password)
if err != nil {
return err
}
teknisiRoleID, err := u.GetRoleByDepartment("Teknisi")
if err != nil {
return errors.New("failed to get Teknisi role: " + err.Error())
}
// Create user with pending status
user := entity.User{
ID: uuid.New(),
Name: registerDTO.Name,
Username: registerDTO.Username,
Password: hashedPassword,
NomorInduk: &registerDTO.NomorInduk,
RoleID: teknisiRoleID,
Status: entity.UserStatusPending, // Set status to pending
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.userRepo.RegisterUser(user)
}
func (u *usersUsecase) GetPendingUsers() ([]dto.PendingUserResponse, error) {
users, err := u.userRepo.GetPendingUsers()
if err != nil {
return nil, err
}
var responses []dto.PendingUserResponse
for _, user := range users {
response := dto.PendingUserResponse{
ID: user.ID,
Name: user.Name,
Username: user.Username,
NomorInduk: user.NomorInduk,
RoleName: user.Role.Name,
Status: dto.UserStatus(user.Status),
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
responses = append(responses, response)
}
return responses, nil
}
func (u *usersUsecase) ApproveUser(userID uuid.UUID) error {
// Check if user exists and is pending
user, err := u.userRepo.GetUserByID(userID)
if err != nil {
return errors.New("user not found")
}
if user.Status != entity.UserStatusPending {
return errors.New("user is not in pending status")
}
return u.userRepo.UpdateUserStatus(userID, entity.UserStatusApproved)
}
func (u *usersUsecase) RejectUser(userID uuid.UUID) error {
// Check if user exists and is pending
user, err := u.userRepo.GetUserByID(userID)
if err != nil {
return errors.New("user not found")
}
if user.Status != entity.UserStatusPending {
return errors.New("user is not in pending status")
}
return u.userRepo.UpdateUserStatus(userID, entity.UserStatusRejected)
}
func (u *usersUsecase) GetUserByUsernameWithStatus(username string) (entity.User, error) {
return u.userRepo.GetUserByUsernameWithStatus(username)
}
func (u *usersUsecase) GetRoleByDepartment(departmentName string) (uuid.UUID, error) {
role, err := u.userRepo.GetRoleByDepartment(departmentName)
if err != nil {
return uuid.Nil, err
@ -33,10 +216,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

@ -0,0 +1,49 @@
package helper
import (
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"github.com/google/uuid"
)
func ConvertToBackboneResponses(backbone []entity.Backbone, totalFishbone map[uuid.UUID]int) ([]res.BackboneResponse, error) {
var responses []res.BackboneResponse
for _, backbone := range backbone {
count := 0
if fishboneCount, exist := totalFishbone[backbone.ID]; exist {
count = fishboneCount
}
backboneResp := res.BackboneResponse {
ID: backbone.ID,
BackboneCode: backbone.BackboneCode,
DevStart: backbone.DeviceStart.DeviceCode,
DevEnd: backbone.DeviceEnd.DeviceCode,
DeviceStartID: backbone.DeviceStart.ID,
DeviceEndID: backbone.DeviceEnd.ID,
CoreAmount: backbone.CoreAmount,
TotalFishbone: count,
CreatedAt: backbone.CreatedAt,
UpdatedAt: backbone.UpdatedAt,
}
responses = append(responses, backboneResp)
}
return responses, nil
}
func ConvertToBackboneRespId(backbone entity.Backbone, fishboneCount int) (res.BackboneResponse, error) {
backboneResp := res.BackboneResponse {
ID: backbone.ID,
DevStart: backbone.DeviceStart.DeviceCode,
DevEnd: backbone.DeviceEnd.DeviceCode,
DeviceStartID: backbone.DeviceStart.ID,
DeviceEndID: backbone.DeviceEnd.ID,
CoreAmount: backbone.CoreAmount,
TotalFishbone: fishboneCount,
CreatedAt: backbone.CreatedAt,
UpdatedAt: backbone.UpdatedAt,
}
return backboneResp, nil
}

View File

@ -0,0 +1,108 @@
package helper
import (
"log"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
"users_management/m/utils/service"
)
func ConvertToDeviceTypeResponse(devices []entity.Device) []res.DeviceTypeResponse {
var responses []res.DeviceTypeResponse
for _, devices := range devices {
deviceResp := res.DeviceTypeResponse{
ID: devices.ID,
DeviceType: string(devices.DeviceType),
DeviceCode: devices.DeviceCode,
}
responses = append(responses, deviceResp)
}
return responses
}
func ConvertToDeviceResponse(devices []entity.Device, geocoder service.GeocodingService) ([]res.DeviceResponse, error) {
var responses []res.DeviceResponse
for _, device := range devices {
address := "Address not found"
if geocoder != nil {
if addr, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude); err == nil {
address = addr
}
}
// Get all image URLs using the entity method
allImageURLs := device.GetAllImageURLs()
// Handle empty slice vs nil for JSON response
if len(allImageURLs) == 0 {
allImageURLs = []string{} // Return empty array instead of nil
}
response := res.DeviceResponse{
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: string(device.Status),
Address: address,
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL, // Primary image (can be null)
ImageURLs: allImageURLs, // All images (empty array if none)
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
responses = append(responses, response)
}
return responses, nil
}
func ConvertToDeviceResponseId(device entity.Device, geocoder service.GeocodingService) (res.DeviceResponse, error) {
address := "Address not found"
if geocoder != nil {
if addr, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude); err == nil {
address = addr
} else {
log.Printf("Geocoding error for device %s: %v", device.DeviceCode, err)
}
} else {
log.Println("WARNING: Geocoder is nil")
}
// Get all image URLs using the entity method
allImageURLs := device.GetAllImageURLs()
// Handle empty slice vs nil for JSON response
if len(allImageURLs) == 0 {
allImageURLs = []string{} // Return empty array instead of nil
}
deviceResp := res.DeviceResponse{
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
Address: address,
Province: device.Province,
City: device.City,
District: device.District,
PortAmount: device.PortAmount,
Status: string(device.Status),
ImageURL: device.ImageURL, // Primary image (can be null)
ImageURLs: allImageURLs, // All images (empty array if none)
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
return deviceResp, nil
}

View File

@ -0,0 +1,242 @@
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)
}
// COMBINE ALL TOWERS INTO A SINGLE ARRAY
towerInfos := make([]res.TowerConnectionDetail, 0)
// First, add the assigned tower (tower referenced by device.TowerID)
if device.TowerID != nil && device.Tower != nil {
distance := calculateDistance(device.Latitude, device.Longitude, device.Tower.Latitude, device.Tower.Longitude)
var externalTower *bool
if device.Tower.ExternalTower != nil {
externalTower = device.Tower.ExternalTower
}
// Get all tower image URLs
allTowerImageURLs := device.Tower.GetAllImageURLs()
// Primary image URL
var imageURL *string
if device.Tower.ImageURL != "" {
imageURL = &device.Tower.ImageURL
}
assignedTowerInfo := res.TowerConnectionDetail{
ID: device.Tower.ID,
TowerCode: device.Tower.TowerCode,
Distance: distance,
ExternalTower: externalTower,
ImageURL: imageURL,
ImageURLs: allTowerImageURLs,
}
towerInfos = append(towerInfos, assignedTowerInfo)
}
// Then, add towers that reference this device via dev_id (but avoid duplicates)
for _, tower := range device.Towers {
// Skip if this tower is already added as assigned tower
if device.TowerID != nil && tower.ID == *device.TowerID {
continue
}
distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude)
var externalTower *bool
if tower.ExternalTower != nil {
externalTower = tower.ExternalTower
}
// Get all tower image URLs
allTowerImageURLs := tower.GetAllImageURLs()
// Primary image URL
var imageURL *string
if tower.ImageURL != "" {
imageURL = &tower.ImageURL
}
info := res.TowerConnectionDetail{
ID: tower.ID,
TowerCode: tower.TowerCode,
Distance: distance,
ExternalTower: externalTower,
ImageURL: imageURL,
ImageURLs: allTowerImageURLs,
}
towerInfos = append(towerInfos, info)
}
// Convert port assignments - only for ODP devices
var finalPortAssignments []res.PortAssignmentResponse
var customerNames []string
if device.DeviceType == "ODP" {
// Only ODP devices have port assignments and customers
entityPortAssignments := device.DevicePort.GetPortAssignmentsWithDetails()
// Ensure we show all ports up to PortAmount
portAssignmentMap := make(map[int]res.PortAssignmentResponse)
for _, pa := range entityPortAssignments {
portAssignmentMap[pa.PortNumber] = res.PortAssignmentResponse{
PortNumber: pa.PortNumber,
CustomerName: pa.CustomerName,
IsOccupied: pa.IsOccupied,
Status: pa.Status,
Bandwidth: pa.Bandwidth,
}
}
// Fill in missing ports
finalPortAssignments = make([]res.PortAssignmentResponse, 0)
for i := 1; i <= device.PortAmount; i++ {
if assignment, exists := portAssignmentMap[i]; exists {
finalPortAssignments = append(finalPortAssignments, assignment)
} else {
finalPortAssignments = append(finalPortAssignments, res.PortAssignmentResponse{
PortNumber: i,
CustomerName: nil,
IsOccupied: false,
Status: entity.PortStatusOff,
Bandwidth: nil,
})
}
}
// Get customer names for ODP devices
customerNames = device.DevicePort.GetCustomerNamesWithPorts()
} else {
// OTB and other device types don't have customers or detailed port assignments
finalPortAssignments = []res.PortAssignmentResponse{}
customerNames = []string{}
}
// Get all image URLs
allImageURLs := device.GetAllImageURLs()
// Handle empty slice vs nil for JSON response
if len(allImageURLs) == 0 {
allImageURLs = []string{} // Return empty array instead of nil
}
// Handle OLT information - create nested object structure
var oltInfo *res.OLTInfo
if device.OLTID != nil && device.OLT != nil && device.OLT.OLTName != "" {
oltInfo = &res.OLTInfo{
OLTID: *device.OLTID,
OLTName: device.OLT.OLTName,
}
}
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, // Empty for OTB
PortAssignments: finalPortAssignments, // Empty for OTB
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
ImageURLs: allImageURLs,
TowerID: device.TowerID, // Keep TowerID for reference
// Remove AssignedTower field - now everything is in Towers
OLT: oltInfo, // Add OLT name
Backbones: backboneInfos,
Fishbones: fishboneInfos,
Towers: towerInfos, // All towers combined here
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
return response, nil
}

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,87 @@
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,
DeviceStartID: fishbone.DeviceStart.ID,
DeviceEndID: fishbone.DeviceEnd.ID,
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,
DeviceStartID: fishbone.DeviceStart.ID,
DeviceEndID: fishbone.DeviceEnd.ID,
BackboneCode: fishbone.Backbone.BackboneCode,
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,
DeviceStartID: fishbone.DeviceStart.ID,
DeviceEndID: fishbone.DeviceEnd.ID,
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,226 @@
package helper
import (
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
)
const (
MaxFileSize = 5 << 20 // 5MB
TowerUploadDir = "./uploads/towers"
DeviceUploadDir = "./uploads/devices"
)
func SaveDeviceImagesBulk(files []*multipart.FileHeader) ([]string, error) {
var imageURLs []string
for i, file := range files {
if file == nil {
imageURLs = append(imageURLs, "")
continue
}
// Validate file
if err := validateImageFile(file); err != nil {
return nil, fmt.Errorf("file %d: %w", i, err)
}
// Create upload directory if it doesn't exist
if err := os.MkdirAll(DeviceUploadDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create upload directory: %v", err)
}
// Generate unique filename
ext := strings.ToLower(filepath.Ext(file.Filename))
filename := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
filePath := filepath.Join(DeviceUploadDir, filename)
// Save file
if err := saveFile(file, filePath); err != nil {
return nil, fmt.Errorf("failed to save file %d: %w", i, err)
}
imageURL := fmt.Sprintf("/uploads/devices/%s", filename)
imageURLs = append(imageURLs, imageURL)
}
return imageURLs, nil
}
func SaveTowerImagesBulk(files []*multipart.FileHeader) ([]string, error) {
var imageURLs []string
for i, file := range files {
if file == nil {
imageURLs = append(imageURLs, "")
continue
}
// Validate file
if err := validateImageFile(file); err != nil {
return nil, fmt.Errorf("file %d: %w", i, err)
}
// Create upload directory if it doesn't exist
if err := os.MkdirAll(TowerUploadDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create upload directory: %v", err)
}
// Generate unique filename
ext := strings.ToLower(filepath.Ext(file.Filename))
filename := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
filePath := filepath.Join(TowerUploadDir, filename)
// Save file
if err := saveFile(file, filePath); err != nil {
return nil, fmt.Errorf("failed to save file %d: %w", i, err)
}
imageURL := fmt.Sprintf("/uploads/towers/%s", filename)
imageURLs = append(imageURLs, imageURL)
}
return imageURLs, nil
}
// Helper functions
func validateImageFile(file *multipart.FileHeader) 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")
}
return nil
}
func DeleteDeviceImage(imageURL string) error {
if imageURL == "" {
return nil
}
// Extract filename from URL
filename := filepath.Base(imageURL)
filePath := filepath.Join(DeviceUploadDir, 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)
}
func saveFile(file *multipart.FileHeader, filePath string) error {
// 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 nil
}
// 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(TowerUploadDir, 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(TowerUploadDir, 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(TowerUploadDir, 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)
}

Some files were not shown because too many files have changed in this diff Show More