kontenhumas-be/plan/implementation-examples.md

36 KiB

Contoh Implementasi Kode - Sistem Approval Artikel Dinamis

1. Database Entities

ApprovalWorkflows Entity

// app/database/entity/approval_workflows.entity.go
package entity

import (
	"github.com/google/uuid"
	"time"
)

type ApprovalWorkflows struct {
	ID          uint       `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
	Name        string     `json:"name" gorm:"type:varchar;not null"`
	Description *string    `json:"description" gorm:"type:text"`
	IsActive    *bool      `json:"is_active" gorm:"type:bool;default:true"`
	ClientId    *uuid.UUID `json:"client_id" gorm:"type:UUID"`
	CreatedAt   time.Time  `json:"created_at" gorm:"default:now()"`
	UpdatedAt   time.Time  `json:"updated_at" gorm:"default:now()"`
	
	// Relations
	Steps []ApprovalWorkflowSteps `json:"steps" gorm:"foreignKey:WorkflowId;references:ID"`
}

ApprovalWorkflowSteps Entity

// app/database/entity/approval_workflow_steps.entity.go
package entity

import (
	"github.com/google/uuid"
	"time"
)

type ApprovalWorkflowSteps struct {
	ID          uint       `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
	WorkflowId  uint       `json:"workflow_id" gorm:"type:int4;not null"`
	StepOrder   int        `json:"step_order" gorm:"type:int4;not null"`
	UserLevelId uint       `json:"user_level_id" gorm:"type:int4;not null"`
	IsRequired  *bool      `json:"is_required" gorm:"type:bool;default:true"`
	CanSkip     *bool      `json:"can_skip" gorm:"type:bool;default:false"`
	ClientId    *uuid.UUID `json:"client_id" gorm:"type:UUID"`
	CreatedAt   time.Time  `json:"created_at" gorm:"default:now()"`
	UpdatedAt   time.Time  `json:"updated_at" gorm:"default:now()"`
	
	// Relations
	Workflow  *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;references:ID"`
	UserLevel *UserLevels        `json:"user_level" gorm:"foreignKey:UserLevelId;references:ID"`
}

ArticleApprovalFlows Entity

// app/database/entity/article_approval_flows.entity.go
package entity

import (
	"github.com/google/uuid"
	"time"
)

type ArticleApprovalFlows struct {
	ID          uint       `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
	ArticleId   uint       `json:"article_id" gorm:"type:int4;not null"`
	WorkflowId  uint       `json:"workflow_id" gorm:"type:int4;not null"`
	CurrentStep int        `json:"current_step" gorm:"type:int4;default:1"`
	StatusId    int        `json:"status_id" gorm:"type:int4;default:1"` // 1=In Progress, 2=Completed, 3=Rejected
	ClientId    *uuid.UUID `json:"client_id" gorm:"type:UUID"`
	CreatedAt   time.Time  `json:"created_at" gorm:"default:now()"`
	UpdatedAt   time.Time  `json:"updated_at" gorm:"default:now()"`
	
	// Relations
	Article  *Articles          `json:"article" gorm:"foreignKey:ArticleId;references:ID"`
	Workflow *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;references:ID"`
	StepLogs []ArticleApprovalStepLogs `json:"step_logs" gorm:"foreignKey:ArticleFlowId;references:ID"`
}

ArticleApprovalStepLogs Entity

// app/database/entity/article_approval_step_logs.entity.go
package entity

import (
	"github.com/google/uuid"
	"time"
	users "netidhub-saas-be/app/database/entity/users"
)

