feat: update feedback analytics and advertisement upload

This commit is contained in:
hanif salafi 2025-03-28 16:24:29 +07:00
parent bbce558ce5
commit 4f3e92808b
12 changed files with 558 additions and 16 deletions

View File

@ -3,13 +3,15 @@ package entity
import "time" import "time"
type Advertisement struct { type Advertisement struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"` Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"` Description string `json:"description" gorm:"type:varchar"`
RedirectLink string `json:"redirect_link" gorm:"type:varchar"` RedirectLink string `json:"redirect_link" gorm:"type:varchar"`
Placement string `json:"placement" gorm:"type:varchar"` ContentFilePath *string `json:"content_file_path" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4"` ContentFileName *string `json:"content_file_name" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"` Placement string `json:"placement" gorm:"type:varchar"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` StatusId int `json:"status_id" gorm:"type:int4"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` 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()"`
} }

View File

@ -47,6 +47,7 @@ func (_i *AdvertisementRouter) RegisterAdvertisementRoutes() {
router.Get("/", advertisementController.All) router.Get("/", advertisementController.All)
router.Get("/:id", advertisementController.Show) router.Get("/:id", advertisementController.Show)
router.Post("/", advertisementController.Save) router.Post("/", advertisementController.Save)
router.Post("/upload/:id", advertisementController.Upload)
router.Put("/:id", advertisementController.Update) router.Put("/:id", advertisementController.Update)
router.Delete("/:id", advertisementController.Delete) router.Delete("/:id", advertisementController.Delete)
}) })

View File

@ -21,6 +21,7 @@ type AdvertisementController interface {
All(c *fiber.Ctx) error All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error Save(c *fiber.Ctx) error
Upload(c *fiber.Ctx) error
Update(c *fiber.Ctx) error Update(c *fiber.Ctx) error
Delete(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 // Update update Advertisement
// @Summary update Advertisement // @Summary update Advertisement
// @Description API for update Advertisement // @Description API for update Advertisement

View File

@ -1,6 +1,10 @@
package service package service
import ( import (
"context"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go-humas-be/app/database/entity" "go-humas-be/app/database/entity"
"go-humas-be/app/module/advertisement/mapper" "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/request"
"go-humas-be/app/module/advertisement/response" "go-humas-be/app/module/advertisement/response"
usersRepository "go-humas-be/app/module/users/repository" usersRepository "go-humas-be/app/module/users/repository"
minioStorage "go-humas-be/config/config"
"go-humas-be/utils/paginator" "go-humas-be/utils/paginator"
"math/rand"
"path/filepath"
"strconv"
"strings"
"time"
) )
// AdvertisementService // AdvertisementService
type advertisementService struct { type advertisementService struct {
Repo repository.AdvertisementRepository Repo repository.AdvertisementRepository
UsersRepo usersRepository.UsersRepository UsersRepo usersRepository.UsersRepository
Log zerolog.Logger Log zerolog.Logger
MinioStorage *minioStorage.MinioStorage
} }
// AdvertisementService define interface of IAdvertisementService // AdvertisementService define interface of IAdvertisementService
@ -23,17 +34,19 @@ type AdvertisementService interface {
All(req request.AdvertisementQueryRequest) (advertisement []*response.AdvertisementResponse, paging paginator.Pagination, err error) All(req request.AdvertisementQueryRequest) (advertisement []*response.AdvertisementResponse, paging paginator.Pagination, err error)
Show(id uint) (advertisement *response.AdvertisementResponse, err error) Show(id uint) (advertisement *response.AdvertisementResponse, err error)
Save(req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, 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) Update(id uint, req request.AdvertisementUpdateRequest) (err error)
Delete(id uint) error Delete(id uint) error
} }
// NewAdvertisementService init AdvertisementService // 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{ return &advertisementService{
Repo: repo, Repo: repo,
Log: log, UsersRepo: usersRepo,
UsersRepo: usersRepo, MinioStorage: minioStorage,
Log: log,
} }
} }
@ -66,6 +79,87 @@ func (_i *advertisementService) Save(req request.AdvertisementCreateRequest) (ad
return _i.Repo.Create(newReq) 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) { func (_i *advertisementService) Update(id uint, req request.AdvertisementUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("") _i.Log.Info().Interface("data", req).Msg("")
return _i.Repo.Update(id, req.ToEntity()) return _i.Repo.Update(id, req.ToEntity())

View File

@ -22,6 +22,7 @@ type FeedbacksController interface {
Save(c *fiber.Ctx) error Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error Delete(c *fiber.Ctx) error
FeedbackMonthlyStats(c *fiber.Ctx) error
} }
func NewFeedbacksController(feedbacksService service.FeedbacksService, log zerolog.Logger) FeedbacksController { 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"}, 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 <Add access token here>)
// @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,
})
}

