Merge pull request 'adding multiple images create and update also for the responses' (#16) from feature/responses-v2 into dev

Reviewed-on: winter-access/backend_nam#16
This commit is contained in:
areeqakbr 2025-06-15 16:01:58 +00:00
commit 45dc3e80da
24 changed files with 1439 additions and 162 deletions

View File

@ -5,7 +5,10 @@ import (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"strings"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -235,13 +238,97 @@ func (c *DeviceDetailsController) updateDeviceDetails(ctx *gin.Context) {
return
}
var request req.UpdateDeviceDetailsDTO
if err := ctx.ShouldBindJSON(&request); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
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
}
err = c.deviceDetailsUC.UpdateDeviceDetails(deviceID, request)
// Handle multipart form request
err = ctx.Request.ParseMultipartForm(50 << 20) // 50MB for multiple images
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Failed to parse multipart form")
return
}
// Create update DTO from form data
deviceUpdateDTO := req.UpdateDeviceDetailsDTO{}
if deviceCode := ctx.PostForm("device_code"); deviceCode != "" {
deviceUpdateDTO.DeviceCode = &deviceCode
}
if deviceType := ctx.PostForm("device_type"); deviceType != "" {
deviceUpdateDTO.DeviceType = &deviceType
}
if longitudeStr := ctx.PostForm("longitude"); longitudeStr != "" {
if longitude, err := strconv.ParseFloat(longitudeStr, 64); err == nil {
deviceUpdateDTO.Longitude = &longitude
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude format")
return
}
}
if latitudeStr := ctx.PostForm("latitude"); latitudeStr != "" {
if latitude, err := strconv.ParseFloat(latitudeStr, 64); err == nil {
deviceUpdateDTO.Latitude = &latitude
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude format")
return
}
}
if portAmountStr := ctx.PostForm("port_amount"); portAmountStr != "" {
if portAmount, err := strconv.Atoi(portAmountStr); err == nil {
deviceUpdateDTO.PortAmount = &portAmount
} else {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid port amount format")
return
}
}
if status := ctx.PostForm("status"); status != "" {
deviceUpdateDTO.Status = &status
}
if region := ctx.PostForm("region"); region != "" {
deviceUpdateDTO.Region = &region
}
if province := ctx.PostForm("province"); province != "" {
deviceUpdateDTO.Province = &province
}
if city := ctx.PostForm("city"); city != "" {
deviceUpdateDTO.City = &city
}
if district := ctx.PostForm("district"); district != "" {
deviceUpdateDTO.District = &district
}
// 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

View File

@ -1,7 +1,12 @@
package controller
import (
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"strconv"
"strings"
"users_management/m/middleware"
"users_management/m/model/dto/req"
"users_management/m/usecase"
@ -24,6 +29,8 @@ func (dc *DeviceController) Route() {
rg.GET("", dc.GetAllDevices())
rg.GET("/:uuid", dc.GetDeviceByID())
rg.PUT("/:uuid", dc.UpdateDevice())
rg.POST("/bulk-upload-images", dc.BulkUploadImages())
}
}
@ -34,27 +41,202 @@ func NewDeviceController(du usecase.DeviceUseCase, rg *gin.RouterGroup) *DeviceC
}
}
func (dc *DeviceController) CreateDevice() gin.HandlerFunc {
return func(c *gin.Context) {
var deviceDTO req.DeviceDTO
err := c.ShouldBindJSON(&deviceDTO)
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
}
common.SingleResponses(c, "Device has been created", nil)
}
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) {
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
}
err = dc.du.CreateDevice(deviceDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Device has been created", nil)
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
}
// 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
}
}
// Create DTO
deviceDTO := req.DeviceDTO{
DeviceCode: deviceCode,
DeviceType: deviceType,
Longitude: longitude,
Latitude: latitude,
PortAmount: portAmount,
Status: status,
}
// 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) {
@ -99,29 +281,108 @@ 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, "Invalid UUID")
return
}
return func(c *gin.Context) {
id := c.Param("uuid")
uuid, err := uuid.Parse(id)
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, "Invalid request")
return
}
err = dc.du.UpdateDevice(uuid, deviceDTO)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Device not found")
return
}
err = dc.du.UpdateDevice(uuid, deviceDTO)
common.SingleResponses(c, "Device has been updated", nil)
return
}
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, "Device not found")
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
}
// 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(uuid, deviceUpdateDTO, imageFiles, replaceImages)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
}
common.SingleResponses(c, "Device has been updated", nil)
}
}

