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:
commit
45dc3e80da
|
|
@ -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 = ®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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue