Merge branch 'dev' of https://git.winteraccess.id/areeqakbr/backend_nam into dev
This commit is contained in:
commit
689505fa6a
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 = ®ion
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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(®isterDTO); 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",
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -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
4
go.sum
|
|
@ -10,8 +10,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: ®isterDTO.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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue