From de618efe3a16519d5eca3cd3e8ff19fcd049b61b Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Fri, 28 Mar 2025 16:24:29 +0700 Subject: [PATCH] feat: update feedback analytics and advertisement upload --- app/database/entity/advertisement.entity.go | 20 +-- .../advertisement/advertisement.module.go | 1 + .../controller/advertisement.controller.go | 31 +++++ .../service/advertisement.service.go | 108 ++++++++++++++-- .../controller/feedbacks.controller.go | 33 +++++ app/module/feedbacks/feedbacks.module.go | 1 + .../repository/feedbacks.repository.go | 51 ++++++++ .../feedbacks/response/feedbacks.response.go | 6 + .../feedbacks/service/feedbacks.service.go | 19 +++ docs/swagger/docs.go | 115 ++++++++++++++++++ docs/swagger/swagger.json | 115 ++++++++++++++++++ docs/swagger/swagger.yaml | 74 +++++++++++ 12 files changed, 558 insertions(+), 16 deletions(-) diff --git a/app/database/entity/advertisement.entity.go b/app/database/entity/advertisement.entity.go index 89ccd05..6353d48 100644 --- a/app/database/entity/advertisement.entity.go +++ b/app/database/entity/advertisement.entity.go @@ -3,13 +3,15 @@ package entity import "time" type Advertisement struct { - ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` - Title string `json:"title" gorm:"type:varchar"` - Description string `json:"description" gorm:"type:varchar"` - RedirectLink string `json:"redirect_link" gorm:"type:varchar"` - Placement string `json:"placement" gorm:"type:varchar"` - StatusId int `json:"status_id" gorm:"type:int4"` - IsActive bool `json:"is_active" gorm:"type:bool;default:true"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` + Title string `json:"title" gorm:"type:varchar"` + Description string `json:"description" gorm:"type:varchar"` + RedirectLink string `json:"redirect_link" gorm:"type:varchar"` + ContentFilePath *string `json:"content_file_path" gorm:"type:varchar"` + ContentFileName *string `json:"content_file_name" gorm:"type:varchar"` + Placement string `json:"placement" gorm:"type:varchar"` + StatusId int `json:"status_id" gorm:"type:int4"` + IsActive bool `json:"is_active" gorm:"type:bool;default:true"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` } diff --git a/app/module/advertisement/advertisement.module.go b/app/module/advertisement/advertisement.module.go index 9427e2a..34be7dd 100644 --- a/app/module/advertisement/advertisement.module.go +++ b/app/module/advertisement/advertisement.module.go @@ -47,6 +47,7 @@ func (_i *AdvertisementRouter) RegisterAdvertisementRoutes() { router.Get("/", advertisementController.All) router.Get("/:id", advertisementController.Show) router.Post("/", advertisementController.Save) + router.Post("/upload/:id", advertisementController.Upload) router.Put("/:id", advertisementController.Update) router.Delete("/:id", advertisementController.Delete) }) diff --git a/app/module/advertisement/controller/advertisement.controller.go b/app/module/advertisement/controller/advertisement.controller.go index 8db140c..e55e043 100644 --- a/app/module/advertisement/controller/advertisement.controller.go +++ b/app/module/advertisement/controller/advertisement.controller.go @@ -21,6 +21,7 @@ type AdvertisementController interface { All(c *fiber.Ctx) error Show(c *fiber.Ctx) error Save(c *fiber.Ctx) error + Upload(c *fiber.Ctx) error Update(c *fiber.Ctx) error Delete(c *fiber.Ctx) error } @@ -132,6 +133,36 @@ func (_i *advertisementController) Save(c *fiber.Ctx) error { }) } +// Upload Advertisement +// @Summary Upload Advertisement +// @Description API for Upload File Advertisement +// @Tags Advertisement +// @Security Bearer +// @Produce json +// @Param file formData file true "Upload file" multiple false +// @Param id path int true "Advertisement ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /advertisement/upload/{id} [post] +func (_i *advertisementController) Upload(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + err = _i.advertisementService.Upload(c, uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Advertisement successfully upload"}, + }) +} + // Update update Advertisement // @Summary update Advertisement // @Description API for update Advertisement diff --git a/app/module/advertisement/service/advertisement.service.go b/app/module/advertisement/service/advertisement.service.go index 4c106bf..158ae20 100644 --- a/app/module/advertisement/service/advertisement.service.go +++ b/app/module/advertisement/service/advertisement.service.go @@ -1,6 +1,10 @@ package service import ( + "context" + "fmt" + "github.com/gofiber/fiber/v2" + "github.com/minio/minio-go/v7" "github.com/rs/zerolog" "go-humas-be/app/database/entity" "go-humas-be/app/module/advertisement/mapper" @@ -8,14 +12,21 @@ import ( "go-humas-be/app/module/advertisement/request" "go-humas-be/app/module/advertisement/response" usersRepository "go-humas-be/app/module/users/repository" + minioStorage "go-humas-be/config/config" "go-humas-be/utils/paginator" + "math/rand" + "path/filepath" + "strconv" + "strings" + "time" ) // AdvertisementService type advertisementService struct { - Repo repository.AdvertisementRepository - UsersRepo usersRepository.UsersRepository - Log zerolog.Logger + Repo repository.AdvertisementRepository + UsersRepo usersRepository.UsersRepository + Log zerolog.Logger + MinioStorage *minioStorage.MinioStorage } // AdvertisementService define interface of IAdvertisementService @@ -23,17 +34,19 @@ type AdvertisementService interface { All(req request.AdvertisementQueryRequest) (advertisement []*response.AdvertisementResponse, paging paginator.Pagination, err error) Show(id uint) (advertisement *response.AdvertisementResponse, err error) Save(req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, err error) + Upload(c *fiber.Ctx, id uint) (err error) Update(id uint, req request.AdvertisementUpdateRequest) (err error) Delete(id uint) error } // NewAdvertisementService init AdvertisementService -func NewAdvertisementService(repo repository.AdvertisementRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository) AdvertisementService { +func NewAdvertisementService(repo repository.AdvertisementRepository, minioStorage *minioStorage.MinioStorage, usersRepo usersRepository.UsersRepository, log zerolog.Logger) AdvertisementService { return &advertisementService{ - Repo: repo, - Log: log, - UsersRepo: usersRepo, + Repo: repo, + UsersRepo: usersRepo, + MinioStorage: minioStorage, + Log: log, } } @@ -66,6 +79,87 @@ func (_i *advertisementService) Save(req request.AdvertisementCreateRequest) (ad return _i.Repo.Create(newReq) } +func (_i *advertisementService) Upload(c *fiber.Ctx, id uint) (err error) { + bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + form, err := c.MultipartForm() + + if err != nil { + return err + } + //filess := form.File["files"] + + // Create minio connection. + minioClient, err := _i.MinioStorage.ConnectMinio() + + result, err := _i.Repo.FindOne(id) + + if result == nil { + // Return status 400. Id not found. + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + if err != nil { + // Return status 500 and minio connection error. + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + for _, files := range form.File { + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top"). + Interface("files", files).Msg("") + + for _, fileHeader := range files { + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop"). + Interface("data", fileHeader).Msg("") + + src, err := fileHeader.Open() + if err != nil { + return err + } + defer src.Close() + + filename := filepath.Base(fileHeader.Filename) + filename = strings.ReplaceAll(filename, " ", "") + filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) + extension := filepath.Ext(fileHeader.Filename)[1:] + + now := time.Now() + rand.New(rand.NewSource(now.UnixNano())) + randUniqueId := rand.Intn(1000000) + + newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) + newFilename := newFilenameWithoutExt + "." + extension + + objectName := fmt.Sprintf("advertisement/upload/%d/%d/%s", now.Year(), now.Month(), newFilename) + + result.ContentFileName = &newFilename + result.ContentFilePath = &objectName + + err = _i.Repo.Update(id, result) + if err != nil { + return err + } + + // Upload file ke MinIO + _, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{}) + if err != nil { + return err + } + } + } + + return +} + func (_i *advertisementService) Update(id uint, req request.AdvertisementUpdateRequest) (err error) { _i.Log.Info().Interface("data", req).Msg("") return _i.Repo.Update(id, req.ToEntity()) diff --git a/app/module/feedbacks/controller/feedbacks.controller.go b/app/module/feedbacks/controller/feedbacks.controller.go index 4aa2ff1..7e4831f 100644 --- a/app/module/feedbacks/controller/feedbacks.controller.go +++ b/app/module/feedbacks/controller/feedbacks.controller.go @@ -22,6 +22,7 @@ type FeedbacksController interface { Save(c *fiber.Ctx) error Update(c *fiber.Ctx) error Delete(c *fiber.Ctx) error + FeedbackMonthlyStats(c *fiber.Ctx) error } func NewFeedbacksController(feedbacksService service.FeedbacksService, log zerolog.Logger) FeedbacksController { @@ -192,3 +193,35 @@ func (_i *feedbacksController) Delete(c *fiber.Ctx) error { Messages: utilRes.Messages{"Feedbacks successfully deleted"}, }) } + +// FeedbackMonthlyStats Feedbacks +// @Summary FeedbackMonthlyStats Feedbacks +// @Description API for FeedbackMonthlyStats of Feedbacks +// @Tags Articles +// @Security Bearer +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param year query int false "year" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /feedbacks/statistic/monthly [get] +func (_i *feedbacksController) FeedbackMonthlyStats(c *fiber.Ctx) error { + authToken := c.Get("Authorization") + year := c.Query("year") + yearInt, err := strconv.Atoi(year) + if err != nil { + return err + } + + response, err := _i.feedbacksService.FeedbackMonthlyStats(authToken, &yearInt) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"FeedbacksMonthlyStats of Feedbacks successfully retrieved"}, + Data: response, + }) +} diff --git a/app/module/feedbacks/feedbacks.module.go b/app/module/feedbacks/feedbacks.module.go index b4e5a3a..39f6858 100644 --- a/app/module/feedbacks/feedbacks.module.go +++ b/app/module/feedbacks/feedbacks.module.go @@ -49,5 +49,6 @@ func (_i *FeedbacksRouter) RegisterFeedbacksRoutes() { router.Post("/", feedbacksController.Save) router.Put("/:id", feedbacksController.Update) router.Delete("/:id", feedbacksController.Delete) + router.Get("/statistic/monthly", feedbacksController.FeedbackMonthlyStats) }) } diff --git a/app/module/feedbacks/repository/feedbacks.repository.go b/app/module/feedbacks/repository/feedbacks.repository.go index 5373131..0db7ed8 100644 --- a/app/module/feedbacks/repository/feedbacks.repository.go +++ b/app/module/feedbacks/repository/feedbacks.repository.go @@ -6,8 +6,10 @@ import ( "go-humas-be/app/database" "go-humas-be/app/database/entity" "go-humas-be/app/module/feedbacks/request" + "go-humas-be/app/module/feedbacks/response" "go-humas-be/utils/paginator" "strings" + "time" ) type feedbacksRepository struct { @@ -22,6 +24,7 @@ type FeedbacksRepository interface { Create(feedbacks *entity.Feedbacks) (feedbacksReturn *entity.Feedbacks, err error) Update(id uint, feedbacks *entity.Feedbacks) (err error) Delete(id uint) (err error) + FeedbacksMonthlyStats(year int) (feedbacksMonthlyStats []*response.FeedbacksMonthlyStats, err error) } func NewFeedbacksRepository(db *database.Database, logger zerolog.Logger) FeedbacksRepository { @@ -99,3 +102,51 @@ func (_i *feedbacksRepository) Update(id uint, feedbacks *entity.Feedbacks) (err func (_i *feedbacksRepository) Delete(id uint) error { return _i.DB.DB.Delete(&entity.Feedbacks{}, id).Error } + +func (_i *feedbacksRepository) FeedbacksMonthlyStats(year int) (feedbacksMonthlyStats []*response.FeedbacksMonthlyStats, err error) { + + if year < 1900 || year > 2100 { + return nil, fmt.Errorf("invalid year") + } + + var results []struct { + Month int + Day int + TotalFeedbacks int + } + + query := _i.DB.DB.Model(&entity.Feedbacks{}). + Select("EXTRACT(MONTH FROM created_at) as month, EXTRACT(DAY FROM created_at) as day, "+ + "count(id) as total_feedbacks"). + Where("EXTRACT(YEAR FROM created_at) = ?", year) + + err = query.Group("month, day").Scan(&results).Error + if err != nil { + return nil, err + } + + // Siapkan struktur untuk menyimpan data bulanan + feedbackStats := make([]*response.FeedbacksMonthlyStats, 12) + for i := 0; i < 12; i++ { + daysInMonth := time.Date(year, time.Month(i+1), 0, 0, 0, 0, 0, time.UTC).Day() + feedbackStats[i] = &response.FeedbacksMonthlyStats{ + Year: year, + Month: i + 1, + Suggestions: make([]int, daysInMonth), + } + } + + // Isi data dari hasil agregasi + for _, result := range results { + monthIndex := result.Month - 1 + dayIndex := result.Day - 1 + + if monthIndex >= 0 && monthIndex < 12 { + if dayIndex >= 0 && dayIndex < len(feedbackStats[monthIndex].Suggestions) { + feedbackStats[monthIndex].Suggestions[dayIndex] = result.TotalFeedbacks + } + } + } + + return feedbackStats, nil +} diff --git a/app/module/feedbacks/response/feedbacks.response.go b/app/module/feedbacks/response/feedbacks.response.go index 2f2004e..1c72ff4 100644 --- a/app/module/feedbacks/response/feedbacks.response.go +++ b/app/module/feedbacks/response/feedbacks.response.go @@ -14,3 +14,9 @@ type FeedbacksResponse struct { CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } + +type FeedbacksMonthlyStats struct { + Year int `json:"year"` + Month int `json:"month"` + Suggestions []int `json:"suggestions"` +} diff --git a/app/module/feedbacks/service/feedbacks.service.go b/app/module/feedbacks/service/feedbacks.service.go index dbdabdd..ffa0557 100644 --- a/app/module/feedbacks/service/feedbacks.service.go +++ b/app/module/feedbacks/service/feedbacks.service.go @@ -25,6 +25,7 @@ type FeedbacksService interface { Save(req request.FeedbacksCreateRequest, authToken string) (feedbacks *entity.Feedbacks, err error) Update(id uint, req request.FeedbacksUpdateRequest) (err error) Delete(id uint) error + FeedbackMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.FeedbacksMonthlyStats, err error) } // NewFeedbacksService init FeedbacksService @@ -78,3 +79,21 @@ func (_i *feedbacksService) Delete(id uint) error { result.IsActive = false return _i.Repo.Update(id, result) } + +func (_i *feedbacksService) FeedbackMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.FeedbacksMonthlyStats, err error) { + //user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + + //var userLevelId *uint + //var userLevelNumber *int + // + //if user != nil { + // userLevelId = &user.UserLevelId + // userLevelNumber = &user.UserLevel.LevelNumber + //} + + result, err := _i.Repo.FeedbacksMonthlyStats(*year) + if err != nil { + return nil, err + } + return result, nil +} diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 7a6841a..0844479 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -489,6 +489,65 @@ const docTemplate = `{ } } }, + "/advertisement/upload/{id}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for Upload File Advertisement", + "produces": [ + "application/json" + ], + "tags": [ + "Advertisement" + ], + "summary": "Upload Advertisement", + "parameters": [ + { + "type": "file", + "description": "Upload file", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "Advertisement ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/advertisement/{id}": { "get": { "security": [ @@ -4618,6 +4677,62 @@ const docTemplate = `{ } } }, + "/feedbacks/statistic/monthly": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for FeedbackMonthlyStats of Feedbacks", + "tags": [ + "Articles" + ], + "summary": "FeedbackMonthlyStats Feedbacks", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "year", + "name": "year", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/feedbacks/{id}": { "get": { "security": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 8bd3078..a66ed62 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -478,6 +478,65 @@ } } }, + "/advertisement/upload/{id}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for Upload File Advertisement", + "produces": [ + "application/json" + ], + "tags": [ + "Advertisement" + ], + "summary": "Upload Advertisement", + "parameters": [ + { + "type": "file", + "description": "Upload file", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "Advertisement ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/advertisement/{id}": { "get": { "security": [ @@ -4607,6 +4666,62 @@ } } }, + "/feedbacks/statistic/monthly": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for FeedbackMonthlyStats of Feedbacks", + "tags": [ + "Articles" + ], + "summary": "FeedbackMonthlyStats Feedbacks", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "year", + "name": "year", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/feedbacks/{id}": { "get": { "security": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 9b54328..cd1006c 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -1301,6 +1301,44 @@ paths: summary: update Advertisement tags: - Advertisement + /advertisement/upload/{id}: + post: + description: API for Upload File Advertisement + parameters: + - description: Upload file + in: formData + name: file + required: true + type: file + - description: Advertisement ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Upload Advertisement + tags: + - Advertisement /article-approvals: get: description: API for getting all ArticleApprovals @@ -3911,6 +3949,42 @@ paths: summary: update Feedbacks tags: - Feedbacks + /feedbacks/statistic/monthly: + get: + description: API for FeedbackMonthlyStats of Feedbacks + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: year + in: query + name: year + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: FeedbackMonthlyStats Feedbacks + tags: + - Articles /magazine-files: get: description: API for getting all MagazineFiles