View File

@ -1,6 +1,9 @@
package controller
import (
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"strconv"
"strings"
@ -27,6 +30,8 @@ func (tc *TowerController) Route() {
rg.GET("/:uuid", tc.GetTowerByID())
rg.PUT("/:uuid", tc.UpdateTower())
rg.POST("/bulk-upload-images", tc.BulkUploadImages())
}
}
@ -37,6 +42,56 @@ func NewTowerController(tu usecase.TowerUseCase, rg *gin.RouterGroup) *TowerCont
}
}
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()
@ -55,7 +110,7 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
contentType := c.GetHeader("Content-Type")
// Handle JSON request
// Handle JSON request (no images)
if strings.Contains(contentType, "application/json") {
var towerDTO req.TowerDTO
if err := c.ShouldBindJSON(&towerDTO); err != nil {
@ -63,7 +118,7 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
return
}
// No image file for JSON requests
// No image files for JSON requests
err := tc.tu.Post(towerDTO, nil)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
@ -73,15 +128,14 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
common.SingleResponses(c, "Tower has been created", nil)
return
}
// Parse multipart form
err := c.Request.ParseMultipartForm(10 << 20) // 10MB max
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
}
// Extract form data
deviceIDStr := c.PostForm("dev_id")
towerCode := c.PostForm("tower_code")
@ -137,10 +191,18 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
ExternalTower: externalTower,
}
// Get image file (optional)
imageFile, _ := c.FormFile("image")
// 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.Post(towerDTO, imageFile)
err = tc.tu.PostWithMultipleImages(towerDTO, imageFiles)
if err != nil {
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return
@ -181,7 +243,7 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
contentType := c.GetHeader("Content-Type")
// Handle JSON request
// Handle JSON request (no images)
if strings.Contains(contentType, "application/json") {
var towerUpdateDTO req.UpdateTowerDTO
if err := c.ShouldBindJSON(&towerUpdateDTO); err != nil {
@ -189,8 +251,8 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
return
}
// No image file for JSON requests
err := tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, nil)
// 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
@ -200,8 +262,8 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
return
}
// Handle multipart form request (existing logic)
err = c.Request.ParseMultipartForm(10 << 20) // 10MB max
// 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
@ -257,10 +319,21 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
towerUpdateDTO.ImageURL = &imageURL
}
// Get image file (optional)
imageFile, _ := c.FormFile("image")
// 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}
}
}
err = tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, imageFile)
// 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

View File

@ -50,6 +50,7 @@ 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.ucManager.NewUserUsecase()))
controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route()
rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase()))
{
controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route()
@ -57,7 +58,6 @@ func (s *Server) setupController() {
controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), rg).Route()
controller.NewTowerController(s.ucManager.NewTowerUsecase(), rg).Route()
controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), rg).Route()
controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route()
controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), rg).Route()
controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), rg).Route()
controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), rg).Route()

View File

@ -25,6 +25,7 @@ type UpdateDeviceDetailsDTO struct {
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
}
type AssignMultipleCustomersDTO struct {

View File

@ -1,25 +1,35 @@
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"`
Latitude float64 `json:"latitude" validate:"required"`
PortAmount int `json:"port_amount"`
Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
DeviceCode string `json:"device_code" validate:"required"`
DeviceType string `json:"device_type" validate:"required"`
Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"`
PortAmount int `json:"port_amount"`
Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
}
type UpdateDeviceDTO struct {
DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"`
DeviceType *string `json:"device_type,omitempty" validate:"omitempty,min=3"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty"`
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"`
DeviceCode *string `json:"device_code,omitempty" validate:"omitempty,min=3"`
DeviceType *string `json:"device_type,omitempty" validate:"omitempty,min=3"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"`
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
}
type BulkDeviceImageUploadDTO struct {
DeviceID uuid.UUID `json:"device_id" validate:"required"`
}
type BulkDeviceImagesDTO struct {
Devices []BulkDeviceImageUploadDTO `json:"devices" validate:"required,min=1"`
}

