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"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"users_management/m/middleware"
|
"users_management/m/middleware"
|
||||||
"users_management/m/model/dto/req"
|
"users_management/m/model/dto/req"
|
||||||
"users_management/m/usecase"
|
"users_management/m/usecase"
|
||||||
|
|
@ -235,13 +238,97 @@ func (c *DeviceDetailsController) updateDeviceDetails(ctx *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var request req.UpdateDeviceDetailsDTO
|
contentType := ctx.GetHeader("Content-Type")
|
||||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
// 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
|
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 {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"users_management/m/middleware"
|
"users_management/m/middleware"
|
||||||
"users_management/m/model/dto/req"
|
"users_management/m/model/dto/req"
|
||||||
"users_management/m/usecase"
|
"users_management/m/usecase"
|
||||||
|
|
@ -24,6 +29,8 @@ func (dc *DeviceController) Route() {
|
||||||
rg.GET("", dc.GetAllDevices())
|
rg.GET("", dc.GetAllDevices())
|
||||||
rg.GET("/:uuid", dc.GetDeviceByID())
|
rg.GET("/:uuid", dc.GetDeviceByID())
|
||||||
rg.PUT("/:uuid", dc.UpdateDevice())
|
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 {
|
func (dc *DeviceController) BulkUploadImages() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var deviceDTO req.DeviceDTO
|
// Parse multipart form
|
||||||
err := c.ShouldBindJSON(&deviceDTO)
|
err := c.Request.ParseMultipartForm(100 << 20) // 100MB for bulk upload
|
||||||
|
if err != nil {
|
||||||
if err != nil {
|
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// Get device data from form
|
||||||
err = dc.du.CreateDevice(deviceDTO)
|
deviceDataStr := c.PostForm("devices")
|
||||||
|
if deviceDataStr == "" {
|
||||||
if err != nil {
|
common.ErrorResponses(c, http.StatusBadRequest, "devices data is required")
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// Parse device data
|
||||||
common.SingleResponses(c, "Device has been created", nil)
|
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 {
|
func (dc *DeviceController) GetAllDevices() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|
||||||
|
|
@ -99,29 +281,108 @@ func (dc *DeviceController) GetDeviceByID() gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DeviceController) UpdateDevice() gin.HandlerFunc {
|
func (dc *DeviceController) UpdateDevice() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
id := c.Param("uuid")
|
id := c.Param("uuid")
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID")
|
common.ErrorResponses(c, http.StatusBadRequest, "Invalid UUID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceDTO req.UpdateDeviceDTO
|
contentType := c.GetHeader("Content-Type")
|
||||||
err = c.ShouldBindJSON(&deviceDTO)
|
|
||||||
|
// 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 {
|
err = dc.du.UpdateDevice(uuid, deviceDTO)
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, "Invalid request")
|
if err != nil {
|
||||||
return
|
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 {
|
// Handle multipart form request
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, "Device not found")
|
err = c.Request.ParseMultipartForm(50 << 20) // 50MB for multiple images
|
||||||
return
|
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
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -27,6 +30,8 @@ func (tc *TowerController) Route() {
|
||||||
|
|
||||||
rg.GET("/:uuid", tc.GetTowerByID())
|
rg.GET("/:uuid", tc.GetTowerByID())
|
||||||
rg.PUT("/:uuid", tc.UpdateTower())
|
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 {
|
func (tc *TowerController) GetTower() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
towers, err := tc.tu.GetAll()
|
towers, err := tc.tu.GetAll()
|
||||||
|
|
@ -55,7 +110,7 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
|
||||||
|
|
||||||
contentType := c.GetHeader("Content-Type")
|
contentType := c.GetHeader("Content-Type")
|
||||||
|
|
||||||
// Handle JSON request
|
// Handle JSON request (no images)
|
||||||
if strings.Contains(contentType, "application/json") {
|
if strings.Contains(contentType, "application/json") {
|
||||||
var towerDTO req.TowerDTO
|
var towerDTO req.TowerDTO
|
||||||
if err := c.ShouldBindJSON(&towerDTO); err != nil {
|
if err := c.ShouldBindJSON(&towerDTO); err != nil {
|
||||||
|
|
@ -63,7 +118,7 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// No image file for JSON requests
|
// No image files for JSON requests
|
||||||
err := tc.tu.Post(towerDTO, nil)
|
err := tc.tu.Post(towerDTO, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
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)
|
common.SingleResponses(c, "Tower has been created", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse multipart form
|
// 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 {
|
if err != nil {
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
|
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Extract form data
|
// Extract form data
|
||||||
deviceIDStr := c.PostForm("dev_id")
|
deviceIDStr := c.PostForm("dev_id")
|
||||||
towerCode := c.PostForm("tower_code")
|
towerCode := c.PostForm("tower_code")
|
||||||
|
|
@ -137,10 +191,18 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
|
||||||
ExternalTower: externalTower,
|
ExternalTower: externalTower,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get image file (optional)
|
// Get multiple image files
|
||||||
imageFile, _ := c.FormFile("image")
|
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 {
|
if err != nil {
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -181,7 +243,7 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
|
||||||
|
|
||||||
contentType := c.GetHeader("Content-Type")
|
contentType := c.GetHeader("Content-Type")
|
||||||
|
|
||||||
// Handle JSON request
|
// Handle JSON request (no images)
|
||||||
if strings.Contains(contentType, "application/json") {
|
if strings.Contains(contentType, "application/json") {
|
||||||
var towerUpdateDTO req.UpdateTowerDTO
|
var towerUpdateDTO req.UpdateTowerDTO
|
||||||
if err := c.ShouldBindJSON(&towerUpdateDTO); err != nil {
|
if err := c.ShouldBindJSON(&towerUpdateDTO); err != nil {
|
||||||
|
|
@ -189,8 +251,8 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// No image file for JSON requests
|
// No image files for JSON requests
|
||||||
err := tc.tu.UpdateTower(tower_uuid, towerUpdateDTO, nil)
|
err := tc.tu.UpdateTowerWithMultipleImages(tower_uuid, towerUpdateDTO, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -200,8 +262,8 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle multipart form request (existing logic)
|
// Handle multipart form request
|
||||||
err = c.Request.ParseMultipartForm(10 << 20) // 10MB max
|
err = c.Request.ParseMultipartForm(50 << 20) // Increase to 50MB for multiple images
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
|
common.ErrorResponses(c, http.StatusBadRequest, "Failed to parse multipart form")
|
||||||
return
|
return
|
||||||
|
|
@ -257,10 +319,21 @@ func (tc *TowerController) UpdateTower() gin.HandlerFunc {
|
||||||
towerUpdateDTO.ImageURL = &imageURL
|
towerUpdateDTO.ImageURL = &imageURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get image file (optional)
|
// Get multiple image files
|
||||||
imageFile, _ := c.FormFile("image")
|
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 {
|
if err != nil {
|
||||||
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ func (s *Server) setupController() {
|
||||||
rg := s.engine.Group("/api/v1")
|
rg := s.engine.Group("/api/v1")
|
||||||
controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route()
|
controller.NewUsersController(s.ucManager.NewUserUsecase(), s.ucManager.NewAuthUsecase(),rg).Route()
|
||||||
rg.Use(middleware.AuthMiddleware(s.ucManager.NewUserUsecase()))
|
rg.Use(middleware.AuthMiddleware(s.ucManager.NewUserUsecase()))
|
||||||
|
controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route()
|
||||||
rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase()))
|
rg.Use(middleware.ActivityLoggingMiddleware(s.ucManager.NewActivityLogUsecase()))
|
||||||
{
|
{
|
||||||
controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route()
|
controller.NewDeviceController(s.ucManager.NewDeviceUsecase(), rg).Route()
|
||||||
|
|
@ -57,7 +58,6 @@ func (s *Server) setupController() {
|
||||||
controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), rg).Route()
|
controller.NewFishboneController(s.ucManager.NewFishboneUsecase(), rg).Route()
|
||||||
controller.NewTowerController(s.ucManager.NewTowerUsecase(), rg).Route()
|
controller.NewTowerController(s.ucManager.NewTowerUsecase(), rg).Route()
|
||||||
controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), rg).Route()
|
controller.NewDevicePortController(s.ucManager.NewDevicePortUsecase(), rg).Route()
|
||||||
controller.NewCountAssetsController(s.ucManager.NewCountAssetsUsecase(), rg).Route()
|
|
||||||
controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), rg).Route()
|
controller.NewActivityLogController(s.ucManager.NewActivityLogUsecase(), rg).Route()
|
||||||
controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), rg).Route()
|
controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), rg).Route()
|
||||||
controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), 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"`
|
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
|
||||||
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
|
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
|
||||||
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
|
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssignMultipleCustomersDTO struct {
|
type AssignMultipleCustomersDTO struct {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,35 @@
|
||||||
package req
|
package req
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
type DeviceDTO struct {
|
type DeviceDTO struct {
|
||||||
DeviceCode string `json:"device_code" validate:"required"`
|
DeviceCode string `json:"device_code" validate:"required"`
|
||||||
DeviceType string `json:"device_type" validate:"required"`
|
DeviceType string `json:"device_type" validate:"required"`
|
||||||
Longitude float64 `json:"longitude" validate:"required"`
|
Longitude float64 `json:"longitude" validate:"required"`
|
||||||
Latitude float64 `json:"latitude" validate:"required"`
|
Latitude float64 `json:"latitude" validate:"required"`
|
||||||
PortAmount int `json:"port_amount"`
|
PortAmount int `json:"port_amount"`
|
||||||
Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
|
Status string `json:"status" validate:"required,oneof=active inactive maintenance"`
|
||||||
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
|
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
|
||||||
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
|
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
|
||||||
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
|
District *string `json:"district,omitempty" validate:"omitempty,min=3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateDeviceDTO struct {
|
type UpdateDeviceDTO struct {
|
||||||
DeviceCode *string `json:"device_code,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"`
|
DeviceType *string `json:"device_type,omitempty" validate:"omitempty,min=3"`
|
||||||
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
|
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
|
||||||
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
|
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
|
||||||
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty"`
|
PortAmount *int `json:"port_amount,omitempty" validate:"omitempty"`
|
||||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"`
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=active inactive maintenance"`
|
||||||
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
|
Province *string `json:"province,omitempty" validate:"omitempty,min=3"`
|
||||||
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
|
City *string `json:"city,omitempty" validate:"omitempty,min=3"`
|
||||||
District *string `json:"district,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"`
|
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
|
||||||
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
|
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
|
||||||
ImageURL *string `json:"image_url,omitempty"`
|
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"`
|
City *string `json:"city,omitempty"`
|
||||||
District *string `json:"district,omitempty"`
|
District *string `json:"district,omitempty"`
|
||||||
ImageURL *string `json:"image_url,omitempty"`
|
ImageURL *string `json:"image_url,omitempty"`
|
||||||
|
ImageURLs []string `json:"image_urls"`
|
||||||
|
|
||||||
// Connection details
|
// Connection details
|
||||||
Backbones []BackboneConnectionInfo `json:"backbones"`
|
Backbones []BackboneConnectionInfo `json:"backbones"`
|
||||||
|
|
@ -39,6 +40,7 @@ type TowerConnectionDetail struct {
|
||||||
Distance float64 `json:"distance_km"` // Distance from tower to device
|
Distance float64 `json:"distance_km"` // Distance from tower to device
|
||||||
ExternalTower *bool `json:"external_tower"` // Indicates if this is an external tower
|
ExternalTower *bool `json:"external_tower"` // Indicates if this is an external tower
|
||||||
ImageURL *string `json:"image_url,omitempty"`
|
ImageURL *string `json:"image_url,omitempty"`
|
||||||
|
ImageURLs []string `json:"image_urls"` // Store multiple images as JSONB
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortAssignmentResponse struct {
|
type PortAssignmentResponse struct {
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,15 @@ type DeviceResponse struct {
|
||||||
DeviceType string `json:"device_type"`
|
DeviceType string `json:"device_type"`
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
Address string `json:"address"`
|
|
||||||
PortAmount int `json:"port_amount"`
|
PortAmount int `json:"port_amount"`
|
||||||
Status string `json:"status"` // Always include, even if null
|
Status string `json:"status"`
|
||||||
CustomerNames []string `json:"customer_names"` // Always include, even if empty
|
Address string `json:"address"`
|
||||||
Province *string `json:"province"` // Always include, even if null
|
Region *string `json:"region"`
|
||||||
City *string `json:"city"` // Always include, even if null
|
Province *string `json:"province"`
|
||||||
District *string `json:"district"` // Always include, even if null
|
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"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type TowerResponse struct {
|
||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
ImageURL string `json:"image_url"`
|
ImageURL string `json:"image_url"`
|
||||||
|
ImageURLs []string `json:"image_urls"` // Store multiple images as JSONB
|
||||||
ExternalTower *bool `json:"external_tower"` // Make nullable
|
ExternalTower *bool `json:"external_tower"` // Make nullable
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type DeviceDetails struct {
|
||||||
City *string `json:"city,omitempty" gorm:"type:varchar(255)"`
|
City *string `json:"city,omitempty" gorm:"type:varchar(255)"`
|
||||||
District *string `json:"district,omitempty" gorm:"type:varchar(255)"`
|
District *string `json:"district,omitempty" gorm:"type:varchar(255)"`
|
||||||
ImageURL *string `json:"image_url,omitempty" gorm:"type:text"`
|
ImageURL *string `json:"image_url,omitempty" gorm:"type:text"`
|
||||||
|
AdditionalImages StringSlice `gorm:"type:text[]"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_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.FishbonesStart...)
|
||||||
allFishbones = append(allFishbones, d.FishbonesEnd...)
|
allFishbones = append(allFishbones, d.FishbonesEnd...)
|
||||||
return allFishbones
|
return allFishbones
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,14 @@ func (p *PortAssignments) Scan(value interface{}) error {
|
||||||
|
|
||||||
var bytes []byte
|
var bytes []byte
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case []byte: // []byte and []uint8 are the same type in Go
|
case []byte:
|
||||||
bytes = v
|
bytes = v
|
||||||
case string:
|
case string:
|
||||||
bytes = []byte(v)
|
bytes = []byte(v)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("cannot scan %T into PortAssignments", value)
|
return fmt.Errorf("cannot scan %T into PortAssignments", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(bytes) == 0 {
|
if len(bytes) == 0 {
|
||||||
*p = PortAssignments{}
|
*p = PortAssignments{}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,43 @@ type Device struct {
|
||||||
City *string `json:"city,omitempty" gorm:"type:varchar(255)"`
|
City *string `json:"city,omitempty" gorm:"type:varchar(255)"`
|
||||||
District *string `json:"district,omitempty" gorm:"type:varchar(255)"`
|
District *string `json:"district,omitempty" gorm:"type:varchar(255)"`
|
||||||
ImageURL *string `json:"image_url,omitempty" gorm:"type:text"`
|
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"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_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 {
|
func (Device) TableName() string {
|
||||||
return "devices"
|
return "devices"
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Update model/entity/tower.go
|
||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -11,12 +12,46 @@ type Tower struct {
|
||||||
TowerCode string `json:"tower_code" gorm:"unique"`
|
TowerCode string `json:"tower_code" gorm:"unique"`
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
ImageURL string `json:"image_url" gorm:"column:image_url"`
|
ImageURL string `json:"image_url"` // Keep for backward compatibility
|
||||||
ExternalTower *bool `json:"external_tower,omitempty" gorm:"column:external_tower;null"` // Make nullable and fix typo
|
ImageURLs StringSlice `json:"image_urls" gorm:"type:jsonb"` // Multiple images
|
||||||
CreatedAt time.Time `json:"created_at"`
|
ExternalTower *bool `json:"external_tower,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
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 {
|
func (Tower) TableName() string {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
"users_management/m/model/entity"
|
"users_management/m/model/entity"
|
||||||
|
|
||||||
|
|
@ -15,6 +16,8 @@ type DevicesRepo interface {
|
||||||
|
|
||||||
GetByID(id uuid.UUID) (entity.Device, error)
|
GetByID(id uuid.UUID) (entity.Device, error)
|
||||||
GetByType(deviceType string) ([]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 {
|
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 {
|
func (r *devicesRepo) Post(device entity.Device) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
// Create the device first
|
// Create the device first
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
"users_management/m/model/entity"
|
"users_management/m/model/entity"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
@ -13,6 +15,7 @@ type TowerRepo interface {
|
||||||
Update(id uuid.UUID,updates map[string]interface{}) error
|
Update(id uuid.UUID,updates map[string]interface{}) error
|
||||||
GetByID(id uuid.UUID) (entity.Tower, error)
|
GetByID(id uuid.UUID) (entity.Tower, error)
|
||||||
CheckDeviceExists(deviceID uuid.UUID) (bool, error)
|
CheckDeviceExists(deviceID uuid.UUID) (bool, error)
|
||||||
|
BulkUpdateImages(updates map[uuid.UUID]string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type towerRepo struct {
|
type towerRepo struct {
|
||||||
|
|
@ -63,4 +66,20 @@ func (r *towerRepo) GetByID(id uuid.UUID) (entity.Tower, error) {
|
||||||
return tower, err
|
return tower, err
|
||||||
}
|
}
|
||||||
return tower, nil
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"time"
|
"time"
|
||||||
"users_management/m/model/dto/req"
|
"users_management/m/model/dto/req"
|
||||||
"users_management/m/model/dto/res"
|
"users_management/m/model/dto/res"
|
||||||
|
|
@ -32,6 +33,7 @@ type DeviceDetailsUseCase interface {
|
||||||
UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error
|
UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error
|
||||||
BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error
|
BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error
|
||||||
RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error
|
RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error
|
||||||
|
UpdateDeviceDetailsWithMultipleImages(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type deviceDetailsUseCase struct {
|
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 {
|
func (u *deviceDetailsUseCase) UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error {
|
||||||
// Validate port number
|
// Validate port number
|
||||||
if update.PortNumber < 1 {
|
if update.PortNumber < 1 {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"time"
|
"time"
|
||||||
"users_management/m/model/dto/req"
|
"users_management/m/model/dto/req"
|
||||||
"users_management/m/model/dto/res"
|
"users_management/m/model/dto/res"
|
||||||
|
|
@ -21,6 +22,10 @@ type DeviceUseCase interface {
|
||||||
GetByID(id uuid.UUID) (res.DeviceResponse, error)
|
GetByID(id uuid.UUID) (res.DeviceResponse, error)
|
||||||
UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) error
|
UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) error
|
||||||
GetByType(deviceType string) ([]res.DeviceTypeResponse, 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 {
|
type deviceUseCase struct {
|
||||||
|
|
@ -37,33 +42,188 @@ func NewDeviceUseCase(deviceRepo repository.DevicesRepo, geocoder service.Geocod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
|
func (u *deviceUseCase) CreateDeviceWithMultipleImages(device req.DeviceDTO, imageFiles []*multipart.FileHeader) error {
|
||||||
err := u.validate.Struct(device)
|
err := u.validate.Struct(device)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validation error: %w", err)
|
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(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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) {
|
func (u *deviceUseCase) GetAllDevices() ([]res.DeviceResponse, error) {
|
||||||
|
|
@ -78,6 +238,15 @@ func (u *deviceUseCase) GetAllDevices() ([]res.DeviceResponse, error) {
|
||||||
return devicesResponse, nil
|
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) {
|
func (u *deviceUseCase) GetByID(id uuid.UUID) (res.DeviceResponse, error) {
|
||||||
device, err := u.deviceRepo.GetByID(id)
|
device, err := u.deviceRepo.GetByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ type TowerUseCase interface {
|
||||||
GetAll() ([]res.TowerResponse, error)
|
GetAll() ([]res.TowerResponse, error)
|
||||||
GetByID(id uuid.UUID) (res.TowerResponse, error)
|
GetByID(id uuid.UUID) (res.TowerResponse, error)
|
||||||
UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) 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 {
|
type towerUsecase struct {
|
||||||
|
|
@ -36,6 +39,135 @@ func NewTowerUseCase(towerRepo repository.TowerRepo, geocoder service.GeocodingS
|
||||||
validate: validator.New(),
|
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 {
|
func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error {
|
||||||
err := u.validate.Struct(tower)
|
err := u.validate.Struct(tower)
|
||||||
|
|
@ -108,6 +240,42 @@ func (u *towerUsecase) GetByID(id uuid.UUID) (res.TowerResponse, error) {
|
||||||
return towerResp, nil
|
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 {
|
func (u *towerUsecase) UpdateTower(id uuid.UUID, tower req.UpdateTowerDTO, imageFile *multipart.FileHeader) error {
|
||||||
err := u.validate.Struct(tower)
|
err := u.validate.Struct(tower)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"users_management/m/model/dto/res"
|
"users_management/m/model/dto/res"
|
||||||
"users_management/m/model/entity"
|
"users_management/m/model/entity"
|
||||||
|
|
@ -28,11 +27,19 @@ func ConvertToDeviceResponse(devices []entity.Device, geocoder service.Geocoding
|
||||||
var responses []res.DeviceResponse
|
var responses []res.DeviceResponse
|
||||||
|
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
// Get address from coordinates
|
address := "Address not found"
|
||||||
address, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude)
|
if geocoder != nil {
|
||||||
log.Printf("Geocoding for device %s: %.6f, %.6f -> %s", device.DeviceCode, device.Latitude, device.Longitude, address)
|
if addr, err := geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude); err == nil {
|
||||||
if err != nil {
|
address = addr
|
||||||
address = fmt.Sprintf("Coordinates: %.6f, %.6f", device.Latitude, device.Longitude)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{
|
response := res.DeviceResponse{
|
||||||
|
|
@ -41,12 +48,14 @@ func ConvertToDeviceResponse(devices []entity.Device, geocoder service.Geocoding
|
||||||
DeviceType: string(device.DeviceType),
|
DeviceType: string(device.DeviceType),
|
||||||
Longitude: device.Longitude,
|
Longitude: device.Longitude,
|
||||||
Latitude: device.Latitude,
|
Latitude: device.Latitude,
|
||||||
Address: address,
|
|
||||||
PortAmount: device.PortAmount,
|
PortAmount: device.PortAmount,
|
||||||
Status: string(device.Status),
|
Status: string(device.Status),
|
||||||
Province: device.Province, // Nullable field
|
Address: address,
|
||||||
City: device.City, // Nullable field
|
Province: device.Province,
|
||||||
District: device.District, // Nullable field
|
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,
|
CreatedAt: device.CreatedAt,
|
||||||
UpdatedAt: device.UpdatedAt,
|
UpdatedAt: device.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
@ -56,36 +65,44 @@ func ConvertToDeviceResponse(devices []entity.Device, geocoder service.Geocoding
|
||||||
|
|
||||||
return responses, nil
|
return responses, nil
|
||||||
}
|
}
|
||||||
func ConvertToDeviceResponseId (devices entity.Device, geocoder service.GeocodingService) (res.DeviceResponse, error) {
|
func ConvertToDeviceResponseId(device entity.Device, geocoder service.GeocodingService) (res.DeviceResponse, error) {
|
||||||
var address string
|
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 {
|
// Get all image URLs using the entity method
|
||||||
generatedAddress, err := geocoder.GetAddressFromCoordinates(devices.Latitude, devices.Longitude)
|
allImageURLs := device.GetAllImageURLs()
|
||||||
if err != nil {
|
|
||||||
// Log specific geocoding error
|
// Handle empty slice vs nil for JSON response
|
||||||
log.Printf("Geocoding error for device %s: %v", devices.DeviceCode, err)
|
if len(allImageURLs) == 0 {
|
||||||
} else {
|
allImageURLs = []string{} // Return empty array instead of nil
|
||||||
address = generatedAddress
|
}
|
||||||
}
|
|
||||||
} else{
|
|
||||||
log.Println("WARNING: Geocoder is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceResp := res.DeviceResponse{
|
deviceResp := res.DeviceResponse{
|
||||||
ID: devices.ID,
|
ID: device.ID,
|
||||||
DeviceCode: devices.DeviceCode,
|
DeviceCode: device.DeviceCode,
|
||||||
DeviceType: string(devices.DeviceType),
|
DeviceType: string(device.DeviceType),
|
||||||
Longitude: devices.Longitude,
|
Longitude: device.Longitude,
|
||||||
Latitude: devices.Latitude,
|
Latitude: device.Latitude,
|
||||||
Address: address,
|
Address: address,
|
||||||
Province: devices.Province,
|
Province: device.Province,
|
||||||
City: devices.City,
|
City: device.City,
|
||||||
District: devices.District,
|
District: device.District,
|
||||||
PortAmount: devices.PortAmount,
|
PortAmount: device.PortAmount,
|
||||||
Status: string(devices.Status),
|
Status: string(device.Status),
|
||||||
CreatedAt: devices.CreatedAt,
|
ImageURL: device.ImageURL, // Primary image (can be null)
|
||||||
UpdatedAt: devices.UpdatedAt,
|
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,
|
Distance: distance,
|
||||||
ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil
|
ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil
|
||||||
ImageURL: imageURL,
|
ImageURL: imageURL,
|
||||||
|
ImageURLs: tower.GetAllImageURLs(), // Get all images as a slice
|
||||||
}
|
}
|
||||||
towerInfos = append(towerInfos, info)
|
towerInfos = append(towerInfos, info)
|
||||||
}
|
}
|
||||||
|
|
@ -196,6 +197,19 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
|
||||||
// Get customer names
|
// Get customer names
|
||||||
customerNames = device.DevicePort.GetCustomerNamesOnly()
|
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{
|
response := res.DeviceDetailsResponse{
|
||||||
ID: device.ID,
|
ID: device.ID,
|
||||||
|
|
@ -215,6 +229,7 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
|
||||||
City: device.City,
|
City: device.City,
|
||||||
District: device.District,
|
District: device.District,
|
||||||
ImageURL: device.ImageURL,
|
ImageURL: device.ImageURL,
|
||||||
|
ImageURLs: allImageURLs, // Use allImageURLs to ensure empty array instead of nil
|
||||||
Backbones: backboneInfos,
|
Backbones: backboneInfos,
|
||||||
Fishbones: fishboneInfos,
|
Fishbones: fishboneInfos,
|
||||||
Towers: towerInfos,
|
Towers: towerInfos,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,145 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MaxFileSize = 5 << 20 // 5MB
|
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
|
// SaveTowerImage saves uploaded image and returns the URL
|
||||||
func SaveTowerImage(file *multipart.FileHeader) (string, error) {
|
func SaveTowerImage(file *multipart.FileHeader) (string, error) {
|
||||||
// Validate file size
|
// Validate file size
|
||||||
|
|
@ -39,13 +175,13 @@ func SaveTowerImage(file *multipart.FileHeader) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create upload directory if it doesn't exist
|
// 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)
|
return "", fmt.Errorf("failed to create upload directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique filename
|
// Generate unique filename
|
||||||
filename := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
|
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
|
// Open uploaded file
|
||||||
src, err := file.Open()
|
src, err := file.Open()
|
||||||
|
|
@ -78,7 +214,7 @@ func DeleteTowerImage(imageURL string) error {
|
||||||
|
|
||||||
// Extract filename from URL
|
// Extract filename from URL
|
||||||
filename := filepath.Base(imageURL)
|
filename := filepath.Base(imageURL)
|
||||||
filePath := filepath.Join(UploadDir, filename)
|
filePath := filepath.Join(TowerUploadDir, filename)
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe
|
||||||
|
|
||||||
for _, tower := range towers {
|
for _, tower := range towers {
|
||||||
var address string
|
var address string
|
||||||
|
allImageURLs := tower.GetAllImageURLs()
|
||||||
|
|
||||||
if geocoder != nil {
|
if geocoder != nil {
|
||||||
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
|
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
|
||||||
|
|
@ -23,7 +24,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceCode *string
|
var deviceCode *string
|
||||||
if tower.Device != nil {
|
if tower.Device.DeviceCode != "" {
|
||||||
deviceCode = &tower.Device.DeviceCode
|
deviceCode = &tower.Device.DeviceCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe
|
||||||
Latitude: tower.Latitude,
|
Latitude: tower.Latitude,
|
||||||
Address: address,
|
Address: address,
|
||||||
ImageURL: tower.ImageURL,
|
ImageURL: tower.ImageURL,
|
||||||
|
ImageURLs: allImageURLs, // All images
|
||||||
ExternalTower: tower.ExternalTower, // Now nullable
|
ExternalTower: tower.ExternalTower, // Now nullable
|
||||||
CreatedAt: tower.CreatedAt,
|
CreatedAt: tower.CreatedAt,
|
||||||
UpdatedAt: tower.UpdatedAt,
|
UpdatedAt: tower.UpdatedAt,
|
||||||
|
|
@ -57,9 +59,11 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceCode *string
|
var deviceCode *string
|
||||||
if tower.Device != nil {
|
if tower.Device.DeviceCode != "" {
|
||||||
deviceCode = &tower.Device.DeviceCode
|
deviceCode = &tower.Device.DeviceCode
|
||||||
}
|
}
|
||||||
|
// Get all image URLs
|
||||||
|
allImageURLs := tower.GetAllImageURLs()
|
||||||
|
|
||||||
towerResp := res.TowerResponse{
|
towerResp := res.TowerResponse{
|
||||||
ID: tower.ID,
|
ID: tower.ID,
|
||||||
|
|
@ -69,6 +73,7 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer
|
||||||
Latitude: tower.Latitude,
|
Latitude: tower.Latitude,
|
||||||
Address: address,
|
Address: address,
|
||||||
ImageURL: tower.ImageURL,
|
ImageURL: tower.ImageURL,
|
||||||
|
ImageURLs: allImageURLs, // All images
|
||||||
ExternalTower: tower.ExternalTower, // Now nullable
|
ExternalTower: tower.ExternalTower, // Now nullable
|
||||||
CreatedAt: tower.CreatedAt,
|
CreatedAt: tower.CreatedAt,
|
||||||
UpdatedAt: tower.UpdatedAt,
|
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