View File

@ -49,5 +49,6 @@ func (_i *FeedbacksRouter) RegisterFeedbacksRoutes() {
router.Post("/", feedbacksController.Save) router.Post("/", feedbacksController.Save)
router.Put("/:id", feedbacksController.Update) router.Put("/:id", feedbacksController.Update)
router.Delete("/:id", feedbacksController.Delete) router.Delete("/:id", feedbacksController.Delete)
router.Get("/statistic/monthly", feedbacksController.FeedbackMonthlyStats)
}) })
} }

View File

@ -6,8 +6,10 @@ import (
"go-humas-be/app/database" "go-humas-be/app/database"
"go-humas-be/app/database/entity" "go-humas-be/app/database/entity"
"go-humas-be/app/module/feedbacks/request" "go-humas-be/app/module/feedbacks/request"
"go-humas-be/app/module/feedbacks/response"
"go-humas-be/utils/paginator" "go-humas-be/utils/paginator"
"strings" "strings"
"time"
) )
type feedbacksRepository struct { type feedbacksRepository struct {
@ -22,6 +24,7 @@ type FeedbacksRepository interface {
Create(feedbacks *entity.Feedbacks) (feedbacksReturn *entity.Feedbacks, err error) Create(feedbacks *entity.Feedbacks) (feedbacksReturn *entity.Feedbacks, err error)
Update(id uint, feedbacks *entity.Feedbacks) (err error) Update(id uint, feedbacks *entity.Feedbacks) (err error)
Delete(id uint) (err error) Delete(id uint) (err error)
FeedbacksMonthlyStats(year int) (feedbacksMonthlyStats []*response.FeedbacksMonthlyStats, err error)
} }
func NewFeedbacksRepository(db *database.Database, logger zerolog.Logger) FeedbacksRepository { 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 { func (_i *feedbacksRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.Feedbacks{}, id).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
}

View File

@ -14,3 +14,9 @@ type FeedbacksResponse struct {
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
type FeedbacksMonthlyStats struct {
Year int `json:"year"`
Month int `json:"month"`
Suggestions []int `json:"suggestions"`
}

View File

@ -25,6 +25,7 @@ type FeedbacksService interface {
Save(req request.FeedbacksCreateRequest, authToken string) (feedbacks *entity.Feedbacks, err error) Save(req request.FeedbacksCreateRequest, authToken string) (feedbacks *entity.Feedbacks, err error)
Update(id uint, req request.FeedbacksUpdateRequest) (err error) Update(id uint, req request.FeedbacksUpdateRequest) (err error)
Delete(id uint) error Delete(id uint) error
FeedbackMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.FeedbacksMonthlyStats, err error)
} }
// NewFeedbacksService init FeedbacksService // NewFeedbacksService init FeedbacksService
@ -78,3 +79,21 @@ func (_i *feedbacksService) Delete(id uint) error {
result.IsActive = false result.IsActive = false
return _i.Repo.Update(id, result) 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
}

View File

@ -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}": { "/advertisement/{id}": {
"get": { "get": {
"security": [ "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}": { "/feedbacks/{id}": {
"get": { "get": {
"security": [ "security": [

View File

@ -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}": { "/advertisement/{id}": {
"get": { "get": {
"security": [ "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}": { "/feedbacks/{id}": {
"get": { "get": {
"security": [ "security": [

View File

@ -1301,6 +1301,44 @@ paths:
summary: update Advertisement summary: update Advertisement
tags: tags:
- Advertisement - 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: /article-approvals:
get: get:
description: API for getting all ArticleApprovals description: API for getting all ArticleApprovals
@ -3911,6 +3949,42 @@ paths:
summary: update Feedbacks summary: update Feedbacks
tags: tags:
- Feedbacks - Feedbacks
/feedbacks/statistic/monthly:
get:
description: API for FeedbackMonthlyStats of Feedbacks
parameters:
- default: Bearer <Add access token here>
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: /magazine-files:
get: get:
description: API for getting all MagazineFiles description: API for getting all MagazineFiles