View File

@ -19,4 +19,13 @@ type UpdateTowerDTO struct {
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
ImageURL *string `json:"image_url,omitempty"`
}
// Add to model/dto/req/tower_dto.go
type BulkTowerImageUploadDTO struct {
TowerID uuid.UUID `json:"tower_id" validate:"required"`
}
type BulkTowerImagesDTO struct {
Towers []BulkTowerImageUploadDTO `json:"towers" validate:"required,min=1"`
}

View File

@ -23,6 +23,7 @@ type DeviceDetailsResponse struct {
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
ImageURL *string `json:"image_url,omitempty"`
ImageURLs []string `json:"image_urls"`
// Connection details
Backbones []BackboneConnectionInfo `json:"backbones"`
@ -39,6 +40,7 @@ type TowerConnectionDetail struct {
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 {

View File

@ -12,13 +12,15 @@ type DeviceResponse struct {
DeviceType string `json:"device_type"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
PortAmount int `json:"port_amount"`
Status string `json:"status"` // Always include, even if null
CustomerNames []string `json:"customer_names"` // Always include, even if empty
Province *string `json:"province"` // Always include, even if null
City *string `json:"city"` // Always include, even if null
District *string `json:"district"` // Always include, even if null
Status string `json:"status"`
Address string `json:"address"`
Region *string `json:"region"`
Province *string `json:"province"`
City *string `json:"city"`
District *string `json:"district"`
ImageURL *string `json:"image_url"` // Primary image
ImageURLs []string `json:"image_urls"` // All images
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -13,6 +13,7 @@ type TowerResponse struct {
Latitude float64 `json:"latitude"`
Address string `json:"address"`
ImageURL string `json:"image_url"`
ImageURLs []string `json:"image_urls"` // Store multiple images as JSONB
ExternalTower *bool `json:"external_tower"` // Make nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

View File

@ -18,6 +18,7 @@ type DeviceDetails struct {
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"`
AdditionalImages StringSlice `gorm:"type:text[]"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@ -48,4 +49,5 @@ func (d *DeviceDetails) GetAllFishbones() []Fishbone {
allFishbones = append(allFishbones, d.FishbonesStart...)
allFishbones = append(allFishbones, d.FishbonesEnd...)
return allFishbones
}
}

View File

@ -72,13 +72,14 @@ func (p *PortAssignments) Scan(value interface{}) error {
var bytes []byte
switch v := value.(type) {
case []byte: // []byte and []uint8 are the same type in Go
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

View File

@ -31,10 +31,43 @@ type Device struct {
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
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (d *Device) GetAllImageURLs() []string {
var allImages []string
// Add main image URL if exists
if d.ImageURL != nil && *d.ImageURL != "" {
allImages = append(allImages, *d.ImageURL)
}
// Add additional images
for _, img := range d.ImageURLs {
if img != "" && (d.ImageURL == nil || img != *d.ImageURL) { // Avoid duplicates
allImages = append(allImages, img)
}
}
return allImages
}
// Set multiple images (first one becomes primary)
func (d *Device) SetMultipleImages(imageURLs []string) {
if len(imageURLs) == 0 {
return
}
// Set primary image
primaryImage := imageURLs[0]
d.ImageURL = &primaryImage
// Set all images
d.ImageURLs = StringSlice(imageURLs)
}
func (Device) TableName() string {
return "devices"
}

View File

@ -1,3 +1,4 @@
// Update model/entity/tower.go
package entity
import (
@ -11,12 +12,46 @@ type Tower struct {
TowerCode string `json:"tower_code" gorm:"unique"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
ImageURL string `json:"image_url" gorm:"column:image_url"`
ExternalTower *bool `json:"external_tower,omitempty" gorm:"column:external_tower;null"` // Make nullable and fix typo
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
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 `json:"device,omitempty" gorm:"foreignKey:DeviceID"` // Make nullable
// Helper method to get all image URLs
func (t *Tower) GetAllImageURLs() []string {
var allImages []string
// Add main image URL if exists
if t.ImageURL != "" {
allImages = append(allImages, t.ImageURL)
}
// Add additional images
for _, img := range t.ImageURLs {
if img != "" && img != t.ImageURL { // Avoid duplicates
allImages = append(allImages, img)
}
}
return allImages
}
// Set multiple images (first one becomes primary)
func (t *Tower) SetMultipleImages(imageURLs []string) {
if len(imageURLs) == 0 {
return
}
// Set primary image
t.ImageURL = imageURLs[0]
// Set all images
t.ImageURLs = StringSlice(imageURLs)
}
func (Tower) TableName() string {

View File

@ -1,6 +1,7 @@
package repository
import (
"fmt"
"time"
"users_management/m/model/entity"
@ -15,6 +16,8 @@ type DevicesRepo interface {
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
}
type devicesRepo struct {
@ -27,6 +30,40 @@ func NewDevicesRepo(db *gorm.DB) DevicesRepo {
}
}
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

View File

@ -1,6 +1,8 @@
package repository
import (
"fmt"
"time"
"users_management/m/model/entity"
"github.com/google/uuid"
@ -13,6 +15,7 @@ type TowerRepo interface {
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
}
type towerRepo struct {
@ -63,4 +66,20 @@ func (r *towerRepo) GetByID(id uuid.UUID) (entity.Tower, error) {
return tower, err
}
return tower, nil
}
func (r *towerRepo) BulkUpdateImages(updates map[uuid.UUID]string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
for towerID, imageURL := range updates {
if err := tx.Model(&entity.Tower{}).
Where("id = ?", towerID).
Updates(map[string]interface{}{
"image_url": imageURL,
"updated_at": time.Now(),
}).Error; err != nil {
return fmt.Errorf("failed to update tower %s: %w", towerID, err)
}
}
return nil
})
}

View File

@ -3,6 +3,7 @@ package usecase
import (
"errors"
"fmt"
"mime/multipart"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
@ -32,6 +33,7 @@ type DeviceDetailsUseCase interface {
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
}
type deviceDetailsUseCase struct {
@ -48,6 +50,122 @@ func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, geo
}
}
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 {
// 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
}
// 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
if currentDevice.ImageURL != nil && *currentDevice.ImageURL != "" {
helper.DeleteDeviceImage(*currentDevice.ImageURL)
}
// Delete other images if they exist
for _, imgURL := range currentDevice.AdditionalImages {
if imgURL != "" && (currentDevice.ImageURL == nil || imgURL != *currentDevice.ImageURL) {
helper.DeleteDeviceImage(imgURL)
}
}
finalImageURLs = newImageURLs
} else {
// Append to existing images
existingImages := make([]string, 0)
if currentDevice.ImageURL != nil && *currentDevice.ImageURL != "" {
existingImages = append(existingImages, *currentDevice.ImageURL)
}
for _, imgURL := range currentDevice.AdditionalImages {
if imgURL != "" && (currentDevice.ImageURL == nil || imgURL != *currentDevice.ImageURL) {
existingImages = append(existingImages, imgURL)
}
}
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
updates["additional_images"] = 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 {

View File

@ -2,6 +2,7 @@ package usecase
import (
"fmt"
"mime/multipart"
"time"
"users_management/m/model/dto/req"
"users_management/m/model/dto/res"
@ -21,6 +22,10 @@ type DeviceUseCase interface {
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
}
type deviceUseCase struct {
@ -37,33 +42,188 @@ func NewDeviceUseCase(deviceRepo repository.DevicesRepo, geocoder service.Geocod
}
}
func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
err := u.validate.Struct(device)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
if device.DeviceType == "OTB" || device.DeviceType == "ODP" && device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
newDevice := entity.Device{
ID: uuid.New(),
DeviceCode: device.DeviceCode,
DeviceType: entity.DeviceType(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: entity.DeviceStatus(device.Status),
Province: device.Province, // Now nullable
City: device.City, // Now nullable
District: device.District, // Now nullable
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
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)
}
return u.deviceRepo.Post(newDevice)
if device.DeviceType == "OTB" || device.DeviceType == "ODP" && device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
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(),
DeviceCode: device.DeviceCode,
DeviceType: entity.DeviceType(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: entity.DeviceStatus(device.Status),
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: &primaryImageURL, // Primary image as pointer
ImageURLs: entity.StringSlice(imageURLs), // All images
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.deviceRepo.Post(newDevice)
}
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 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) {
@ -78,6 +238,15 @@ func (u *deviceUseCase) GetAllDevices() ([]res.DeviceResponse, error) {
return devicesResponse, nil
}
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 {

View File

@ -21,6 +21,9 @@ type TowerUseCase interface {
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 {
@ -36,6 +39,135 @@ func NewTowerUseCase(towerRepo repository.TowerRepo, geocoder service.GeocodingS
validate: validator.New(),
}
}
func (u *towerUsecase) PostWithMultipleImages(tower req.TowerDTO, imageFiles []*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 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
}
if tower.TowerCode != nil {
updates["TowerCode"] = *tower.TowerCode
}
if tower.Longitude != nil {
updates["Longitude"] = *tower.Longitude
}
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 {
return errors.New("no fields to update")
}
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)
@ -108,6 +240,42 @@ func (u *towerUsecase) GetByID(id uuid.UUID) (res.TowerResponse, error) {
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 {

View File

@ -1,7 +1,6 @@
package helper
import (
"fmt"
"log"
"users_management/m/model/dto/res"
"users_management/m/model/entity"
@ -28,11 +27,19 @@ func ConvertToDeviceResponse(devices []entity.Device, geocoder service.Geocoding
var responses []res.DeviceResponse
for _, device := range devices {
// Get address from coordinates
address, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude)
log.Printf("Geocoding for device %s: %.6f, %.6f -> %s", device.DeviceCode, device.Latitude, device.Longitude, address)
if err != nil {
address = fmt.Sprintf("Coordinates: %.6f, %.6f", device.Latitude, device.Longitude)
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{
@ -41,12 +48,14 @@ func ConvertToDeviceResponse(devices []entity.Device, geocoder service.Geocoding
DeviceType: string(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
Address: address,
PortAmount: device.PortAmount,
Status: string(device.Status),
Province: device.Province, // Nullable field
City: device.City, // Nullable field
District: device.District, // Nullable field
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,
}
@ -56,36 +65,44 @@ func ConvertToDeviceResponse(devices []entity.Device, geocoder service.Geocoding
return responses, nil
}
func ConvertToDeviceResponseId (devices entity.Device, geocoder service.GeocodingService) (res.DeviceResponse, error) {
var address string
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")
}
if geocoder != nil {
generatedAddress, err := geocoder.GetAddressFromCoordinates(devices.Latitude, devices.Longitude)
if err != nil {
// Log specific geocoding error
log.Printf("Geocoding error for device %s: %v", devices.DeviceCode, err)
} else {
address = generatedAddress
}
} else{
log.Println("WARNING: Geocoder is nil")
}
// 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: devices.ID,
DeviceCode: devices.DeviceCode,
DeviceType: string(devices.DeviceType),
Longitude: devices.Longitude,
Latitude: devices.Latitude,
Address: address,
Province: devices.Province,
City: devices.City,
District: devices.District,
PortAmount: devices.PortAmount,
Status: string(devices.Status),
CreatedAt: devices.CreatedAt,
UpdatedAt: devices.UpdatedAt,
}
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
return deviceResp, nil
}

View File

@ -105,6 +105,7 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
Distance: distance,
ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil
ImageURL: imageURL,
ImageURLs: tower.GetAllImageURLs(), // Get all images as a slice
}
towerInfos = append(towerInfos, info)
}
@ -196,6 +197,19 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
// Get customer names
customerNames = device.DevicePort.GetCustomerNamesOnly()
}
var deviceImageURLs []string
if device.ImageURL != nil && *device.ImageURL != "" {
deviceImageURLs = append(deviceImageURLs, *device.ImageURL)
}
// Use deviceImageURLs instead of trying to call a non-existent method
allImageURLs := deviceImageURLs
// Handle empty slice vs nil for JSON response
if len(allImageURLs) == 0 {
allImageURLs = []string{} // Return empty array instead of nil
}
response := res.DeviceDetailsResponse{
ID: device.ID,
@ -215,6 +229,7 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
ImageURLs: allImageURLs, // Use allImageURLs to ensure empty array instead of nil
Backbones: backboneInfos,
Fishbones: fishboneInfos,
Towers: towerInfos,

View File

@ -14,9 +14,145 @@ import (
const (
MaxFileSize = 5 << 20 // 5MB
UploadDir = "./uploads/towers"
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
@ -39,13 +175,13 @@ func SaveTowerImage(file *multipart.FileHeader) (string, error) {
}
// Create upload directory if it doesn't exist
if err := os.MkdirAll(UploadDir, 0755); err != nil {
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(UploadDir, filename)
filePath := filepath.Join(TowerUploadDir, filename)
// Open uploaded file
src, err := file.Open()
@ -78,7 +214,7 @@ func DeleteTowerImage(imageURL string) error {
// Extract filename from URL
filename := filepath.Base(imageURL)
filePath := filepath.Join(UploadDir, filename)
filePath := filepath.Join(TowerUploadDir, filename)
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {

View File

@ -12,6 +12,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe
for _, tower := range towers {
var address string
allImageURLs := tower.GetAllImageURLs()
if geocoder != nil {
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
@ -23,7 +24,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe
}
var deviceCode *string
if tower.Device != nil {
if tower.Device.DeviceCode != "" {
deviceCode = &tower.Device.DeviceCode
}
@ -35,6 +36,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe
Latitude: tower.Latitude,
Address: address,
ImageURL: tower.ImageURL,
ImageURLs: allImageURLs, // All images
ExternalTower: tower.ExternalTower, // Now nullable
CreatedAt: tower.CreatedAt,
UpdatedAt: tower.UpdatedAt,
@ -57,9 +59,11 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer
}
var deviceCode *string
if tower.Device != nil {
if tower.Device.DeviceCode != "" {
deviceCode = &tower.Device.DeviceCode
}
// Get all image URLs
allImageURLs := tower.GetAllImageURLs()
towerResp := res.TowerResponse{
ID: tower.ID,
@ -69,6 +73,7 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer
Latitude: tower.Latitude,
Address: address,
ImageURL: tower.ImageURL,
ImageURLs: allImageURLs, // All images
ExternalTower: tower.ExternalTower, // Now nullable
CreatedAt: tower.CreatedAt,
UpdatedAt: tower.UpdatedAt,

View File

@ -0,0 +1,76 @@
// Create utils/migration/tower_migration.go
package migrations
import (
"fmt"
"gorm.io/gorm"
)
func MigrateTowerConstraints(db *gorm.DB) error {
// First, check if the constraint exists before trying to drop it
var constraintExists bool
err := db.Raw(`
SELECT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'uni_towers_tower_code'
AND table_name = 'towers'
)
`).Scan(&constraintExists).Error
if err != nil {
return fmt.Errorf("failed to check constraint existence: %w", err)
}
// Only drop the constraint if it exists
if constraintExists {
err = db.Exec("ALTER TABLE towers DROP CONSTRAINT uni_towers_tower_code").Error
if err != nil {
return fmt.Errorf("failed to drop constraint: %w", err)
}
}
// Check if the new index exists
var indexExists bool
err = db.Raw(`
SELECT EXISTS (
SELECT 1 FROM pg_indexes
WHERE indexname = 'idx_towers_tower_code'
AND tablename = 'towers'
)
`).Scan(&indexExists).Error
if err != nil {
return fmt.Errorf("failed to check index existence: %w", err)
}
// Create the new unique index if it doesn't exist
if !indexExists {
err = db.Exec("CREATE UNIQUE INDEX idx_towers_tower_code ON towers (tower_code)").Error
if err != nil {
return fmt.Errorf("failed to create unique index: %w", err)
}
}
// Add image_urls column if it doesn't exist
var columnExists bool
err = db.Raw(`
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'towers'
AND column_name = 'image_urls'
)
`).Scan(&columnExists).Error
if err != nil {
return fmt.Errorf("failed to check column existence: %w", err)
}
if !columnExists {
err = db.Exec("ALTER TABLE towers ADD COLUMN image_urls JSONB").Error
if err != nil {
return fmt.Errorf("failed to add image_urls column: %w", err)
}
}
return nil
}