type ArticleApprovalStepLogs struct {
	ID            uint       `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
	ArticleFlowId uint       `json:"article_flow_id" gorm:"type:int4;not null"`
	StepOrder     int        `json:"step_order" gorm:"type:int4;not null"`
	UserLevelId   uint       `json:"user_level_id" gorm:"type:int4;not null"`
	ApprovedBy    uint       `json:"approved_by" gorm:"type:int4;not null"`
	Action        string     `json:"action" gorm:"type:varchar(50);not null"` // 'approved', 'rejected', 'revision_requested'
	Message       *string    `json:"message" gorm:"type:text"`
	ApprovedAt    *time.Time `json:"approved_at" gorm:"type:timestamp"`
	ClientId      *uuid.UUID `json:"client_id" gorm:"type:UUID"`
	CreatedAt     time.Time  `json:"created_at" gorm:"default:now()"`
	
	// Relations
	ArticleFlow *ArticleApprovalFlows `json:"article_flow" gorm:"foreignKey:ArticleFlowId;references:ID"`
	UserLevel   *UserLevels           `json:"user_level" gorm:"foreignKey:UserLevelId;references:ID"`
	Approver    *users.Users          `json:"approver" gorm:"foreignKey:ApprovedBy;references:ID"`
}

2. Service Layer Implementation

ApprovalWorkflowService

// app/module/approval_workflows/service/approval_workflows.service.go
package service

import (
	"errors"
	"github.com/google/uuid"
	"github.com/rs/zerolog"
	"netidhub-saas-be/app/database/entity"
	"netidhub-saas-be/app/module/approval_workflows/repository"
	"netidhub-saas-be/app/module/approval_workflows/request"
	"netidhub-saas-be/app/module/approval_workflows/response"
	"netidhub-saas-be/utils/paginator"
)

type approvalWorkflowService struct {
	Repo repository.ApprovalWorkflowRepository
	Log  zerolog.Logger
}

type ApprovalWorkflowService interface {
	All(clientId *uuid.UUID, req request.ApprovalWorkflowQueryRequest) ([]*response.ApprovalWorkflowResponse, paginator.Pagination, error)
	Show(clientId *uuid.UUID, id uint) (*response.ApprovalWorkflowResponse, error)
	Save(clientId *uuid.UUID, req request.ApprovalWorkflowCreateRequest) (*entity.ApprovalWorkflows, error)
	Update(clientId *uuid.UUID, id uint, req request.ApprovalWorkflowUpdateRequest) error
	Delete(clientId *uuid.UUID, id uint) error
	GetActiveWorkflow(clientId *uuid.UUID) (*entity.ApprovalWorkflows, error)
	ValidateWorkflow(workflowId uint) error
}

func NewApprovalWorkflowService(repo repository.ApprovalWorkflowRepository, log zerolog.Logger) ApprovalWorkflowService {
	return &approvalWorkflowService{
		Repo: repo,
		Log:  log,
	}
}

func (s *approvalWorkflowService) Save(clientId *uuid.UUID, req request.ApprovalWorkflowCreateRequest) (*entity.ApprovalWorkflows, error) {
	// Validate workflow steps
	if len(req.Steps) == 0 {
		return nil, errors.New("workflow must have at least one step")
	}
	
	// Validate step order sequence
	for i, step := range req.Steps {
		if step.StepOrder != i+1 {
			return nil, errors.New("step order must be sequential starting from 1")
		}
	}
	
	workflow := req.ToEntity()
	workflow.ClientId = clientId
	
	return s.Repo.Create(workflow)
}

func (s *approvalWorkflowService) ValidateWorkflow(workflowId uint) error {
	workflow, err := s.Repo.FindOneWithSteps(workflowId)
	if err != nil {
		return err
	}
	
	if workflow.IsActive == nil || !*workflow.IsActive {
		return errors.New("workflow is not active")
	}
	
	if len(workflow.Steps) == 0 {
		return errors.New("workflow has no steps defined")
	}
	
	return nil
}

Dynamic Article Approval Service

// app/module/article_approval_flows/service/article_approval_flows.service.go
package service

import (
	"errors"
	"fmt"
	"github.com/google/uuid"
	"github.com/rs/zerolog"
	"time"
	"netidhub-saas-be/app/database/entity"
	users "netidhub-saas-be/app/database/entity/users"
	"netidhub-saas-be/app/module/article_approval_flows/repository"
	"netidhub-saas-be/app/module/articles/repository" as articlesRepo
	"netidhub-saas-be/app/module/approval_workflows/service" as workflowService
)

type articleApprovalFlowService struct {
	Repo            repository.ArticleApprovalFlowRepository
	ArticlesRepo    articlesRepo.ArticlesRepository
	WorkflowService workflowService.ApprovalWorkflowService
	Log             zerolog.Logger
}

type ArticleApprovalFlowService interface {
	InitiateApprovalFlow(clientId *uuid.UUID, articleId uint, workflowId uint) (*entity.ArticleApprovalFlows, error)
	ProcessApprovalStep(clientId *uuid.UUID, articleId uint, action string, message *string, approver *users.Users) error
	GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, filters ApprovalFilters) ([]*entity.ArticleApprovalFlows, paginator.Pagination, error)
	GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, filters ApprovalFilters) ([]*DetailedApprovalFlow, paginator.Pagination, error)
	GetApprovalHistory(clientId *uuid.UUID, articleId uint) ([]*entity.ArticleApprovalStepLogs, error)
	GetApprovalStatistics(clientId *uuid.UUID, userLevelId uint) (*ApprovalStatistics, error)
	GetWorkloadDistribution(clientId *uuid.UUID, userLevelId uint) (*WorkloadDistribution, error)
	BulkApprovalAction(clientId *uuid.UUID, articleIds []uint, action string, message *string, approver *users.Users) (*BulkActionResult, error)
}

// Supporting structs for enhanced approver dashboard
type ApprovalFilters struct {
	Page       int
	Limit      int
	Priority   *string
	CategoryId *uint
	Search     *string
	DateFrom   *time.Time
	DateTo     *time.Time
	SortBy     string
	SortOrder  string
	WorkflowId *uint
	UrgencyOnly bool
	IncludePreview bool
}

type DetailedApprovalFlow struct {
	ID      uint                     `json:"id"`
	Article DetailedArticleInfo      `json:"article"`
	ApprovalContext ApprovalContext  `json:"approval_context"`
	WorkflowProgress WorkflowProgress `json:"workflow_progress"`
}

type DetailedArticleInfo struct {
	ID                  uint                `json:"id"`
	Title               string              `json:"title"`
	ContentPreview      *string             `json:"content_preview"`
	FullContentAvailable bool               `json:"full_content_available"`
	Author              AuthorInfo          `json:"author"`
	Category            CategoryInfo        `json:"category"`
	SubmissionNotes     *string             `json:"submission_notes"`
	Tags                []string            `json:"tags"`
	WordCount           int                 `json:"word_count"`
	EstimatedReadTime   string              `json:"estimated_read_time"`
	SEOScore            int                 `json:"seo_score"`
	ReadabilityScore    string              `json:"readability_score"`
	CreatedAt           time.Time           `json:"created_at"`
	SubmittedAt         time.Time           `json:"submitted_at"`
}

type AuthorInfo struct {
	ID                  uint    `json:"id"`
	Name                string  `json:"name"`
	Email               string  `json:"email"`
	ProfilePicture      *string `json:"profile_picture"`
	ReputationScore     float64 `json:"reputation_score"`
	ArticlesPublished   int     `json:"articles_published"`
	ApprovalSuccessRate float64 `json:"approval_success_rate"`
}

type CategoryInfo struct {
	ID    uint   `json:"id"`
	Name  string `json:"name"`
	Color string `json:"color"`
}

type ApprovalContext struct {
	MyRoleInWorkflow    string     `json:"my_role_in_workflow"`
	StepDescription     string     `json:"step_description"`
	ExpectedAction      string     `json:"expected_action"`
	Deadline            *time.Time `json:"deadline"`
	IsOverdue           bool       `json:"is_overdue"`
	EscalationAvailable bool       `json:"escalation_available"`
}

type WorkflowProgress struct {
	CompletedSteps      int     `json:"completed_steps"`
	TotalSteps          int     `json:"total_steps"`
	ProgressPercentage  float64 `json:"progress_percentage"`
	NextApprover        string  `json:"next_approver"`
}

type ApprovalStatistics struct {
	PendingCount              int                      `json:"pending_count"`
	OverdueCount              int                      `json:"overdue_count"`
	ApprovedToday             int                      `json:"approved_today"`
	ApprovedThisWeek          int                      `json:"approved_this_week"`
	ApprovedThisMonth         int                      `json:"approved_this_month"`
	RejectedThisMonth         int                      `json:"rejected_this_month"`
	RevisionRequestsThisMonth int                      `json:"revision_requests_this_month"`
	AverageApprovalTimeHours  float64                  `json:"average_approval_time_hours"`
	FastestApprovalMinutes    int                      `json:"fastest_approval_minutes"`
	SlowestApprovalHours      int                      `json:"slowest_approval_hours"`
	ApprovalRatePercentage    float64                  `json:"approval_rate_percentage"`
	CategoriesBreakdown       []CategoryBreakdown      `json:"categories_breakdown"`
}

type CategoryBreakdown struct {
	CategoryName        string `json:"category_name"`
	Pending             int    `json:"pending"`
	ApprovedThisMonth   int    `json:"approved_this_month"`
}

type WorkloadDistribution struct {
	MyLevel        LevelWorkload    `json:"my_level"`
	TeamComparison []TeamMember     `json:"team_comparison"`
	Bottlenecks    []BottleneckInfo `json:"bottlenecks"`
}

type LevelWorkload struct {
	LevelName          string  `json:"level_name"`
	LevelNumber        int     `json:"level_number"`
	PendingArticles    int     `json:"pending_articles"`
	AvgDailyApprovals  float64 `json:"avg_daily_approvals"`
}

type TeamMember struct {
	ApproverName           string  `json:"approver_name"`
	LevelName              string  `json:"level_name"`
	PendingArticles        int     `json:"pending_articles"`
	AvgResponseTimeHours   float64 `json:"avg_response_time_hours"`
}

type BottleneckInfo struct {
	LevelName            string  `json:"level_name"`
	PendingCount         int     `json:"pending_count"`
	AvgWaitingTimeHours  float64 `json:"avg_waiting_time_hours"`
	IsBottleneck         bool    `json:"is_bottleneck"`
}

type BulkActionResult struct {
	ProcessedCount  int                    `json:"processed_count"`
	SuccessfulCount int                    `json:"successful_count"`
	FailedCount     int                    `json:"failed_count"`
	Results         []BulkActionItemResult `json:"results"`
}

type BulkActionItemResult struct {
	ArticleId uint   `json:"article_id"`
	Status    string `json:"status"`
	NextStep  string `json:"next_step"`
	Error     *string `json:"error,omitempty"`
}

func NewArticleApprovalFlowService(
	repo repository.ArticleApprovalFlowRepository,
	articlesRepo articlesRepo.ArticlesRepository,
	workflowService workflowService.ApprovalWorkflowService,
	log zerolog.Logger,
) ArticleApprovalFlowService {
	return &articleApprovalFlowService{
		Repo:            repo,
		ArticlesRepo:    articlesRepo,
		WorkflowService: workflowService,
		Log:             log,
	}
}

func (s *articleApprovalFlowService) InitiateApprovalFlow(clientId *uuid.UUID, articleId uint, workflowId uint) (*entity.ArticleApprovalFlows, error) {
	// Validate workflow
	err := s.WorkflowService.ValidateWorkflow(workflowId)
	if err != nil {
		return nil, fmt.Errorf("invalid workflow: %v", err)
	}
	
	// Check if article already has active approval flow
	existingFlow, _ := s.Repo.FindActiveByArticleId(articleId)
	if existingFlow != nil {
		return nil, errors.New("article already has active approval flow")
	}
	
	// Create new approval flow
	approvalFlow := &entity.ArticleApprovalFlows{
		ArticleId:   articleId,
		WorkflowId:  workflowId,
		CurrentStep: 1,
		StatusId:    1, // In Progress
		ClientId:    clientId,
	}
	
	// Update article status
	statusId := 1 // Pending approval
	err = s.ArticlesRepo.UpdateSkipNull(clientId, articleId, &entity.Articles{
		StatusId:    &statusId,
		WorkflowId:  &workflowId,
		CurrentApprovalStep: &[]int{1}[0],
	})
	if err != nil {
		return nil, fmt.Errorf("failed to update article status: %v", err)
	}
	
	return s.Repo.Create(approvalFlow)
}

func (s *articleApprovalFlowService) ProcessApprovalStep(clientId *uuid.UUID, articleId uint, action string, message *string, approver *users.Users) error {
	// Get active approval flow
	approvalFlow, err := s.Repo.FindActiveByArticleId(articleId)
	if err != nil {
		return fmt.Errorf("approval flow not found: %v", err)
	}
	
	// Get workflow with steps
	workflow, err := s.WorkflowService.GetWorkflowWithSteps(approvalFlow.WorkflowId)
	if err != nil {
		return fmt.Errorf("workflow not found: %v", err)
	}
	
	// Find current step
	var currentStep *entity.ApprovalWorkflowSteps
	for _, step := range workflow.Steps {
		if step.StepOrder == approvalFlow.CurrentStep {
			currentStep = &step
			break
		}
	}
	
	if currentStep == nil {
		return errors.New("current step not found in workflow")
	}
	
	// Validate approver has permission for this step
	if approver.UserLevelId != currentStep.UserLevelId {
		return errors.New("approver does not have permission for this approval step")
	}
	
	// Create step log
	now := time.Now()
	stepLog := &entity.ArticleApprovalStepLogs{
		ArticleFlowId: approvalFlow.ID,
		StepOrder:     approvalFlow.CurrentStep,
		UserLevelId:   currentStep.UserLevelId,
		ApprovedBy:    approver.ID,
		Action:        action,
		Message:       message,
		ApprovedAt:    &now,
		ClientId:      clientId,
	}
	
	err = s.Repo.CreateStepLog(stepLog)
	if err != nil {
		return fmt.Errorf("failed to create step log: %v", err)
	}
	
	// Process action
	switch action {
	case "approved":
		return s.processApproval(clientId, approvalFlow, workflow)
	case "rejected":
		return s.processRejection(clientId, approvalFlow)
	case "revision_requested":
		return s.processRevisionRequest(clientId, approvalFlow)
	default:
		return errors.New("invalid action")
	}
}

func (s *articleApprovalFlowService) processApproval(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows, workflow *entity.ApprovalWorkflows) error {
	// Check if this is the last step
	maxStep := 0
	for _, step := range workflow.Steps {
		if step.StepOrder > maxStep {
			maxStep = step.StepOrder
		}
	}
	
	if approvalFlow.CurrentStep >= maxStep {
		// Final approval - publish article
		return s.finalizeApproval(clientId, approvalFlow)
	} else {
		// Move to next step
		return s.moveToNextStep(clientId, approvalFlow)
	}
}

func (s *articleApprovalFlowService) finalizeApproval(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error {
	// Update approval flow status
	approvalFlow.StatusId = 2 // Completed
	err := s.Repo.Update(approvalFlow.ID, approvalFlow)
	if err != nil {
		return err
	}
	
	// Update article status to published
	isPublish := true
	statusId := 2 // Published
	now := time.Now()
	
	err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{
		StatusId:    &statusId,
		IsPublish:   &isPublish,
		PublishedAt: &now,
		CurrentApprovalStep: nil, // Clear approval step
	})
	
	return err
}

func (s *articleApprovalFlowService) moveToNextStep(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error {
	// Move to next step
	approvalFlow.CurrentStep++
	err := s.Repo.Update(approvalFlow.ID, approvalFlow)
	if err != nil {
		return err
	}
	
	// Update article current step
	err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{
		CurrentApprovalStep: &approvalFlow.CurrentStep,
	})
	
	return err
}

func (s *articleApprovalFlowService) processRejection(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error {
	// Update approval flow status
	approvalFlow.StatusId = 3 // Rejected
	err := s.Repo.Update(approvalFlow.ID, approvalFlow)
	if err != nil {
		return err
	}
	
	// Update article status
	statusId := 3 // Rejected
	err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{
		StatusId: &statusId,
		CurrentApprovalStep: nil,
	})
	
	return err
}

func (s *articleApprovalFlowService) processRevisionRequest(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error {
	// Reset approval flow to step 0 (back to contributor)
	approvalFlow.CurrentStep = 0
	approvalFlow.StatusId = 4 // Revision Requested
	err := s.Repo.Update(approvalFlow.ID, approvalFlow)
	if err != nil {
		return err
	}
	
	// Update article status
	statusId := 4 // Revision Requested
	err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{
		StatusId: &statusId,
		CurrentApprovalStep: &[]int{0}[0],
	})
	
	return err
}

// Enhanced methods for approver dashboard
func (s *articleApprovalFlowService) GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, filters ApprovalFilters) ([]*DetailedApprovalFlow, paginator.Pagination, error) {
	// Build query with filters
	query := s.Repo.BuildApprovalQueueQuery(clientId, userLevelId, filters)
	
	// Get paginated results
	approvalFlows, pagination, err := s.Repo.GetPaginatedApprovalQueue(query, filters.Page, filters.Limit)
	if err != nil {
		return nil, paginator.Pagination{}, err
	}
	
	// Transform to detailed approval flows
	detailedFlows := make([]*DetailedApprovalFlow, 0, len(approvalFlows))
	for _, flow := range approvalFlows {
		detailedFlow, err := s.transformToDetailedFlow(flow, filters.IncludePreview)
		if err != nil {
			s.Log.Error().Err(err).Uint("flow_id", flow.ID).Msg("Failed to transform approval flow")
			continue
		}
		detailedFlows = append(detailedFlows, detailedFlow)
	}
	
	return detailedFlows, pagination, nil
}

func (s *articleApprovalFlowService) GetApprovalStatistics(clientId *uuid.UUID, userLevelId uint) (*ApprovalStatistics, error) {
	stats := &ApprovalStatistics{}
	
	// Get pending count
	pendingCount, err := s.Repo.GetPendingCountByLevel(clientId, userLevelId)
	if err != nil {
		return nil, err
	}
	stats.PendingCount = pendingCount
	
	// Get overdue count
	overdueCount, err := s.Repo.GetOverdueCountByLevel(clientId, userLevelId)
	if err != nil {
		return nil, err
	}
	stats.OverdueCount = overdueCount
	
	// Get approval counts by time period
	now := time.Now()
	startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	startOfWeek := startOfDay.AddDate(0, 0, -int(now.Weekday()))
	startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
	
	stats.ApprovedToday, _ = s.Repo.GetApprovedCountByPeriod(clientId, userLevelId, startOfDay, now)
	stats.ApprovedThisWeek, _ = s.Repo.GetApprovedCountByPeriod(clientId, userLevelId, startOfWeek, now)
	stats.ApprovedThisMonth, _ = s.Repo.GetApprovedCountByPeriod(clientId, userLevelId, startOfMonth, now)
	stats.RejectedThisMonth, _ = s.Repo.GetRejectedCountByPeriod(clientId, userLevelId, startOfMonth, now)
	stats.RevisionRequestsThisMonth, _ = s.Repo.GetRevisionRequestCountByPeriod(clientId, userLevelId, startOfMonth, now)
	
	// Get timing statistics
	timingStats, err := s.Repo.GetApprovalTimingStats(clientId, userLevelId)
	if err == nil {
		stats.AverageApprovalTimeHours = timingStats.AverageHours
		stats.FastestApprovalMinutes = timingStats.FastestMinutes
		stats.SlowestApprovalHours = timingStats.SlowestHours
	}
	
	// Calculate approval rate
	totalProcessed := stats.ApprovedThisMonth + stats.RejectedThisMonth
	if totalProcessed > 0 {
		stats.ApprovalRatePercentage = float64(stats.ApprovedThisMonth) / float64(totalProcessed) * 100
	}
	
	// Get categories breakdown
	categoriesBreakdown, err := s.Repo.GetCategoriesBreakdown(clientId, userLevelId)
	if err == nil {
		stats.CategoriesBreakdown = categoriesBreakdown
	}
	
	return stats, nil
}

func (s *articleApprovalFlowService) GetWorkloadDistribution(clientId *uuid.UUID, userLevelId uint) (*WorkloadDistribution, error) {
	distribution := &WorkloadDistribution{}
	
	// Get my level workload
	myLevel, err := s.Repo.GetLevelWorkload(clientId, userLevelId)
	if err != nil {
		return nil, err
	}
	distribution.MyLevel = *myLevel
	
	// Get team comparison (same level users)
	teamMembers, err := s.Repo.GetTeamWorkloadComparison(clientId, userLevelId)
	if err == nil {
		distribution.TeamComparison = teamMembers
	}
	
	// Identify bottlenecks
	bottlenecks, err := s.Repo.GetWorkflowBottlenecks(clientId)
	if err == nil {
		distribution.Bottlenecks = bottlenecks
	}
	
	return distribution, nil
}

func (s *articleApprovalFlowService) BulkApprovalAction(clientId *uuid.UUID, articleIds []uint, action string, message *string, approver *users.Users) (*BulkActionResult, error) {
	result := &BulkActionResult{
		ProcessedCount: len(articleIds),
		Results:       make([]BulkActionItemResult, 0, len(articleIds)),
	}
	
	for _, articleId := range articleIds {
		itemResult := BulkActionItemResult{
			ArticleId: articleId,
		}
		
		err := s.ProcessApprovalStep(clientId, articleId, action, message, approver)
		if err != nil {
			itemResult.Status = "failed"
			errorMsg := err.Error()
			itemResult.Error = &errorMsg
			result.FailedCount++
		} else {
			itemResult.Status = "success"
			
			// Get next step info
			approvalFlow, err := s.Repo.FindActiveByArticleId(articleId)
			if err == nil {
				if approvalFlow.StatusId == 2 {
					itemResult.NextStep = "published"
				} else {
					itemResult.NextStep = fmt.Sprintf("%d", approvalFlow.CurrentStep)
				}
			}
			
			result.SuccessfulCount++
		}
		
		result.Results = append(result.Results, itemResult)
	}
	
	return result, nil
}

func (s *articleApprovalFlowService) transformToDetailedFlow(flow *entity.ArticleApprovalFlows, includePreview bool) (*DetailedApprovalFlow, error) {
	// This method would transform the basic approval flow to detailed view
	// Implementation would include:
	// 1. Fetch article details with author info
	// 2. Calculate approval context (deadlines, role description)
	// 3. Build workflow progress information
	// 4. Add content preview if requested
	
	detailedFlow := &DetailedApprovalFlow{
		ID: flow.ID,
		// Article details would be populated from database joins
		// ApprovalContext would be calculated based on workflow step
		// WorkflowProgress would show current position in workflow
	}
	
	return detailedFlow, nil
}

3. Controller Implementation

Article Approval Controller

// app/module/articles/controller/articles_approval.controller.go
package controller

import (
	"github.com/gofiber/fiber/v2"
	"github.com/rs/zerolog"
	"strconv"
	"netidhub-saas-be/app/middleware"
	"netidhub-saas-be/app/module/article_approval_flows/service"
	"netidhub-saas-be/app/module/articles/request"
	usersRepository "netidhub-saas-be/app/module/users/repository"
	utilRes "netidhub-saas-be/utils/response"
	utilSvc "netidhub-saas-be/utils/service"
	utilVal "netidhub-saas-be/utils/validator"
)

type articleApprovalController struct {
	approvalFlowService service.ArticleApprovalFlowService
	usersRepo           usersRepository.UsersRepository
	log                 zerolog.Logger
}

type ArticleApprovalController interface {
	SubmitForApproval(c *fiber.Ctx) error
	ApproveStep(c *fiber.Ctx) error
	RejectArticle(c *fiber.Ctx) error
	RequestRevision(c *fiber.Ctx) error
	GetPendingApprovals(c *fiber.Ctx) error
	GetApprovalHistory(c *fiber.Ctx) error
}

func NewArticleApprovalController(
	approvalFlowService service.ArticleApprovalFlowService,
	usersRepo usersRepository.UsersRepository,
	log zerolog.Logger,
) ArticleApprovalController {
	return &articleApprovalController{
		approvalFlowService: approvalFlowService,
		usersRepo:           usersRepo,
		log:                 log,
	}
}

// SubmitForApproval submit article for approval
// @Summary      Submit article for approval
// @Description  API for submitting article for approval workflow
// @Tags         Articles
// @Security     Bearer
// @Param        id path int true "Article ID"
// @Param        payload body request.SubmitApprovalRequest true "Required payload"
// @Success      200  {object}  response.Response
// @Failure      400  {object}  response.BadRequestError
// @Failure      401  {object}  response.UnauthorizedError
// @Failure      500  {object}  response.InternalServerError
// @Router       /articles/{id}/submit-approval [post]
func (ctrl *articleApprovalController) SubmitForApproval(c *fiber.Ctx) error {
	clientId := middleware.GetClientId(c)
	
	id, err := strconv.ParseUint(c.Params("id"), 10, 0)
	if err != nil {
		return err
	}
	
	req := new(request.SubmitApprovalRequest)
	if err := utilVal.ParseAndValidate(c, req); err != nil {
		return err
	}
	
	approvalFlow, err := ctrl.approvalFlowService.InitiateApprovalFlow(clientId, uint(id), req.WorkflowId)
	if err != nil {
		return err
	}
	
	return utilRes.Resp(c, utilRes.Response{
		Success:  true,
		Messages: utilRes.Messages{"Article submitted for approval successfully"},
		Data:     approvalFlow,
	})
}

// ApproveStep approve current approval step
// @Summary      Approve article step
// @Description  API for approving current approval step
// @Tags         Articles
// @Security     Bearer
// @Param        id path int true "Article ID"
// @Param        payload body request.ApprovalActionRequest true "Required payload"
// @Success      200  {object}  response.Response
// @Failure      400  {object}  response.BadRequestError
// @Failure      401  {object}  response.UnauthorizedError
// @Failure      500  {object}  response.InternalServerError
// @Router       /articles/{id}/approve [post]
func (ctrl *articleApprovalController) ApproveStep(c *fiber.Ctx) error {
	clientId := middleware.GetClientId(c)
	authToken := c.Get("Authorization")
	
	id, err := strconv.ParseUint(c.Params("id"), 10, 0)
	if err != nil {
		return err
	}
	
	req := new(request.ApprovalActionRequest)
	if err := utilVal.ParseAndValidate(c, req); err != nil {
		return err
	}
	
	// Get current user info
	approver := utilSvc.GetUserInfo(ctrl.log, ctrl.usersRepo, authToken)
	
	err = ctrl.approvalFlowService.ProcessApprovalStep(clientId, uint(id), "approved", req.Message, approver)
	if err != nil {
		return err
	}
	
	return utilRes.Resp(c, utilRes.Response{
		Success:  true,
		Messages: utilRes.Messages{"Article approved successfully"},
	})
}

// RequestRevision request revision for article
// @Summary      Request article revision
// @Description  API for requesting article revision
// @Tags         Articles
// @Security     Bearer
// @Param        id path int true "Article ID"
// @Param        payload body request.ApprovalActionRequest true "Required payload"
// @Success      200  {object}  response.Response
// @Failure      400  {object}  response.BadRequestError
// @Failure      401  {object}  response.UnauthorizedError
// @Failure      500  {object}  response.InternalServerError
// @Router       /articles/{id}/request-revision [post]
func (ctrl *articleApprovalController) RequestRevision(c *fiber.Ctx) error {
	clientId := middleware.GetClientId(c)
	authToken := c.Get("Authorization")
	
	id, err := strconv.ParseUint(c.Params("id"), 10, 0)
	if err != nil {
		return err
	}
	
	req := new(request.ApprovalActionRequest)
	if err := utilVal.ParseAndValidate(c, req); err != nil {
		return err
	}
	
	// Get current user info
	approver := utilSvc.GetUserInfo(ctrl.log, ctrl.usersRepo, authToken)
	
	err = ctrl.approvalFlowService.ProcessApprovalStep(clientId, uint(id), "revision_requested", req.Message, approver)
	if err != nil {
		return err
	}
	
	return utilRes.Resp(c, utilRes.Response{
		Success:  true,
		Messages: utilRes.Messages{"Revision requested successfully"},
	})
}

4. Request/Response Models

Request Models

// app/module/articles/request/approval.request.go
package request

type SubmitApprovalRequest struct {
	WorkflowId uint `json:"workflowId" validate:"required"`
}

type ApprovalActionRequest struct {
	Message *string `json:"message"`
}

// app/module/approval_workflows/request/approval_workflows.request.go
package request

import "netidhub-saas-be/app/database/entity"

type ApprovalWorkflowCreateRequest struct {
	Name        string                           `json:"name" validate:"required"`
	Description *string                          `json:"description"`
	Steps       []ApprovalWorkflowStepRequest    `json:"steps" validate:"required,min=1"`
}

type ApprovalWorkflowStepRequest struct {
	StepOrder   int  `json:"stepOrder" validate:"required,min=1"`
	UserLevelId uint `json:"userLevelId" validate:"required"`
	IsRequired  bool `json:"isRequired"`
	CanSkip     bool `json:"canSkip"`
}

func (req ApprovalWorkflowCreateRequest) ToEntity() *entity.ApprovalWorkflows {
	workflow := &entity.ApprovalWorkflows{
		Name:        req.Name,
		Description: req.Description,
		IsActive:    &[]bool{true}[0],
	}
	
	for _, stepReq := range req.Steps {
		step := entity.ApprovalWorkflowSteps{
			StepOrder:   stepReq.StepOrder,
			UserLevelId: stepReq.UserLevelId,
			IsRequired:  &stepReq.IsRequired,
			CanSkip:     &stepReq.CanSkip,
		}
		workflow.Steps = append(workflow.Steps, step)
	}
	
	return workflow
}

5. Migration Scripts

Database Migration

-- migrations/001_create_approval_system_tables.sql

-- Create approval_workflows table
CREATE TABLE approval_workflows (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    is_active BOOLEAN DEFAULT true,
    client_id UUID,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Create approval_workflow_steps table
CREATE TABLE approval_workflow_steps (
    id SERIAL PRIMARY KEY,
    workflow_id INT NOT NULL REFERENCES approval_workflows(id) ON DELETE CASCADE,
    step_order INT NOT NULL,
    user_level_id INT NOT NULL REFERENCES user_levels(id),
    is_required BOOLEAN DEFAULT true,
    can_skip BOOLEAN DEFAULT false,
    client_id UUID,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(workflow_id, step_order)
);

-- Create article_approval_flows table
CREATE TABLE article_approval_flows (
    id SERIAL PRIMARY KEY,
    article_id INT NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
    workflow_id INT NOT NULL REFERENCES approval_workflows(id),
    current_step INT DEFAULT 1,
    status_id INT DEFAULT 1, -- 1=In Progress, 2=Completed, 3=Rejected, 4=Revision Requested
    client_id UUID,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Create article_approval_step_logs table
CREATE TABLE article_approval_step_logs (
    id SERIAL PRIMARY KEY,
    article_flow_id INT NOT NULL REFERENCES article_approval_flows(id) ON DELETE CASCADE,
    step_order INT NOT NULL,
    user_level_id INT NOT NULL REFERENCES user_levels(id),
    approved_by INT NOT NULL REFERENCES users(id),
    action VARCHAR(50) NOT NULL CHECK (action IN ('approved', 'rejected', 'revision_requested')),
    message TEXT,
    approved_at TIMESTAMP,
    client_id UUID,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Add indexes for performance
CREATE INDEX idx_approval_workflows_client_id ON approval_workflows(client_id);
CREATE INDEX idx_approval_workflows_is_active ON approval_workflows(is_active);
CREATE INDEX idx_approval_workflow_steps_workflow_id ON approval_workflow_steps(workflow_id);
CREATE INDEX idx_article_approval_flows_article_id ON article_approval_flows(article_id);
CREATE INDEX idx_article_approval_flows_status_id ON article_approval_flows(status_id);
CREATE INDEX idx_article_approval_step_logs_article_flow_id ON article_approval_step_logs(article_flow_id);
CREATE INDEX idx_article_approval_step_logs_approved_by ON article_approval_step_logs(approved_by);

-- Modify articles table
ALTER TABLE articles ADD COLUMN workflow_id INT REFERENCES approval_workflows(id);
ALTER TABLE articles ADD COLUMN current_approval_step INT DEFAULT 0;

-- Create default workflow for backward compatibility
INSERT INTO approval_workflows (name, description, is_active) 
VALUES ('Legacy 3-Level Approval', 'Default 3-level approval system for backward compatibility', true);

-- Get the workflow ID (assuming it's 1 for the first insert)
INSERT INTO approval_workflow_steps (workflow_id, step_order, user_level_id, is_required, can_skip) 
VALUES 
(1, 1, 3, true, false), -- Level 3 approver (first step)
(1, 2, 2, true, false), -- Level 2 approver (second step)
(1, 3, 1, true, false); -- Level 1 approver (final step)

-- Update existing articles to use default workflow
UPDATE articles SET workflow_id = 1 WHERE workflow_id IS NULL;

6. Usage Examples

Creating a Custom Workflow

# Create a 2-level approval workflow
curl -X POST http://localhost:8800/api/approval-workflows \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "name": "Simple 2-Level Approval",
    "description": "Quick approval for simple articles",
    "steps": [
      {
        "stepOrder": 1,
        "userLevelId": 2,
        "isRequired": true,
        "canSkip": false
      },
      {
        "stepOrder": 2,
        "userLevelId": 1,
        "isRequired": true,
        "canSkip": false
      }
    ]
  }'

Submitting Article for Approval

# Submit article ID 123 for approval using workflow ID 2
curl -X POST http://localhost:8800/api/articles/123/submit-approval \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "workflowId": 2
  }'

Approving an Article

# Approve article ID 123
curl -X POST http://localhost:8800/api/articles/123/approve \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "message": "Article looks good, approved for next level"
  }'

Requesting Revision

# Request revision for article ID 123
curl -X POST http://localhost:8800/api/articles/123/request-revision \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "message": "Please fix the grammar issues in paragraph 2"
  }'

Dengan implementasi ini, sistem approval artikel menjadi sangat fleksibel dan dapat disesuaikan dengan kebutuhan organisasi yang berbeda-beda. Workflow dapat dibuat, dimodifikasi, dan dinonaktifkan tanpa mengubah kode aplikasi.