491 lines
16 KiB
Go
491 lines
16 KiB
Go
package repository
|
|
|
|
import (
|
|
"fmt"
|
|
"netidhub-saas-be/app/database"
|
|
"netidhub-saas-be/app/database/entity"
|
|
"netidhub-saas-be/app/module/articles/request"
|
|
"netidhub-saas-be/app/module/articles/response"
|
|
"netidhub-saas-be/utils/paginator"
|
|
utilSvc "netidhub-saas-be/utils/service"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
type articlesRepository struct {
|
|
DB *database.Database
|
|
Log zerolog.Logger
|
|
}
|
|
|
|
// ArticlesRepository define interface of IArticlesRepository
|
|
type ArticlesRepository interface {
|
|
GetAll(clientId *uuid.UUID, userLevelId *uint, req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error)
|
|
GetAllPublishSchedule(clientId *uuid.UUID) (articless []*entity.Articles, err error)
|
|
FindOne(clientId *uuid.UUID, id uint) (articles *entity.Articles, err error)
|
|
FindByFilename(clientId *uuid.UUID, thumbnailName string) (articleReturn *entity.Articles, err error)
|
|
FindByOldId(clientId *uuid.UUID, oldId uint) (articles *entity.Articles, err error)
|
|
Create(clientId *uuid.UUID, articles *entity.Articles) (articleReturn *entity.Articles, err error)
|
|
Update(clientId *uuid.UUID, id uint, articles *entity.Articles) (err error)
|
|
UpdateSkipNull(clientId *uuid.UUID, id uint, articles *entity.Articles) (err error)
|
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
|
SummaryStats(clientId *uuid.UUID, userID uint) (articleSummaryStats *response.ArticleSummaryStats, err error)
|
|
ArticlePerUserLevelStats(clientId *uuid.UUID, userLevelId *uint, levelNumber *int, startDate *time.Time, endDate *time.Time) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error)
|
|
ArticleMonthlyStats(clientId *uuid.UUID, userLevelId *uint, levelNumber *int, year int) (articleMontlyStats []*response.ArticleMonthlyStats, err error)
|
|
}
|
|
|
|
func NewArticlesRepository(db *database.Database, log zerolog.Logger) ArticlesRepository {
|
|
return &articlesRepository{
|
|
DB: db,
|
|
Log: log,
|
|
}
|
|
}
|
|
|
|
// implement interface of IArticlesRepository
|
|
func (_i *articlesRepository) GetAll(clientId *uuid.UUID, userLevelId *uint, req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error) {
|
|
var count int64
|
|
|
|
query := _i.DB.DB.Model(&entity.Articles{})
|
|
|
|
// Add client filter
|
|
if clientId != nil {
|
|
query = query.Where("client_id = ?", clientId)
|
|
}
|
|
|
|
_i.Log.Info().Interface("userLevelId", userLevelId).Msg("")
|
|
// Add approval workflow filtering based on user level
|
|
if userLevelId != nil {
|
|
// Strict filtering logic for article visibility based on approval workflow
|
|
query = query.Where(`
|
|
(
|
|
-- Articles that don't require approval (bypass or exempt)
|
|
(bypass_approval = true OR approval_exempt = true)
|
|
OR
|
|
-- Articles that are published AND approved through workflow
|
|
(is_publish = true AND status_id = 2)
|
|
OR
|
|
-- Articles created by users at HIGHER hierarchy only (not same or lower)
|
|
EXISTS (
|
|
SELECT 1 FROM users u
|
|
JOIN user_levels ul ON u.user_level_id = ul.id
|
|
WHERE u.id = articles.created_by_id
|
|
AND ul.level_number < (
|
|
SELECT ul2.level_number FROM user_levels ul2 WHERE ul2.id = ?
|
|
)
|
|
)
|
|
OR
|
|
-- Articles where this user level is the CURRENT approver in the workflow
|
|
(
|
|
workflow_id IS NOT NULL
|
|
AND EXISTS (
|
|
SELECT 1 FROM article_approval_flows aaf
|
|
JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id
|
|
WHERE aaf.article_id = articles.id
|
|
AND aaf.status_id = 1 -- Only in progress
|
|
AND aws.required_user_level_id = ?
|
|
AND aws.step_order = aaf.current_step -- Must be current step
|
|
)
|
|
)
|
|
OR
|
|
-- Articles that have been approved by this user level
|
|
(
|
|
workflow_id IS NOT NULL
|
|
AND EXISTS (
|
|
SELECT 1 FROM article_approval_flows aaf
|
|
JOIN article_approval_step_logs aasl ON aaf.id = aasl.approval_flow_id
|
|
WHERE aaf.article_id = articles.id
|
|
AND aasl.user_level_id = ?
|
|
AND aasl.action = 'approve'
|
|
)
|
|
)
|
|
)
|
|
`, *userLevelId, *userLevelId, *userLevelId)
|
|
}
|
|
|
|
if req.CategoryId != nil {
|
|
query = query.Joins("JOIN article_category_details acd ON acd.article_id = articles.id").
|
|
Where("acd.category_id = ?", req.CategoryId)
|
|
}
|
|
query = query.Where("articles.is_active = ?", true)
|
|
|
|
if req.Title != nil && *req.Title != "" {
|
|
title := strings.ToLower(*req.Title)
|
|
query = query.Where("LOWER(articles.title) LIKE ?", "%"+strings.ToLower(title)+"%")
|
|
}
|
|
if req.Description != nil && *req.Description != "" {
|
|
description := strings.ToLower(*req.Description)
|
|
query = query.Where("LOWER(articles.description) LIKE ?", "%"+strings.ToLower(description)+"%")
|
|
}
|
|
if req.Tags != nil && *req.Tags != "" {
|
|
tags := strings.ToLower(*req.Tags)
|
|
query = query.Where("LOWER(articles.tags) LIKE ?", "%"+strings.ToLower(tags)+"%")
|
|
}
|
|
if req.TypeId != nil {
|
|
query = query.Where("articles.type_id = ?", req.TypeId)
|
|
}
|
|
if req.IsPublish != nil {
|
|
query = query.Where("articles.is_publish = ?", req.IsPublish)
|
|
}
|
|
if req.IsBanner != nil {
|
|
query = query.Where("articles.is_banner = ?", req.IsBanner)
|
|
}
|
|
if req.IsDraft != nil {
|
|
query = query.Where("articles.is_draft = ?", req.IsDraft)
|
|
}
|
|
if req.StatusId != nil {
|
|
query = query.Where("articles.status_id = ?", req.StatusId)
|
|
}
|
|
if req.CreatedById != nil {
|
|
query = query.Where("articles.created_by_id = ?", req.CreatedById)
|
|
}
|
|
// Count total records
|
|
query.Count(&count)
|
|
|
|
// Apply sorting
|
|
if req.Pagination.SortBy != "" {
|
|
direction := "ASC"
|
|
if req.Pagination.Sort == "desc" {
|
|
direction = "DESC"
|
|
}
|
|
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
|
|
} else {
|
|
direction := "DESC"
|
|
sortBy := "articles.created_at"
|
|
query.Order(fmt.Sprintf("%s %s", sortBy, direction))
|
|
}
|
|
|
|
// Apply pagination (manual calculation for better performance)
|
|
page := req.Pagination.Page
|
|
limit := req.Pagination.Limit
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
|
|
offset := (page - 1) * limit
|
|
err = query.Offset(offset).Limit(limit).Find(&articless).Error
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Create pagination response
|
|
paging = paginator.Pagination{
|
|
Page: page,
|
|
Limit: limit,
|
|
Count: count,
|
|
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (_i *articlesRepository) GetAllPublishSchedule(clientId *uuid.UUID) (articles []*entity.Articles, err error) {
|
|
query := _i.DB.DB.Where("publish_schedule IS NOT NULL")
|
|
if clientId != nil {
|
|
query = query.Where("client_id = ?", clientId)
|
|
}
|
|
err = query.Find(&articles).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return articles, nil
|
|
}
|
|
|
|
func (_i *articlesRepository) FindOne(clientId *uuid.UUID, id uint) (articles *entity.Articles, err error) {
|
|
query := _i.DB.DB
|
|
if clientId != nil {
|
|
query = query.Where("client_id = ?", clientId)
|
|
}
|
|
if err := query.First(&articles, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return articles, nil
|
|
}
|
|
|
|
func (_i *articlesRepository) FindByFilename(clientId *uuid.UUID, thumbnailName string) (articles *entity.Articles, err error) {
|
|
query := _i.DB.DB.Where("thumbnail_name = ?", thumbnailName)
|
|
if clientId != nil {
|
|
query = query.Where("client_id = ?", clientId)
|
|
}
|
|
if err := query.First(&articles).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return articles, nil
|
|
}
|
|
|
|
func (_i *articlesRepository) FindByOldId(clientId *uuid.UUID, oldId uint) (articles *entity.Articles, err error) {
|
|
query := _i.DB.DB.Where("old_id = ?", oldId)
|
|
if clientId != nil {
|
|
query = query.Where("client_id = ?", clientId)
|
|
}
|
|
if err := query.First(&articles).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return articles, nil
|
|
}
|
|
|
|
func (_i *articlesRepository) Create(clientId *uuid.UUID, articles *entity.Articles) (articleReturn *entity.Articles, err error) {
|
|
// Set client ID
|
|
if clientId != nil {
|
|
articles.ClientId = clientId
|
|
}
|
|
|
|
result := _i.DB.DB.Create(articles)
|
|
return articles, result.Error
|
|
}
|
|
|
|
func (_i *articlesRepository) Update(clientId *uuid.UUID, id uint, articles *entity.Articles) (err error) {
|
|
// Validate client access
|
|
if clientId != nil {
|
|
var count int64
|
|
if err := _i.DB.DB.Model(&entity.Articles{}).Where("id = ? AND client_id = ?", id, clientId).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return fmt.Errorf("access denied to this resource")
|
|
}
|
|
}
|
|
|
|
articlesMap, err := utilSvc.StructToMap(articles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove fields that could cause foreign key constraint violations
|
|
// delete(articlesMap, "workflow_id")
|
|
// delete(articlesMap, "id")
|
|
// delete(articlesMap, "created_at")
|
|
|
|
return _i.DB.DB.Model(&entity.Articles{}).
|
|
Where(&entity.Articles{ID: id}).
|
|
Updates(articlesMap).Error
|
|
}
|
|
|
|
func (_i *articlesRepository) UpdateSkipNull(clientId *uuid.UUID, id uint, articles *entity.Articles) (err error) {
|
|
// Validate client access
|
|
if clientId != nil {
|
|
var count int64
|
|
if err := _i.DB.DB.Model(&entity.Articles{}).Where("id = ? AND client_id = ?", id, clientId).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return fmt.Errorf("access denied to this resource")
|
|
}
|
|
}
|
|
|
|
// Create a copy to avoid modifying the original struct
|
|
updateData := *articles
|
|
// Clear fields that could cause foreign key constraint violations
|
|
updateData.WorkflowId = nil
|
|
updateData.ID = 0
|
|
updateData.CreatedAt = time.Time{}
|
|
|
|
return _i.DB.DB.Model(&entity.Articles{}).
|
|
Where(&entity.Articles{ID: id}).
|
|
Updates(&updateData).Error
|
|
}
|
|
|
|
func (_i *articlesRepository) Delete(clientId *uuid.UUID, id uint) error {
|
|
// Validate client access
|
|
if clientId != nil {
|
|
var count int64
|
|
if err := _i.DB.DB.Model(&entity.Articles{}).Where("id = ? AND client_id = ?", id, clientId).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return fmt.Errorf("access denied to this resource")
|
|
}
|
|
}
|
|
|
|
// Use soft delete by setting is_active to false
|
|
isActive := false
|
|
return _i.DB.DB.Model(&entity.Articles{}).Where("id = ?", id).Update("is_active", isActive).Error
|
|
}
|
|
|
|
func (_i *articlesRepository) SummaryStats(clientId *uuid.UUID, userID uint) (articleSummaryStats *response.ArticleSummaryStats, err error) {
|
|
now := time.Now()
|
|
startOfDay := now.Truncate(24 * time.Hour)
|
|
startOfWeek := now.AddDate(0, 0, -int(now.Weekday())+1).Truncate(24 * time.Hour)
|
|
|
|
// Query
|
|
query := _i.DB.DB.Model(&entity.Articles{}).
|
|
Select(
|
|
"COUNT(*) AS total_all, "+
|
|
"COALESCE(SUM(view_count), 0) AS total_views, "+
|
|
"COALESCE(SUM(share_count), 0) AS total_shares, "+
|
|
"COALESCE(SUM(comment_count), 0) AS total_comments, "+
|
|
"COUNT(CASE WHEN created_at >= ? THEN 1 END) AS total_today, "+
|
|
"COUNT(CASE WHEN created_at >= ? THEN 1 END) AS total_this_week",
|
|
startOfDay, startOfWeek,
|
|
).
|
|
Where("created_by_id = ?", userID)
|
|
|
|
// Add client filter
|
|
if clientId != nil {
|
|
query = query.Where("client_id = ?", clientId)
|
|
}
|
|
|
|
err = query.Scan(&articleSummaryStats).Error
|
|
|
|
return articleSummaryStats, err
|
|
}
|
|
|
|
func (_i *articlesRepository) ArticlePerUserLevelStats(clientId *uuid.UUID, userLevelId *uint, levelNumber *int, startDate *time.Time, endDate *time.Time) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) {
|
|
|
|
levelNumberTop := 1
|
|
|
|
query := _i.DB.DB.Model(&entity.Articles{}).
|
|
Select("user_levels.id as user_level_id", "user_levels.name as user_level_name", "COUNT(articles.id) as total_article").
|
|
Joins("LEFT JOIN users ON articles.created_by_id = users.id").
|
|
Joins("LEFT JOIN user_levels ON users.user_level_id = user_levels.id").
|
|
Where("articles.is_active = true")
|
|
|
|
// Add client filter
|
|
if clientId != nil {
|
|
query = query.Where("articles.client_id = ?", clientId)
|
|
}
|
|
|
|
if userLevelId != nil && *levelNumber != levelNumberTop {
|
|
query = query.Where("user_levels.id = ? or user_levels.parent_level_id = ?", *userLevelId, *userLevelId)
|
|
} else {
|
|
query = _i.DB.DB.Raw(`
|
|
WITH LevelHierarchy AS (
|
|
SELECT
|
|
id,
|
|
name,
|
|
level_number,
|
|
parent_level_id,
|
|
CASE
|
|
WHEN level_number = 1 THEN id
|
|
WHEN level_number = 2 and name ILIKE '%polda%' THEN id
|
|
WHEN level_number = 2 and name NOT ILIKE '%polda%' THEN parent_level_id
|
|
WHEN level_number = 3 THEN parent_level_id
|
|
END AS level_2_id,
|
|
CASE
|
|
WHEN level_number = 1 THEN name
|
|
WHEN level_number = 2 and name ILIKE '%polda%' THEN name
|
|
WHEN level_number = 2 and name NOT ILIKE '%polda%' THEN (SELECT name FROM user_levels ul2 WHERE ul2.id = user_levels.parent_level_id)
|
|
WHEN level_number = 3 THEN (SELECT name FROM user_levels ul2 WHERE ul2.id = user_levels.parent_level_id)
|
|
END AS level_2_name
|
|
FROM user_levels
|
|
)
|
|
SELECT
|
|
lh.level_2_id AS user_level_id,
|
|
UPPER(lh.level_2_name) AS user_level_name,
|
|
COUNT(articles.id) AS total_article
|
|
FROM articles
|
|
JOIN users ON articles.created_by_id = users.id
|
|
JOIN LevelHierarchy lh ON users.user_level_id = lh.id
|
|
WHERE articles.is_active = true AND lh.level_2_id > 0`)
|
|
|
|
// Add client filter to raw query
|
|
if clientId != nil {
|
|
query = query.Where("articles.client_id = ?", clientId)
|
|
}
|
|
|
|
query = query.Group("lh.level_2_id, lh.level_2_name").Order("total_article DESC")
|
|
}
|
|
|
|
// Apply date filters if provided
|
|
if startDate != nil {
|
|
query = query.Where("articles.created_at >= ?", *startDate)
|
|
}
|
|
if endDate != nil {
|
|
query = query.Where("articles.created_at <= ?", *endDate)
|
|
}
|
|
|
|
// Group by all non-aggregated columns
|
|
err = query.Group("user_levels.id, user_levels.name").
|
|
Order("total_article DESC").
|
|
Scan(&articlePerUserLevelStats).Error
|
|
|
|
return articlePerUserLevelStats, err
|
|
}
|
|
|
|
func (_i *articlesRepository) ArticleMonthlyStats(clientId *uuid.UUID, userLevelId *uint, levelNumber *int, year int) (articleMontlyStats []*response.ArticleMonthlyStats, err error) {
|
|
levelNumberTop := 1
|
|
|
|
if year < 1900 || year > 2100 {
|
|
return nil, fmt.Errorf("invalid year")
|
|
}
|
|
|
|
var results []struct {
|
|
Month int
|
|
Day int
|
|
TotalView int
|
|
TotalComment int
|
|
TotalShare int
|
|
}
|
|
|
|
query := _i.DB.DB.Model(&entity.Articles{}).
|
|
Select("EXTRACT(MONTH FROM created_at) as month, EXTRACT(DAY FROM created_at) as day, "+
|
|
"SUM(view_count) as total_view, "+
|
|
"SUM(comment_count) as total_comment, "+
|
|
"SUM(share_count) as total_share").
|
|
Where("EXTRACT(YEAR FROM created_at) = ?", year)
|
|
|
|
// Add client filter
|
|
if clientId != nil {
|
|
query = query.Where("client_id = ?", clientId)
|
|
}
|
|
|
|
if userLevelId != nil && *levelNumber != levelNumberTop {
|
|
query = _i.DB.DB.Model(&entity.Articles{}).
|
|
Select("EXTRACT(MONTH FROM articles.created_at) as month, EXTRACT(DAY FROM articles.created_at) as day, "+
|
|
"SUM(articles.view_count) as total_view, "+
|
|
"SUM(articles.comment_count) as total_comment, "+
|
|
"SUM(articles.share_count) as total_share").
|
|
Joins("LEFT JOIN users ON articles.created_by_id = users.id").
|
|
Joins("LEFT JOIN user_levels ON users.user_level_id = user_levels.id").
|
|
Where("articles.is_active = true").
|
|
Where("EXTRACT(YEAR FROM articles.created_at) = ?", year).
|
|
Where("(user_levels.id = ? OR user_levels.parent_level_id = ?)", *userLevelId, *userLevelId)
|
|
|
|
// Add client filter
|
|
if clientId != nil {
|
|
query = query.Where("articles.client_id = ?", clientId)
|
|
}
|
|
}
|
|
|
|
err = query.Group("month, day").Scan(&results).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Siapkan struktur untuk menyimpan data bulanan
|
|
monthlyAnalytics := make([]*response.ArticleMonthlyStats, 12)
|
|
for i := 0; i < 12; i++ {
|
|
daysInMonth := time.Date(year, time.Month(i+1), 0, 0, 0, 0, 0, time.UTC).Day()
|
|
monthlyAnalytics[i] = &response.ArticleMonthlyStats{
|
|
Year: year,
|
|
Month: i + 1,
|
|
View: make([]int, daysInMonth),
|
|
Comment: make([]int, daysInMonth),
|
|
Share: 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(monthlyAnalytics[monthIndex].View) {
|
|
monthlyAnalytics[monthIndex].View[dayIndex] = result.TotalView
|
|
monthlyAnalytics[monthIndex].Comment[dayIndex] = result.TotalComment
|
|
monthlyAnalytics[monthIndex].Share[dayIndex] = result.TotalShare
|
|
}
|
|
}
|
|
}
|
|
|
|
return monthlyAnalytics, nil
|
|
}
|