qudoco-be/app/module/articles/repository/articles.repository.go

519 lines
17 KiB
Go

package repository
import (
"fmt"
"strings"
"time"
"web-qudo-be/app/database"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/articles/request"
"web-qudo-be/app/module/articles/response"
"web-qudo-be/utils/paginator"
utilSvc "web-qudo-be/utils/service"
"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)
FindBySlug(clientId *uuid.UUID, slug string) (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{})
_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)
// Add client filter
if clientId != nil {
query = query.Where("articles.client_id = ?", clientId)
}
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)
}
if req.Source != nil && *req.Source != "" {
source := strings.ToLower(*req.Source)
query = query.Where("LOWER(articles.source) = ?", strings.ToLower(source))
}
if req.CustomCreatorName != nil && *req.CustomCreatorName != "" {
customCreatorName := strings.ToLower(*req.CustomCreatorName)
query = query.Where("LOWER(articles.custom_creator_name) LIKE ?", "%"+strings.ToLower(customCreatorName)+"%")
}
if req.StartDate != nil {
query = query.Where("DATE(articles.created_at) >= ?", req.StartDate.Format("2006-01-02"))
}
if req.EndDate != nil {
query = query.Where("DATE(articles.created_at) <= ?", req.EndDate.Format("2006-01-02"))
}
// 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 and is_publish = false")
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) FindBySlug(clientId *uuid.UUID, slug string) (articles *entity.Articles, err error) {
query := _i.DB.DB.Where("slug = ?", slug)
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.UpdatedAt = 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
}