initial commit
This commit is contained in:
commit
4d9b323691
|
|
@ -0,0 +1,3 @@
|
||||||
|
/vendor
|
||||||
|
debug.log
|
||||||
|
/.ideadebug.log
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
stages:
|
||||||
|
- build-image
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
#build-1:
|
||||||
|
# stage: build-app
|
||||||
|
# image: golang:alpine
|
||||||
|
# script:
|
||||||
|
# - go build -o main .
|
||||||
|
# artifacts:
|
||||||
|
# paths:
|
||||||
|
# - main
|
||||||
|
|
||||||
|
build-2:
|
||||||
|
stage: build-image
|
||||||
|
image: docker/compose:latest
|
||||||
|
services:
|
||||||
|
- name: docker:dind
|
||||||
|
command: [ "--insecure-registry=103.82.242.92:8900" ]
|
||||||
|
script:
|
||||||
|
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
|
||||||
|
- docker-compose build
|
||||||
|
- docker tag registry.gitlab.com/hanifsalafi/netidhub-saas-be:dev 103.82.242.92:8900/medols/netidhub-saas-be:dev
|
||||||
|
- docker push 103.82.242.92:8900/medols/netidhub-saas-be:dev
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
stage: deploy
|
||||||
|
when: on_success
|
||||||
|
image: curlimages/curl:latest
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
script:
|
||||||
|
- curl --user $JENKINS_USER:$JENKINS_PWD http://38.47.180.165:8080/job/autodeploy-medols-be/build?token=autodeploymedols
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
FROM golang:alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
RUN go get -v ./...
|
||||||
|
RUN go mod vendor
|
||||||
|
RUN go build -o main .
|
||||||
|
|
||||||
|
EXPOSE 8800
|
||||||
|
|
||||||
|
CMD ["sh", "-c", "go run main.go"]
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type ActivityLogTypes struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogs struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
ActivityTypeId int `json:"activity_type_id" gorm:"type:int4"`
|
||||||
|
Url string `json:"url" gorm:"type:varchar"`
|
||||||
|
VisitorIp *string `json:"visitor_ip" gorm:"type:varchar"`
|
||||||
|
ArticleId *uint `json:"article_id" gorm:"type:int4"`
|
||||||
|
UserId *uint `json:"user_id" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Advertisement struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
RedirectLink string `json:"redirect_link" gorm:"type:varchar"`
|
||||||
|
ContentFilePath *string `json:"content_file_path" gorm:"type:varchar"`
|
||||||
|
ContentFileName *string `json:"content_file_name" gorm:"type:varchar"`
|
||||||
|
Placement string `json:"placement" gorm:"type:varchar"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4"`
|
||||||
|
IsPublish bool `json:"is_publish" gorm:"type:bool"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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"`
|
||||||
|
StepName string `json:"step_name" gorm:"type:varchar;not null"`
|
||||||
|
RequiredUserLevelId uint `json:"required_user_level_id" gorm:"type:int4;not null"`
|
||||||
|
CanSkip *bool `json:"can_skip" gorm:"type:bool;default:false"`
|
||||||
|
AutoApproveAfterHours *int `json:"auto_approve_after_hours" gorm:"type:int4"`
|
||||||
|
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
|
||||||
|
Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;constraint:OnDelete:CASCADE"`
|
||||||
|
RequiredUserLevel UserLevels `json:"required_user_level" gorm:"foreignKey:RequiredUserLevelId"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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"`
|
||||||
|
IsDefault *bool `json:"is_default" gorm:"type:bool;default:false"`
|
||||||
|
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||||
|
// New fields for no-approval support
|
||||||
|
RequiresApproval *bool `json:"requires_approval" gorm:"type:bool;default:true"` // false = no approval needed
|
||||||
|
AutoPublish *bool `json:"auto_publish" gorm:"type:bool;default:false"` // true = auto publish after creation
|
||||||
|
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;constraint:OnDelete:CASCADE"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
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=pending, 2=approved, 3=rejected, 4=revision_requested
|
||||||
|
SubmittedById uint `json:"submitted_by_id" gorm:"type:int4;not null"`
|
||||||
|
SubmittedAt time.Time `json:"submitted_at" gorm:"default:now()"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at" gorm:"type:timestamp"`
|
||||||
|
RejectionReason *string `json:"rejection_reason" gorm:"type:text"`
|
||||||
|
RevisionRequested *bool `json:"revision_requested" gorm:"type:bool;default:false"`
|
||||||
|
RevisionMessage *string `json:"revision_message" gorm:"type:text"`
|
||||||
|
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;constraint:OnDelete:CASCADE"`
|
||||||
|
Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId"`
|
||||||
|
SubmittedBy *Users `json:"submitted_by" gorm:"foreignKey:SubmittedById"`
|
||||||
|
StepLogs []ArticleApprovalStepLogs `json:"step_logs" gorm:"foreignKey:ApprovalFlowId;constraint:OnDelete:CASCADE"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogs struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
ApprovalFlowId uint `json:"approval_flow_id" gorm:"type:int4;not null"`
|
||||||
|
StepOrder int `json:"step_order" gorm:"type:int4;not null"`
|
||||||
|
StepName string `json:"step_name" gorm:"type:varchar;not null"`
|
||||||
|
ApprovedById *uint `json:"approved_by_id" gorm:"type:int4"`
|
||||||
|
Action string `json:"action" gorm:"type:varchar;not null"` // approve, reject, request_revision
|
||||||
|
Message *string `json:"message" gorm:"type:text"`
|
||||||
|
ProcessedAt time.Time `json:"processed_at" gorm:"default:now()"`
|
||||||
|
UserLevelId uint `json:"user_level_id" gorm:"type:int4;not null"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
ApprovalFlow ArticleApprovalFlows `json:"approval_flow" gorm:"foreignKey:ApprovalFlowId;constraint:OnDelete:CASCADE"`
|
||||||
|
ApprovedBy Users `json:"approved_by" gorm:"foreignKey:ApprovedById"`
|
||||||
|
UserLevel UserLevels `json:"user_level" gorm:"foreignKey:UserLevelId"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleApprovals struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
ArticleId uint `json:"article_id" gorm:"type:int4"`
|
||||||
|
ApprovalBy uint `json:"approval_by" gorm:"type:int4"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4"`
|
||||||
|
Message string `json:"message" gorm:"type:varchar"`
|
||||||
|
ApprovalAtLevel *int `json:"approval_at_level" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleCategories struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
|
||||||
|
Slug *string `json:"slug" gorm:"type:varchar"`
|
||||||
|
ParentId *int `json:"parent_id" gorm:"type:int4"`
|
||||||
|
Tags *string `json:"tags" gorm:"type:varchar"`
|
||||||
|
Position *int `json:"position" gorm:"type:int4"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
OldCategoryId *uint `json:"old_category_id" gorm:"type:int4"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4;default:1"`
|
||||||
|
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
|
||||||
|
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package article_category_details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
entity "web-medols-be/app/database/entity"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleCategoryDetails struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
ArticleId uint `json:"article_id" gorm:"type:int4"`
|
||||||
|
CategoryId int `json:"category_id" gorm:"type:int4"`
|
||||||
|
Category *entity.ArticleCategories `json:"category" gorm:"foreignKey:CategoryId;references:ID"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Article entity.Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleComments struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Message string `json:"message" gorm:"type:varchar"`
|
||||||
|
ArticleId uint `json:"article_id" gorm:"type:int4"`
|
||||||
|
CommentFrom *uint `json:"comment_from" gorm:"type:int4"`
|
||||||
|
ParentId *int `json:"parent_id" gorm:"type:int4"`
|
||||||
|
IsPublic bool `json:"is_public" gorm:"type:bool;default:false"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4;default:0"`
|
||||||
|
ApprovedAt *time.Time `json:"approved_at" gorm:"type:timestamp"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// statusId => 0: waiting, 1: accepted, 2: replied, 3: rejected
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleFiles struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
ArticleId uint `json:"article_id" gorm:"type:int4"`
|
||||||
|
UploadID *string `json:"upload_id" gorm:"type:varchar"`
|
||||||
|
FilePath *string `json:"file_path" gorm:"type:varchar"`
|
||||||
|
FileUrl *string `json:"file_url" gorm:"type:varchar"`
|
||||||
|
FileName *string `json:"file_name" gorm:"type:varchar"`
|
||||||
|
FileThumbnail *string `json:"file_thumbnail" gorm:"type:varchar"`
|
||||||
|
FileAlt *string `json:"file_alt" gorm:"type:varchar"`
|
||||||
|
WidthPixel *string `json:"width_pixel" gorm:"type:varchar"`
|
||||||
|
HeightPixel *string `json:"height_pixel" gorm:"type:varchar"`
|
||||||
|
Size *string `json:"size" gorm:"type:varchar"`
|
||||||
|
DownloadCount *int `json:"download_count" gorm:"type:int4;default:0"`
|
||||||
|
CreatedById int `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4"`
|
||||||
|
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
|
||||||
|
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Article Articles `json:"article" gorm:"foreignKey:ArticleId"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ArticleNulisAI struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
NulisAiId int `json:"nulis_ai_id" gorm:"type:int4"`
|
||||||
|
ArticleId int `json:"article_id" gorm:"type:int4"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
HtmlDescription string `json:"html_description" gorm:"type:varchar"`
|
||||||
|
CategoryId int `json:"category_id" gorm:"type:int4"`
|
||||||
|
CreatorId uint `json:"creator_id" gorm:"type:int4"`
|
||||||
|
Tags string `json:"tags" gorm:"type:varchar"`
|
||||||
|
ThumbnailPath string `json:"thumbnail_path" gorm:"type:varchar"`
|
||||||
|
IsPublish bool `json:"is_publish" gorm:"type:bool"`
|
||||||
|
PublishedAt time.Time `json:"published_at" gorm:"type:timestamp"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Articles struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Slug string `json:"slug" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
CategoryId int `json:"category_id" gorm:"type:int4"`
|
||||||
|
HtmlDescription string `json:"html_description" gorm:"type:varchar"`
|
||||||
|
TypeId int `json:"type_id" gorm:"type:int4"`
|
||||||
|
Tags string `json:"tags" gorm:"type:varchar"`
|
||||||
|
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
|
||||||
|
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
|
||||||
|
PageUrl *string `json:"page_url" gorm:"type:varchar"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
AiArticleId *int `json:"ai_article_id" gorm:"type:int4"`
|
||||||
|
CommentCount *int `json:"comment_count" gorm:"type:int4;default:0"`
|
||||||
|
ShareCount *int `json:"share_count" gorm:"type:int4;default:0"`
|
||||||
|
ViewCount *int `json:"view_count" gorm:"type:int4;default:0"`
|
||||||
|
StatusId *int `json:"status_id" gorm:"type:int4"`
|
||||||
|
OldId *uint `json:"old_id" gorm:"type:int4"`
|
||||||
|
NeedApprovalFrom *int `json:"need_approval_from" gorm:"type:int4"`
|
||||||
|
HasApprovedBy *string `json:"has_approved_by" gorm:"type:varchar"`
|
||||||
|
WorkflowId *uint `json:"workflow_id" gorm:"type:int4"`
|
||||||
|
CurrentApprovalStep *int `json:"current_approval_step" gorm:"type:int4;default:0"` // 0=not submitted, 1+=approval step
|
||||||
|
// New fields for no-approval support
|
||||||
|
BypassApproval *bool `json:"bypass_approval" gorm:"type:bool;default:false"` // true = skip approval process
|
||||||
|
ApprovalExempt *bool `json:"approval_exempt" gorm:"type:bool;default:false"` // true = permanently exempt from approval
|
||||||
|
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
|
||||||
|
IsBanner *bool `json:"is_banner" gorm:"type:bool;default:false"`
|
||||||
|
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
|
||||||
|
IsDraft *bool `json:"is_draft" gorm:"type:bool;default:false"`
|
||||||
|
DraftedAt *time.Time `json:"drafted_at" gorm:"type:timestamp"`
|
||||||
|
PublishSchedule *string `json:"publish_schedule" gorm:"type:varchar"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuditTrails struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
IP string
|
||||||
|
Status int
|
||||||
|
UserID *string
|
||||||
|
RequestHeaders string
|
||||||
|
RequestBody string
|
||||||
|
ResponseBody string
|
||||||
|
DurationMs int64
|
||||||
|
ClientId *uuid.UUID
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bookmarks struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
UserId uint `json:"user_id" gorm:"type:int4"`
|
||||||
|
ArticleId uint `json:"article_id" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Article Articles `json:"article" gorm:"foreignKey:ArticleId"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type Cities struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
CityName string `json:"city_name" gorm:"type:varchar"`
|
||||||
|
ProvId int `json:"prov_id" gorm:"type:int4"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringArray is a custom type for handling string arrays with JSON serialization
|
||||||
|
type StringArray []string
|
||||||
|
|
||||||
|
// Scan implements the sql.Scanner interface
|
||||||
|
func (s *StringArray) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*s = StringArray{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
return json.Unmarshal(v, s)
|
||||||
|
case string:
|
||||||
|
return json.Unmarshal([]byte(v), s)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver.Valuer interface
|
||||||
|
func (s StringArray) Value() (driver.Value, error) {
|
||||||
|
if s == nil || len(s) == 0 {
|
||||||
|
return "[]", nil
|
||||||
|
}
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientApprovalSettings struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
ClientId uuid.UUID `json:"client_id" gorm:"type:UUID;not null;uniqueIndex"`
|
||||||
|
RequiresApproval *bool `json:"requires_approval" gorm:"type:bool;default:true"` // false = no approval needed
|
||||||
|
DefaultWorkflowId *uint `json:"default_workflow_id" gorm:"type:int4"` // default workflow for this client
|
||||||
|
AutoPublishArticles *bool `json:"auto_publish_articles" gorm:"type:bool;default:false"` // auto publish after creation
|
||||||
|
ApprovalExemptUsers []uint `json:"approval_exempt_users" gorm:"type:int4[]"` // user IDs exempt from approval
|
||||||
|
ApprovalExemptRoles []uint `json:"approval_exempt_roles" gorm:"type:int4[]"` // role IDs exempt from approval
|
||||||
|
ApprovalExemptCategories []uint `json:"approval_exempt_categories" gorm:"type:int4[]"` // category IDs exempt from approval
|
||||||
|
RequireApprovalFor []string `json:"require_approval_for" gorm:"type:jsonb"` // specific content types that need approval
|
||||||
|
SkipApprovalFor []string `json:"skip_approval_for" gorm:"type:jsonb"` // specific content types that skip approval
|
||||||
|
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()"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Client Clients `json:"client" gorm:"foreignKey:ClientId;constraint:OnDelete:CASCADE"`
|
||||||
|
Workflow *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:DefaultWorkflowId"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Clients struct {
|
||||||
|
ID uuid.UUID `json:"id" gorm:"primaryKey;type:UUID"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CsrfTokenRecords struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
Token string `gorm:"uniqueIndex;size:255"`
|
||||||
|
Value []byte `gorm:"value"`
|
||||||
|
ExpireAt time.Time `gorm:"index"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomStaticPages struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
Slug string `json:"slug" gorm:"type:varchar"`
|
||||||
|
HtmlBody string `json:"html_body" gorm:"type:text"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type Districts struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
DisName string `json:"dis_name" gorm:"type:varchar"`
|
||||||
|
CityId int `json:"city_id" gorm:"type:int4"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Feedbacks struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Message string `json:"message" gorm:"type:varchar"`
|
||||||
|
CommentFromName string `json:"comment_from_name" gorm:"type:varchar"`
|
||||||
|
CommentFromEmail string `json:"comment_from_email" gorm:"type:varchar"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4;default:0"`
|
||||||
|
ApprovedAt *time.Time `json:"approved_at" gorm:"type:timestamp"`
|
||||||
|
ReplyMessage *string `json:"reply_message" gorm:"type:varchar"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// statusId => 0: waiting, 1: accepted, 2: replied, 3: nothing
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ForgotPasswords struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
KeycloakID string `json:"keycloak_id" gorm:"type:varchar"`
|
||||||
|
CodeRequest string `json:"code_request" gorm:"type:varchar"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MagazineFiles struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
MagazineId uint `json:"magazine_id" gorm:"type:int4"`
|
||||||
|
DownloadCount *int `json:"download_count" gorm:"type:int4"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4"`
|
||||||
|
IsPublish *bool `json:"is_publish" gorm:"type:bool"`
|
||||||
|
FilePath *string `json:"file_path" gorm:"type:varchar"`
|
||||||
|
FileUrl *string `json:"file_url" gorm:"type:varchar"`
|
||||||
|
FileName *string `json:"file_name" gorm:"type:varchar"`
|
||||||
|
FileAlt *string `json:"file_alt" gorm:"type:varchar"`
|
||||||
|
WidthPixel *string `json:"width_pixel" gorm:"type:varchar"`
|
||||||
|
HeightPixel *string `json:"height_pixel" gorm:"type:varchar"`
|
||||||
|
Size *string `json:"size" gorm:"type:varchar"`
|
||||||
|
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Magazines struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
|
||||||
|
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
|
||||||
|
ThumbnailUrl *string `json:"thumbnail_url" gorm:"type:varchar"`
|
||||||
|
PageUrl *string `json:"page_url" gorm:"type:varchar"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4"`
|
||||||
|
IsPublish *bool `json:"is_publish" gorm:"type:bool"`
|
||||||
|
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type MasterApprovalStatuses struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterMenus struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
ModuleId int `json:"module_id" gorm:"type:int4"`
|
||||||
|
ParentMenuId *int `json:"parent_menu_id" gorm:"type:int4"`
|
||||||
|
Icon *string `json:"icon" gorm:"type:varchar"`
|
||||||
|
Group string `json:"group" gorm:"type:varchar"`
|
||||||
|
Position *int `json:"position" gorm:"type:int4"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterModules struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
PathUrl string `json:"path_url" gorm:"type:varchar"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type MasterStatuses struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OneTimePasswords struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Email string `json:"email" gorm:"type:varchar"`
|
||||||
|
Name *string `json:"name" gorm:"type:varchar"`
|
||||||
|
Identity *string `json:"identity" gorm:"type:varchar"`
|
||||||
|
OtpCode string `json:"otp_code" gorm:"type:varchar"`
|
||||||
|
ValidUntil time.Time `json:"valid_until" gorm:"default:(NOW() + INTERVAL '10 minutes')"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type Provinces struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
ProvName string `json:"prov_name" gorm:"type:varchar"`
|
||||||
|
LocationId int `json:"location_id" gorm:"type:int4"`
|
||||||
|
Status int `json:"status" gorm:"type:int4"`
|
||||||
|
Timezone string `json:"timezone" gorm:"type:varchar"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schedules struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
Location string `json:"location" gorm:"type:varchar"`
|
||||||
|
IsLiveStreaming *bool `json:"is_live_streaming" gorm:"type:bool;default:false"`
|
||||||
|
LiveStreamingUrl *string `json:"live_streaming_url" gorm:"type:varchar"`
|
||||||
|
TypeId int `json:"type_id" gorm:"type:int4"`
|
||||||
|
StartDate *time.Time `json:"start_date" gorm:"type:date"`
|
||||||
|
EndDate *time.Time `json:"end_date" gorm:"type:date"`
|
||||||
|
StartTime *string `json:"start_time" gorm:"type:varchar"`
|
||||||
|
EndTime *string `json:"end_time" gorm:"type:varchar"`
|
||||||
|
Speakers string `json:"speakers" gorm:"type:varchar"`
|
||||||
|
PosterImagePath *string `json:"poster_image_path" gorm:"type:varchar"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
StatusId *int `json:"status_id" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Email string `json:"email" gorm:"type:varchar"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserLevels struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
AliasName string `json:"alias_name" gorm:"type:varchar"`
|
||||||
|
LevelNumber int `json:"level_number" gorm:"type:int4"`
|
||||||
|
ParentLevelId *int `json:"parent_level_id" gorm:"type:int4"`
|
||||||
|
ProvinceId *int `json:"province_id" gorm:"type:int4"`
|
||||||
|
Group *string `json:"group" gorm:"type:varchar"`
|
||||||
|
IsApprovalActive *bool `json:"is_approval_active" gorm:"type:bool;default:false"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive *bool `json:"is_active" gorm:"type:bool"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRoleAccesses struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
|
||||||
|
MenuId int `json:"menu_id" gorm:"type:int4"`
|
||||||
|
IsViewEnabled bool `json:"is_view_enabled" gorm:"type:bool"`
|
||||||
|
IsInsertEnabled bool `json:"is_insert_enabled" gorm:"type:bool"`
|
||||||
|
IsUpdateEnabled bool `json:"is_update_enabled" gorm:"type:bool"`
|
||||||
|
IsDeleteEnabled bool `json:"is_delete_enabled" gorm:"type:bool"`
|
||||||
|
IsApprovalEnabled bool `json:"is_approval_enabled" gorm:"type:bool"`
|
||||||
|
IsAdminEnabled bool `json:"is_admin_enabled" gorm:"type:bool"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRoleLevelDetails struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
|
||||||
|
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRoles struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar"`
|
||||||
|
Code string `json:"code" gorm:"type:varchar"`
|
||||||
|
StatusId int `json:"status_id" gorm:"type:int4;default:1"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Users struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Username string `json:"username" gorm:"type:varchar"`
|
||||||
|
Email string `json:"email" gorm:"type:varchar"`
|
||||||
|
Fullname string `json:"fullname" gorm:"type:varchar"`
|
||||||
|
Address *string `json:"address" gorm:"type:varchar"`
|
||||||
|
PhoneNumber *string `json:"phone_number" gorm:"type:varchar"`
|
||||||
|
WorkType *string `json:"work_type" gorm:"type:varchar"`
|
||||||
|
GenderType *string `json:"gender_type" gorm:"type:varchar"`
|
||||||
|
IdentityType *string `json:"identity_type" gorm:"type:varchar"`
|
||||||
|
IdentityGroup *string `json:"identity_group" gorm:"type:varchar"`
|
||||||
|
IdentityGroupNumber *string `json:"identity_group_number" gorm:"type:varchar"`
|
||||||
|
IdentityNumber *string `json:"identity_number" gorm:"type:varchar"`
|
||||||
|
DateOfBirth *string `json:"date_of_birth" gorm:"type:varchar"`
|
||||||
|
LastEducation *string `json:"last_education" gorm:"type:varchar"`
|
||||||
|
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
|
||||||
|
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
|
||||||
|
UserLevel *UserLevels `json:"user_levels" gorm:"foreignKey:UserLevelId;references:ID"`
|
||||||
|
KeycloakId *string `json:"keycloak_id" gorm:"type:varchar"`
|
||||||
|
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
ProfilePicturePath *string `json:"profile_picture_path" gorm:"type:varchar"`
|
||||||
|
TempPassword *string `json:"temp_password" gorm:"type:varchar"`
|
||||||
|
IsEmailUpdated *bool `json:"is_email_updated" gorm:"type:bool;default:false"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Users struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
Username string `json:"username" gorm:"type:varchar"`
|
||||||
|
Email string `json:"email" gorm:"type:varchar"`
|
||||||
|
Fullname string `json:"fullname" gorm:"type:varchar"`
|
||||||
|
Address *string `json:"address" gorm:"type:varchar"`
|
||||||
|
PhoneNumber *string `json:"phone_number" gorm:"type:varchar"`
|
||||||
|
WorkType *string `json:"work_type" gorm:"type:varchar"`
|
||||||
|
GenderType *string `json:"gender_type" gorm:"type:varchar"`
|
||||||
|
IdentityType *string `json:"identity_type" gorm:"type:varchar"`
|
||||||
|
IdentityGroup *string `json:"identity_group" gorm:"type:varchar"`
|
||||||
|
IdentityGroupNumber *string `json:"identity_group_number" gorm:"type:varchar"`
|
||||||
|
IdentityNumber *string `json:"identity_number" gorm:"type:varchar"`
|
||||||
|
DateOfBirth *string `json:"date_of_birth" gorm:"type:varchar"`
|
||||||
|
LastEducation *string `json:"last_education" gorm:"type:varchar"`
|
||||||
|
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
|
||||||
|
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
|
||||||
|
UserLevel *entity.UserLevels `json:"user_levels" gorm:"foreignKey:UserLevelId;references:ID"`
|
||||||
|
KeycloakId *string `json:"keycloak_id" gorm:"type:varchar"`
|
||||||
|
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
|
||||||
|
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||||
|
ProfilePicturePath *string `json:"profile_picture_path" gorm:"type:varchar"`
|
||||||
|
TempPassword *string `json:"temp_password" gorm:"type:varchar"`
|
||||||
|
IsEmailUpdated *bool `json:"is_email_updated" gorm:"type:bool;default:false"`
|
||||||
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
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()"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/database/entity/article_category_details"
|
||||||
|
"web-medols-be/config/config"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database setup database with gorm
|
||||||
|
type Database struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
Log zerolog.Logger
|
||||||
|
Cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Seeder interface {
|
||||||
|
Seed(*gorm.DB) error
|
||||||
|
Count(*gorm.DB) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatabase(cfg *config.Config, log zerolog.Logger) *Database {
|
||||||
|
db := &Database{
|
||||||
|
Cfg: cfg,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectDatabase connect database
|
||||||
|
func (_db *Database) ConnectDatabase() {
|
||||||
|
logMode := _db.Cfg.DB.Postgres.LogMode
|
||||||
|
var logLevel logger.LogLevel
|
||||||
|
if logMode == "INFO" {
|
||||||
|
logLevel = logger.Info
|
||||||
|
} else if logMode == "WARN" {
|
||||||
|
logLevel = logger.Warn
|
||||||
|
} else if logMode == "ERROR" {
|
||||||
|
logLevel = logger.Error
|
||||||
|
} else if logMode == "NONE" {
|
||||||
|
logLevel = logger.Silent
|
||||||
|
}
|
||||||
|
conn, err := gorm.Open(postgres.Open(_db.Cfg.DB.Postgres.DSN), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logLevel),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_db.Log.Error().Err(err).Msg("An unknown error occurred when to connect the database!")
|
||||||
|
} else {
|
||||||
|
_db.Log.Info().Msg("Connected the database succesfully!")
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.DB = conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShutdownDatabase shutdown database
|
||||||
|
func (_db *Database) ShutdownDatabase() {
|
||||||
|
sqlDB, err := _db.DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
_db.Log.Error().Err(err).Msg("An unknown error occurred when to shutdown the database!")
|
||||||
|
} else {
|
||||||
|
_db.Log.Info().Msg("Shutdown the database succesfully!")
|
||||||
|
}
|
||||||
|
sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateModels migrate models
|
||||||
|
func (_db *Database) MigrateModels() {
|
||||||
|
err := _db.DB.AutoMigrate(
|
||||||
|
Models()...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
_db.Log.Error().Err(err).Msg("An unknown error occurred when to migrate the database!")
|
||||||
|
} else {
|
||||||
|
_db.Log.Info().Msg("Migrate the database entity succesfully!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Models list of models for migration
|
||||||
|
func Models() []interface{} {
|
||||||
|
return []interface{}{
|
||||||
|
entity.ActivityLogs{},
|
||||||
|
entity.ActivityLogTypes{},
|
||||||
|
entity.Advertisement{},
|
||||||
|
entity.ApprovalWorkflows{},
|
||||||
|
entity.ApprovalWorkflowSteps{},
|
||||||
|
entity.Articles{},
|
||||||
|
entity.ArticleApprovalFlows{},
|
||||||
|
entity.ArticleApprovalStepLogs{},
|
||||||
|
entity.ArticleCategories{},
|
||||||
|
entity.ArticleApprovals{},
|
||||||
|
article_category_details.ArticleCategoryDetails{},
|
||||||
|
entity.ArticleFiles{},
|
||||||
|
entity.ArticleComments{},
|
||||||
|
entity.ArticleNulisAI{},
|
||||||
|
entity.AuditTrails{},
|
||||||
|
entity.Bookmarks{},
|
||||||
|
entity.Cities{},
|
||||||
|
entity.Clients{},
|
||||||
|
entity.ClientApprovalSettings{},
|
||||||
|
entity.CsrfTokenRecords{},
|
||||||
|
entity.CustomStaticPages{},
|
||||||
|
entity.Districts{},
|
||||||
|
entity.Feedbacks{},
|
||||||
|
entity.ForgotPasswords{},
|
||||||
|
entity.Magazines{},
|
||||||
|
entity.MagazineFiles{},
|
||||||
|
entity.MasterMenus{},
|
||||||
|
entity.MasterModules{},
|
||||||
|
entity.MasterStatuses{},
|
||||||
|
entity.MasterApprovalStatuses{},
|
||||||
|
entity.Provinces{},
|
||||||
|
entity.OneTimePasswords{},
|
||||||
|
entity.Subscription{},
|
||||||
|
entity.Schedules{},
|
||||||
|
entity.UserLevels{},
|
||||||
|
entity.UserRoles{},
|
||||||
|
entity.UserRoleAccesses{},
|
||||||
|
entity.Users{},
|
||||||
|
entity.UserRoleLevelDetails{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedModels seed data
|
||||||
|
func (_db *Database) SeedModels(seeder []Seeder) {
|
||||||
|
for _, seed := range seeder {
|
||||||
|
count, err := seed.Count(_db.DB)
|
||||||
|
if err != nil {
|
||||||
|
_db.Log.Error().Err(err).Msg("An unknown error occurred when to seed the database!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
if err := seed.Seed(_db.DB); err != nil {
|
||||||
|
_db.Log.Error().Err(err).Msg("An unknown error occurred when to seed the database!")
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.Log.Info().Msg("Seeded the database successfully!")
|
||||||
|
} else {
|
||||||
|
_db.Log.Info().Msg("Database is already seeded!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.Log.Info().Msg("Seeded the database succesfully!")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package seeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogsSeeder struct{}
|
||||||
|
|
||||||
|
var activityLogTypes = []entity.ActivityLogTypes{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Login",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "View",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
Name: "Share",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 4,
|
||||||
|
Name: "Comment",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ActivityLogsSeeder) Seed(conn *gorm.DB) error {
|
||||||
|
for _, row := range activityLogTypes {
|
||||||
|
if err := conn.Create(&row).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ActivityLogsSeeder) Count(conn *gorm.DB) (int, error) {
|
||||||
|
var count int64
|
||||||
|
if err := conn.Model(&entity.ActivityLogTypes{}).Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(count), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package seeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalWorkflowsSeeder struct{}
|
||||||
|
|
||||||
|
// Sample 3-level approval workflow
|
||||||
|
var approvalWorkflows = []entity.ApprovalWorkflows{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "3-Level Approval Workflow",
|
||||||
|
Description: &[]string{"Standard 3-level approval workflow for articles: Editor -> Senior Editor -> Chief Editor"}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "2-Level Approval Workflow",
|
||||||
|
Description: &[]string{"Simple 2-level approval workflow: Editor -> Chief Editor"}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample approval workflow steps for 3-level workflow
|
||||||
|
var approvalWorkflowSteps = []entity.ApprovalWorkflowSteps{
|
||||||
|
// 3-Level Workflow Steps
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
WorkflowId: 1,
|
||||||
|
StepOrder: 1,
|
||||||
|
StepName: "Editor Review",
|
||||||
|
RequiredUserLevelId: 3, // Assuming Editor user level ID is 3
|
||||||
|
CanSkip: &[]bool{false}[0],
|
||||||
|
AutoApproveAfterHours: &[]int{24}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
WorkflowId: 1,
|
||||||
|
StepOrder: 2,
|
||||||
|
StepName: "Senior Editor Review",
|
||||||
|
RequiredUserLevelId: 4, // Assuming Senior Editor user level ID is 4
|
||||||
|
CanSkip: &[]bool{false}[0],
|
||||||
|
AutoApproveAfterHours: &[]int{48}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
WorkflowId: 1,
|
||||||
|
StepOrder: 3,
|
||||||
|
StepName: "Chief Editor Final Approval",
|
||||||
|
RequiredUserLevelId: 5, // Assuming Chief Editor user level ID is 5
|
||||||
|
CanSkip: &[]bool{false}[0],
|
||||||
|
AutoApproveAfterHours: &[]int{72}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
},
|
||||||
|
// 2-Level Workflow Steps
|
||||||
|
{
|
||||||
|
ID: 4,
|
||||||
|
WorkflowId: 2,
|
||||||
|
StepOrder: 1,
|
||||||
|
StepName: "Editor Review",
|
||||||
|
RequiredUserLevelId: 3, // Editor user level
|
||||||
|
CanSkip: &[]bool{false}[0],
|
||||||
|
AutoApproveAfterHours: &[]int{24}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 5,
|
||||||
|
WorkflowId: 2,
|
||||||
|
StepOrder: 2,
|
||||||
|
StepName: "Chief Editor Approval",
|
||||||
|
RequiredUserLevelId: 5, // Chief Editor user level
|
||||||
|
CanSkip: &[]bool{false}[0],
|
||||||
|
AutoApproveAfterHours: &[]int{48}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ApprovalWorkflowsSeeder) Seed(conn *gorm.DB) error {
|
||||||
|
// Seed approval workflows
|
||||||
|
for _, workflow := range approvalWorkflows {
|
||||||
|
if err := conn.Create(&workflow).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed approval workflow steps
|
||||||
|
for _, step := range approvalWorkflowSteps {
|
||||||
|
if err := conn.Create(&step).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ApprovalWorkflowsSeeder) Count(conn *gorm.DB) (int, error) {
|
||||||
|
var workflowCount int64
|
||||||
|
if err := conn.Model(&entity.ApprovalWorkflows{}).Count(&workflowCount).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stepCount int64
|
||||||
|
if err := conn.Model(&entity.ApprovalWorkflowSteps{}).Count(&stepCount).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(workflowCount + stepCount), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package seeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterApprovalStatusesSeeder struct{}
|
||||||
|
|
||||||
|
var masterApprovalStatuses = []entity.MasterApprovalStatuses{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Accepted",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "Need Update",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
Name: "Rejected",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MasterApprovalStatusesSeeder) Seed(conn *gorm.DB) error {
|
||||||
|
for _, row := range masterApprovalStatuses {
|
||||||
|
if err := conn.Create(&row).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MasterApprovalStatusesSeeder) Count(conn *gorm.DB) (int, error) {
|
||||||
|
var count int64
|
||||||
|
if err := conn.Model(&entity.MasterApprovalStatuses{}).Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(count), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package seeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterStatusesSeeder struct{}
|
||||||
|
|
||||||
|
var masterStatuses = []entity.MasterStatuses{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Waiting",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "Active",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
Name: "Inactive",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MasterStatusesSeeder) Seed(conn *gorm.DB) error {
|
||||||
|
for _, row := range masterStatuses {
|
||||||
|
if err := conn.Create(&row).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MasterStatusesSeeder) Count(conn *gorm.DB) (int, error) {
|
||||||
|
var count int64
|
||||||
|
if err := conn.Model(&entity.MasterStatuses{}).Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(count), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package seeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateNoApprovalWorkflow creates a special workflow that bypasses approval
|
||||||
|
func CreateNoApprovalWorkflow() *entity.ApprovalWorkflows {
|
||||||
|
return &entity.ApprovalWorkflows{
|
||||||
|
Name: "No Approval Required",
|
||||||
|
Description: &[]string{"Workflow for clients that don't require approval process"}[0],
|
||||||
|
IsDefault: &[]bool{false}[0],
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
RequiresApproval: &[]bool{false}[0], // This is the key field
|
||||||
|
AutoPublish: &[]bool{true}[0], // Auto publish articles
|
||||||
|
Steps: []entity.ApprovalWorkflowSteps{}, // No steps needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateClientApprovalSettings creates default settings for a client
|
||||||
|
func CreateClientApprovalSettings(clientId string, requiresApproval bool) *entity.ClientApprovalSettings {
|
||||||
|
clientUUID, _ := uuid.Parse(clientId)
|
||||||
|
return &entity.ClientApprovalSettings{
|
||||||
|
ClientId: clientUUID,
|
||||||
|
RequiresApproval: &[]bool{requiresApproval}[0],
|
||||||
|
AutoPublishArticles: &[]bool{!requiresApproval}[0], // Auto publish if no approval needed
|
||||||
|
IsActive: &[]bool{true}[0],
|
||||||
|
ApprovalExemptUsers: []uint{},
|
||||||
|
ApprovalExemptRoles: []uint{},
|
||||||
|
ApprovalExemptCategories: []uint{},
|
||||||
|
RequireApprovalFor: []string{},
|
||||||
|
SkipApprovalFor: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,68 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
utilSvc "web-medols-be/utils/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AuditTrailsMiddleware(db *gorm.DB) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
start := time.Now()
|
||||||
|
requestBody := c.Body()
|
||||||
|
|
||||||
|
headersMap := c.GetReqHeaders()
|
||||||
|
headersJSON, _ := json.Marshal(headersMap)
|
||||||
|
|
||||||
|
authHeader := c.Get("Authorization")
|
||||||
|
userId := utilSvc.GetUserId(authHeader)
|
||||||
|
|
||||||
|
err := c.Next()
|
||||||
|
|
||||||
|
audit := entity.AuditTrails{
|
||||||
|
Method: c.Method(),
|
||||||
|
Path: c.OriginalURL(),
|
||||||
|
IP: getIP(c),
|
||||||
|
Status: c.Response().StatusCode(),
|
||||||
|
UserID: userId,
|
||||||
|
RequestHeaders: string(headersJSON),
|
||||||
|
RequestBody: string(requestBody),
|
||||||
|
ResponseBody: string(c.Response().Body()),
|
||||||
|
DurationMs: time.Since(start).Milliseconds(),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go db.Create(&audit)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartAuditTrailCleanup(db *gorm.DB, retention int) {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
time.Sleep(24 * time.Hour)
|
||||||
|
|
||||||
|
cutoff := time.Now().AddDate(0, 0, retention)
|
||||||
|
db.Where("created_at < ?", cutoff).Delete(&entity.AuditTrails{})
|
||||||
|
|
||||||
|
log.Printf(" at: %s", cutoff)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIP(c *fiber.Ctx) string {
|
||||||
|
ip := c.Get("X-Forwarded-For")
|
||||||
|
if ip == "" {
|
||||||
|
ip = c.IP()
|
||||||
|
}
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
ip = strings.Split(ip, ":")[0]
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClientKeyHeader = "X-Client-Key"
|
||||||
|
ClientContextKey = "client_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// excludedPaths contains paths that don't require client key validation
|
||||||
|
var excludedPaths = []string{
|
||||||
|
"/swagger/*",
|
||||||
|
"/docs/*",
|
||||||
|
"/users/login",
|
||||||
|
"/health/*",
|
||||||
|
"/clients",
|
||||||
|
"/clients/*",
|
||||||
|
"*/viewer/*",
|
||||||
|
"/bookmarks/test-table",
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPathExcluded checks if the given path should be excluded from client key validation
|
||||||
|
func isPathExcluded(path string) bool {
|
||||||
|
for _, excludedPath := range excludedPaths {
|
||||||
|
if strings.HasPrefix(excludedPath, "*") && strings.HasSuffix(excludedPath, "*") {
|
||||||
|
// Handle wildcard at both beginning and end (e.g., "*/viewer/*")
|
||||||
|
pattern := excludedPath[1 : len(excludedPath)-1] // Remove * from both ends
|
||||||
|
if strings.Contains(path, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(excludedPath, "*") {
|
||||||
|
// Handle wildcard at the beginning
|
||||||
|
if strings.HasSuffix(path, excludedPath[1:]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(excludedPath, "*") {
|
||||||
|
// Handle wildcard at the end
|
||||||
|
prefix := excludedPath[:len(excludedPath)-1]
|
||||||
|
if strings.HasPrefix(path, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exact match
|
||||||
|
if path == excludedPath {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientMiddleware extracts and validates the Client Key from request headers
|
||||||
|
func ClientMiddleware(db *gorm.DB) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Check if path should be excluded from client key validation
|
||||||
|
if isPathExcluded(c.Path()) {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Client Key from header
|
||||||
|
clientKey := c.Get(ClientKeyHeader)
|
||||||
|
|
||||||
|
if clientKey == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"success": false,
|
||||||
|
"code": 400,
|
||||||
|
"messages": []string{"Client Key is required in header: " + ClientKeyHeader},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse UUID
|
||||||
|
clientUUID, err := uuid.Parse(clientKey)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"success": false,
|
||||||
|
"code": 400,
|
||||||
|
"messages": []string{"Invalid Client Key format"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate client exists and is active
|
||||||
|
var client entity.Clients
|
||||||
|
if err := db.Where("id = ? AND is_active = ?", clientUUID, true).First(&client).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"success": false,
|
||||||
|
"code": 401,
|
||||||
|
"messages": []string{"Invalid or inactive Client Key"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"success": false,
|
||||||
|
"code": 500,
|
||||||
|
"messages": []string{"Error validating Client Key"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store client ID in context for use in handlers
|
||||||
|
c.Locals(ClientContextKey, clientUUID)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientID retrieves the client ID from the context
|
||||||
|
func GetClientID(c *fiber.Ctx) *uuid.UUID {
|
||||||
|
if clientID, ok := c.Locals(ClientContextKey).(uuid.UUID); ok {
|
||||||
|
return &clientID
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddExcludedPath adds a new path to the excluded paths list
|
||||||
|
func AddExcludedPath(path string) {
|
||||||
|
excludedPaths = append(excludedPaths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExcludedPaths returns the current list of excluded paths
|
||||||
|
func GetExcludedPaths() []string {
|
||||||
|
return excludedPaths
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostgresStorage struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStorage) Get(key string) ([]byte, error) {
|
||||||
|
//log.Printf("CSRF Storage: Get token %s", key)
|
||||||
|
|
||||||
|
var record entity.CsrfTokenRecords
|
||||||
|
result := s.DB.Where("token = ?", key).First(&record)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
//log.Printf("CSRF Storage Get error: %v for token: %s", result.Error, key)
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.ExpireAt.Before(time.Now()) {
|
||||||
|
//log.Printf("CSRF token %s is expired", key)
|
||||||
|
return nil, fmt.Errorf("CSRF token is expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStorage) Set(key string, value []byte, exp time.Duration) error {
|
||||||
|
//log.Printf("CSRF Storage: Setting token %s with expiration %v", key, exp)
|
||||||
|
|
||||||
|
// Calculate expiration time
|
||||||
|
expireAt := time.Now().Add(exp)
|
||||||
|
|
||||||
|
// Try to update existing record first
|
||||||
|
result := s.DB.Model(&entity.CsrfTokenRecords{}).
|
||||||
|
Where("token = ?", key).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"expire_at": expireAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
// If no rows were affected (not found), create a new record
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
record := entity.CsrfTokenRecords{
|
||||||
|
Token: key,
|
||||||
|
Value: value,
|
||||||
|
ExpireAt: expireAt,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.DB.Create(&record).Error; err != nil {
|
||||||
|
//log.Printf("CSRF Storage: Error saving token: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if result.Error != nil {
|
||||||
|
//log.Printf("CSRF Storage: Error updating token: %v", result.Error)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("CSRF Storage: Successfully saved/updated token")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStorage) Delete(key string) error {
|
||||||
|
return s.DB.Where("token = ?", key).Delete(&entity.CsrfTokenRecords{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStorage) Reset() error {
|
||||||
|
return s.DB.Where("expire_at < ?", time.Now()).Delete(&entity.CsrfTokenRecords{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStorage) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database"
|
||||||
|
"web-medols-be/config/config"
|
||||||
|
utilsSvc "web-medols-be/utils"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/csrf"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/session"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/monitor"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/pprof"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
|
"github.com/gofiber/fiber/v2/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware is a struct that contains all the middleware functions
|
||||||
|
type Middleware struct {
|
||||||
|
App *fiber.App
|
||||||
|
Cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMiddleware(app *fiber.App, cfg *config.Config) *Middleware {
|
||||||
|
return &Middleware{
|
||||||
|
App: app,
|
||||||
|
Cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register registers all the middleware functions
|
||||||
|
func (m *Middleware) Register(db *database.Database) {
|
||||||
|
// Add Extra Middlewares
|
||||||
|
|
||||||
|
m.App.Use(limiter.New(limiter.Config{
|
||||||
|
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Limiter.Enable),
|
||||||
|
Max: m.Cfg.Middleware.Limiter.Max,
|
||||||
|
Expiration: m.Cfg.Middleware.Limiter.Expiration * time.Second,
|
||||||
|
}))
|
||||||
|
|
||||||
|
m.App.Use(compress.New(compress.Config{
|
||||||
|
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Compress.Enable),
|
||||||
|
Level: m.Cfg.Middleware.Compress.Level,
|
||||||
|
}))
|
||||||
|
|
||||||
|
m.App.Use(recover.New(recover.Config{
|
||||||
|
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Recover.Enable),
|
||||||
|
}))
|
||||||
|
|
||||||
|
m.App.Use(pprof.New(pprof.Config{
|
||||||
|
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Pprof.Enable),
|
||||||
|
}))
|
||||||
|
|
||||||
|
m.App.Use(cors.New(cors.Config{
|
||||||
|
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Cors.Enable),
|
||||||
|
AllowOrigins: "http://localhost:3000, http://localhost:4000, https://dev.mikulnews.com, https://n8n.qudoco.com, https://narasiahli.com, https://dev.asuransiaman.com, https://dev.beritabumn.com, https://dev.kabarharapan.com, https://dev.kebaikanindonesia.com, https://dev.isukini.com",
|
||||||
|
AllowMethods: "HEAD, GET, POST, PUT, DELETE, OPTION, PATCH",
|
||||||
|
AllowHeaders: "Origin, Content-Type, Accept, Accept-Language, Authorization, X-Requested-With, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, X-Csrf-Token, Cookie, Set-Cookie, X-Client-Key",
|
||||||
|
ExposeHeaders: "Content-Length, Content-Type",
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 12,
|
||||||
|
}))
|
||||||
|
|
||||||
|
//===============================
|
||||||
|
// CSRF CONFIG
|
||||||
|
//===============================
|
||||||
|
|
||||||
|
// Custom storage for CSRF
|
||||||
|
csrfSessionStorage := &PostgresStorage{
|
||||||
|
DB: db.DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store initialization for session
|
||||||
|
store := session.New(session.Config{
|
||||||
|
CookieSameSite: m.Cfg.Middleware.Csrf.CookieSameSite,
|
||||||
|
CookieSecure: m.Cfg.Middleware.Csrf.CookieSecure,
|
||||||
|
CookieSessionOnly: m.Cfg.Middleware.Csrf.CookieSessionOnly,
|
||||||
|
CookieHTTPOnly: m.Cfg.Middleware.Csrf.CookieHttpOnly,
|
||||||
|
Storage: csrfSessionStorage,
|
||||||
|
})
|
||||||
|
|
||||||
|
m.App.Use(func(c *fiber.Ctx) error {
|
||||||
|
sess, err := store.Get(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Locals("session", sess)
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup the expired token
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if err := csrfSessionStorage.Reset(); err != nil {
|
||||||
|
log.Printf("Error cleaning up expired CSRF tokens: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
m.App.Use(csrf.New(csrf.Config{
|
||||||
|
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Csrf.Enable),
|
||||||
|
KeyLookup: "header:" + csrf.HeaderName,
|
||||||
|
CookieName: m.Cfg.Middleware.Csrf.CookieName,
|
||||||
|
CookieSameSite: m.Cfg.Middleware.Csrf.CookieSameSite,
|
||||||
|
CookieSecure: m.Cfg.Middleware.Csrf.CookieSecure,
|
||||||
|
CookieSessionOnly: m.Cfg.Middleware.Csrf.CookieSessionOnly,
|
||||||
|
CookieHTTPOnly: m.Cfg.Middleware.Csrf.CookieHttpOnly,
|
||||||
|
Expiration: 1 * time.Hour,
|
||||||
|
KeyGenerator: utils.UUIDv4,
|
||||||
|
ContextKey: "csrf",
|
||||||
|
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||||
|
return utilsSvc.CsrfErrorHandler(c, err)
|
||||||
|
},
|
||||||
|
Extractor: csrf.CsrfFromHeader(csrf.HeaderName),
|
||||||
|
Session: store,
|
||||||
|
SessionKey: "fiber.csrf.token",
|
||||||
|
}))
|
||||||
|
|
||||||
|
//===============================
|
||||||
|
|
||||||
|
// Client middleware - must be applied before other business logic
|
||||||
|
m.App.Use(ClientMiddleware(db.DB))
|
||||||
|
|
||||||
|
m.App.Use(AuditTrailsMiddleware(db.DB))
|
||||||
|
// StartAuditTrailCleanup(db.DB, m.Cfg.Middleware.AuditTrails.Retention)
|
||||||
|
|
||||||
|
//m.App.Use(filesystem.New(filesystem.Config{
|
||||||
|
// Next: utils.IsEnabled(m.Cfg.Middleware.FileSystem.Enable),
|
||||||
|
// Root: http.Dir(m.Cfg.Middleware.FileSystem.Root),
|
||||||
|
// Browse: m.Cfg.Middleware.FileSystem.Browse,
|
||||||
|
// MaxAge: m.Cfg.Middleware.FileSystem.MaxAge,
|
||||||
|
//}))
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
m.App.Get(m.Cfg.Middleware.Monitor.Path, monitor.New(monitor.Config{
|
||||||
|
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Monitor.Enable),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Route for generate CSRF token
|
||||||
|
m.App.Get("/csrf-token", func(c *fiber.Ctx) error {
|
||||||
|
// Retrieve CSRF token from Fiber's middleware context
|
||||||
|
token, ok := c.Locals("csrf").(string)
|
||||||
|
|
||||||
|
//c.Context().VisitUserValues(func(key []byte, value interface{}) {
|
||||||
|
// log.Printf("Local Key: %s, Value: %v", key, value)
|
||||||
|
//})
|
||||||
|
|
||||||
|
if !ok || token == "" {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"success": false,
|
||||||
|
"code": 500,
|
||||||
|
"messages": []string{"Failed to retrieve CSRF token"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"success": true,
|
||||||
|
"csrf_token": token,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/database/entity/users"
|
||||||
|
"web-medols-be/app/module/users/repository"
|
||||||
|
utilSvc "web-medols-be/utils/service"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserContextKey = "user"
|
||||||
|
UserLevelContextKey = "user_level_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserMiddleware extracts user information from JWT token and stores in context
|
||||||
|
func UserMiddleware(usersRepo repository.UsersRepository) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Skip if no Authorization header
|
||||||
|
authHeader := c.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user info from token
|
||||||
|
// Create a default logger if not available in context
|
||||||
|
log := zerolog.Nop()
|
||||||
|
if logFromCtx, ok := c.Locals("log").(zerolog.Logger); ok {
|
||||||
|
log = logFromCtx
|
||||||
|
}
|
||||||
|
user := utilSvc.GetUserInfo(log, usersRepo, authHeader)
|
||||||
|
if user != nil {
|
||||||
|
// Store user in context
|
||||||
|
c.Locals(UserContextKey, user)
|
||||||
|
c.Locals(UserLevelContextKey, user.UserLevelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser retrieves the user from the context
|
||||||
|
func GetUser(c *fiber.Ctx) *users.Users {
|
||||||
|
if user, ok := c.Locals(UserContextKey).(*users.Users); ok {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserLevelID retrieves the user level ID from the context
|
||||||
|
func GetUserLevelID(c *fiber.Ctx) *uint {
|
||||||
|
if userLevelId, ok := c.Locals(UserLevelContextKey).(uint); ok {
|
||||||
|
return &userLevelId
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package activity_logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/module/activity_logs/controller"
|
||||||
|
"web-medols-be/app/module/activity_logs/repository"
|
||||||
|
"web-medols-be/app/module/activity_logs/service"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// struct of ActivityLogsRouter
|
||||||
|
type ActivityLogsRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller *controller.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// register bulky of ActivityLogs module
|
||||||
|
var NewActivityLogsModule = fx.Options(
|
||||||
|
// register repository of ActivityLogs module
|
||||||
|
fx.Provide(repository.NewActivityLogsRepository),
|
||||||
|
|
||||||
|
// register service of ActivityLogs module
|
||||||
|
fx.Provide(service.NewActivityLogsService),
|
||||||
|
|
||||||
|
// register controller of ActivityLogs module
|
||||||
|
fx.Provide(controller.NewController),
|
||||||
|
|
||||||
|
// register router of ActivityLogs module
|
||||||
|
fx.Provide(NewActivityLogsRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// init ActivityLogsRouter
|
||||||
|
func NewActivityLogsRouter(fiber *fiber.App, controller *controller.Controller) *ActivityLogsRouter {
|
||||||
|
return &ActivityLogsRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register routes of ActivityLogs module
|
||||||
|
func (_i *ActivityLogsRouter) RegisterActivityLogsRoutes() {
|
||||||
|
// define controllers
|
||||||
|
activityLogsController := _i.Controller.ActivityLogs
|
||||||
|
|
||||||
|
// define routes
|
||||||
|
_i.App.Route("/activity-logs", func(router fiber.Router) {
|
||||||
|
router.Get("/", activityLogsController.All)
|
||||||
|
router.Get("/statistics", activityLogsController.GetActivityStats)
|
||||||
|
router.Get("/detail/:id", activityLogsController.Show)
|
||||||
|
router.Post("/", activityLogsController.Save)
|
||||||
|
router.Put("/:id", activityLogsController.Update)
|
||||||
|
router.Delete("/:id", activityLogsController.Delete)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"web-medols-be/app/middleware"
|
||||||
|
"web-medols-be/app/module/activity_logs/request"
|
||||||
|
"web-medols-be/app/module/activity_logs/service"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
utilRes "web-medols-be/utils/response"
|
||||||
|
utilVal "web-medols-be/utils/validator"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activityLogsController struct {
|
||||||
|
activityLogsService service.ActivityLogsService
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogsController interface {
|
||||||
|
All(c *fiber.Ctx) error
|
||||||
|
Show(c *fiber.Ctx) error
|
||||||
|
Save(c *fiber.Ctx) error
|
||||||
|
Update(c *fiber.Ctx) error
|
||||||
|
Delete(c *fiber.Ctx) error
|
||||||
|
GetActivityStats(c *fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityLogsController(activityLogsService service.ActivityLogsService, log zerolog.Logger) ActivityLogsController {
|
||||||
|
return &activityLogsController{
|
||||||
|
activityLogsService: activityLogsService,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All get all ActivityLogs
|
||||||
|
// @Summary Get all ActivityLogs
|
||||||
|
// @Description API for getting all ActivityLogs
|
||||||
|
// @Tags ActivityLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param req query request.ActivityLogsQueryRequest false "query parameters"
|
||||||
|
// @Param req query paginator.Pagination false "pagination parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /activity-logs [get]
|
||||||
|
func (_i *activityLogsController) All(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
reqContext := request.ActivityLogsQueryRequestContext{
|
||||||
|
ActivityTypeId: c.Query("activityTypeId"),
|
||||||
|
Url: c.Query("url"),
|
||||||
|
ArticleId: c.Query("articleId"),
|
||||||
|
UserId: c.Query("userId"),
|
||||||
|
}
|
||||||
|
req := reqContext.ToParamRequest()
|
||||||
|
req.Pagination = paginate
|
||||||
|
|
||||||
|
activityLogsData, paging, err := _i.activityLogsService.All(clientId, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ActivityLogs list successfully retrieved"},
|
||||||
|
Data: activityLogsData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show get one ActivityLogs
|
||||||
|
// @Summary Get one ActivityLogs
|
||||||
|
// @Description API for getting one ActivityLogs
|
||||||
|
// @Tags ActivityLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ActivityLogs ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /activity-logs/detail/{id} [get]
|
||||||
|
func (_i *activityLogsController) Show(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
activityLogsData, err := _i.activityLogsService.Show(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ActivityLogs successfully retrieved"},
|
||||||
|
Data: activityLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save create ActivityLogs
|
||||||
|
// @Summary Create ActivityLogs
|
||||||
|
// @Description API for create ActivityLogs
|
||||||
|
// @Tags ActivityLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param payload body request.ActivityLogsCreateRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /activity-logs [post]
|
||||||
|
func (_i *activityLogsController) Save(c *fiber.Ctx) error {
|
||||||
|
req := new(request.ActivityLogsCreateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
var authToken *string
|
||||||
|
getTokenFromHeader := c.Get("Authorization")
|
||||||
|
if getTokenFromHeader == "" {
|
||||||
|
authToken = nil
|
||||||
|
} else {
|
||||||
|
authToken = &getTokenFromHeader
|
||||||
|
}
|
||||||
|
visitorIp := GetVisitorIP(c)
|
||||||
|
req.VisitorIp = &visitorIp
|
||||||
|
dataResult, err := _i.activityLogsService.Save(clientId, *req, authToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ActivityLogs successfully created"},
|
||||||
|
Data: dataResult,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update update ActivityLogs
|
||||||
|
// @Summary update ActivityLogs
|
||||||
|
// @Description API for update ActivityLogs
|
||||||
|
// @Tags ActivityLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @Param payload body request.ActivityLogsUpdateRequest true "Required payload"
|
||||||
|
// @Param id path int true "ActivityLogs ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /activity-logs/{id} [put]
|
||||||
|
func (_i *activityLogsController) Update(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ActivityLogsUpdateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.activityLogsService.Update(clientId, uint(id), *req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ActivityLogs successfully updated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete delete ActivityLogs
|
||||||
|
// @Summary delete ActivityLogs
|
||||||
|
// @Description API for delete ActivityLogs
|
||||||
|
// @Tags ActivityLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @Param id path int true "ActivityLogs ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /activity-logs/{id} [delete]
|
||||||
|
func (_i *activityLogsController) Delete(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.activityLogsService.Delete(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ActivityLogs successfully deleted"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActivityStats get activity stats ActivityLogs
|
||||||
|
// @Summary Get activity stats ActivityLogs
|
||||||
|
// @Description API for get activity stats ActivityLogs
|
||||||
|
// @Tags ActivityLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /activity-logs/statistics [get]
|
||||||
|
func (_i *activityLogsController) GetActivityStats(c *fiber.Ctx) error {
|
||||||
|
_i.Log.Info().Interface("GetActivityStats", "checker controller").Msg("")
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
activityStatsData, err := _i.activityLogsService.GetActivityStats(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ActivityLogs Stats successfully retrieved"},
|
||||||
|
Data: activityStatsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVisitorIP(c *fiber.Ctx) string {
|
||||||
|
ip := c.Get("X-Forwarded-For")
|
||||||
|
if ip == "" {
|
||||||
|
ip = c.IP()
|
||||||
|
}
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
ip = strings.Split(ip, ":")[0]
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/module/activity_logs/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
ActivityLogs ActivityLogsController
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(ActivityLogsService service.ActivityLogsService, log zerolog.Logger) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
ActivityLogs: NewActivityLogsController(ActivityLogsService, log),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
res "web-medols-be/app/module/activity_logs/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ActivityLogsResponseMapper(activityLogsReq *entity.ActivityLogs) (activityLogsRes *res.ActivityLogsResponse) {
|
||||||
|
if activityLogsReq != nil {
|
||||||
|
activityLogsRes = &res.ActivityLogsResponse{
|
||||||
|
ID: activityLogsReq.ID,
|
||||||
|
ActivityTypeId: activityLogsReq.ActivityTypeId,
|
||||||
|
Url: activityLogsReq.Url,
|
||||||
|
ArticleId: activityLogsReq.ArticleId,
|
||||||
|
UserId: activityLogsReq.UserId,
|
||||||
|
CreatedAt: activityLogsReq.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activityLogsRes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/activity_logs/request"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activityLogsRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityLogsRepository define interface of IActivityLogsRepository
|
||||||
|
type ActivityLogsRepository interface {
|
||||||
|
GetAll(clientId *uuid.UUID, req request.ActivityLogsQueryRequest) (activityLogss []*entity.ActivityLogs, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (activityLogs *entity.ActivityLogs, err error)
|
||||||
|
Create(activityLogs *entity.ActivityLogs) (activityLogsReturn *entity.ActivityLogs, err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, activityLogs *entity.ActivityLogs) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
CountUniqueVisitorAllTime(clientId *uuid.UUID) (count int64, err error)
|
||||||
|
CountUniqueVisitorToday(clientId *uuid.UUID) (count int64, err error)
|
||||||
|
CountTotalViewAllTime(clientId *uuid.UUID) (count int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityLogsRepository(db *database.Database, logger zerolog.Logger) ActivityLogsRepository {
|
||||||
|
return &activityLogsRepository{
|
||||||
|
DB: db,
|
||||||
|
Log: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement interface of IActivityLogsRepository
|
||||||
|
func (_i *activityLogsRepository) GetAll(clientId *uuid.UUID, req request.ActivityLogsQueryRequest) (activityLogss []*entity.ActivityLogs, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ActivityLogs{})
|
||||||
|
|
||||||
|
// Add ClientId filter
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ActivityTypeId != nil {
|
||||||
|
query = query.Where("activity_type_id = ?", req.ActivityTypeId)
|
||||||
|
}
|
||||||
|
if req.Url != nil && *req.Url != "" {
|
||||||
|
url := strings.ToLower(*req.Url)
|
||||||
|
query = query.Where("LOWER(url) LIKE ?", "%"+strings.ToLower(url)+"%")
|
||||||
|
}
|
||||||
|
if req.ArticleId != nil {
|
||||||
|
query = query.Where("article_id = ?", req.ArticleId)
|
||||||
|
}
|
||||||
|
if req.UserId != nil {
|
||||||
|
query = query.Where("user_id = ?", req.UserId)
|
||||||
|
}
|
||||||
|
query.Count(&count)
|
||||||
|
|
||||||
|
if req.Pagination.SortBy != "" {
|
||||||
|
direction := "ASC"
|
||||||
|
if req.Pagination.Sort == "desc" {
|
||||||
|
direction = "DESC"
|
||||||
|
}
|
||||||
|
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Pagination.Count = count
|
||||||
|
req.Pagination = paginator.Paging(req.Pagination)
|
||||||
|
|
||||||
|
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&activityLogss).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = *req.Pagination
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsRepository) FindOne(clientId *uuid.UUID, id uint) (activityLogs *entity.ActivityLogs, err error) {
|
||||||
|
query := _i.DB.DB.Where("id = ?", id)
|
||||||
|
|
||||||
|
// Add ClientId filter
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.First(&activityLogs).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return activityLogs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsRepository) Create(activityLogs *entity.ActivityLogs) (activityLogsReturn *entity.ActivityLogs, err error) {
|
||||||
|
result := _i.DB.DB.Create(activityLogs)
|
||||||
|
return activityLogs, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsRepository) Update(clientId *uuid.UUID, id uint, activityLogs *entity.ActivityLogs) (err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ActivityLogs{}).Where(&entity.ActivityLogs{ID: id})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
return query.Updates(activityLogs).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsRepository) Delete(clientId *uuid.UUID, id uint) error {
|
||||||
|
query := _i.DB.DB.Model(&entity.ActivityLogs{}).Where("id = ?", id)
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
return query.Delete(&entity.ActivityLogs{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsRepository) CountUniqueVisitorAllTime(clientId *uuid.UUID) (count int64, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ActivityLogs{})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
err = query.Distinct("visitor_ip").Count(&count).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsRepository) CountTotalViewAllTime(clientId *uuid.UUID) (count int64, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ActivityLogs{}).Where("activity_type_id = ?", 2)
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsRepository) CountUniqueVisitorToday(clientId *uuid.UUID) (count int64, err error) {
|
||||||
|
tenMinutesAgo := time.Now().Add(-10 * time.Minute)
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.AuditTrails{}).Where("created_at >= ?", tenMinutesAgo)
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
err = query.Select("ip").Group("ip").Count(&count).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogsGeneric interface {
|
||||||
|
ToEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogsQueryRequest struct {
|
||||||
|
ActivityTypeId *int `json:"activityTypeId"`
|
||||||
|
Url *string `json:"url"`
|
||||||
|
ArticleId *int `json:"articleId"`
|
||||||
|
UserId *int `json:"userId"`
|
||||||
|
Pagination *paginator.Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogsCreateRequest struct {
|
||||||
|
ActivityTypeId int `json:"activityTypeId" validate:"required"`
|
||||||
|
Url string `json:"url" validate:"required"`
|
||||||
|
ArticleId *uint `json:"articleId"`
|
||||||
|
UserId *uint `json:"userId"`
|
||||||
|
VisitorIp *string `json:"visitorIp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ActivityLogsCreateRequest) ToEntity() *entity.ActivityLogs {
|
||||||
|
return &entity.ActivityLogs{
|
||||||
|
ActivityTypeId: req.ActivityTypeId,
|
||||||
|
Url: req.Url,
|
||||||
|
ArticleId: req.ArticleId,
|
||||||
|
UserId: req.UserId,
|
||||||
|
VisitorIp: req.VisitorIp,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogsUpdateRequest struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
ActivityTypeId int `json:"activityTypeId" validate:"required"`
|
||||||
|
Url string `json:"url" validate:"required"`
|
||||||
|
ArticleId *uint `json:"articleId"`
|
||||||
|
UserId *uint `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ActivityLogsUpdateRequest) ToEntity() *entity.ActivityLogs {
|
||||||
|
return &entity.ActivityLogs{
|
||||||
|
ID: req.ID,
|
||||||
|
ActivityTypeId: req.ActivityTypeId,
|
||||||
|
Url: req.Url,
|
||||||
|
ArticleId: req.ArticleId,
|
||||||
|
UserId: req.UserId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogsQueryRequestContext struct {
|
||||||
|
ActivityTypeId string `json:"activityTypeId"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
ArticleId string `json:"articleId"`
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ActivityLogsQueryRequestContext) ToParamRequest() ActivityLogsQueryRequest {
|
||||||
|
var request ActivityLogsQueryRequest
|
||||||
|
|
||||||
|
if activityTypeIdStr := req.ActivityTypeId; activityTypeIdStr != "" {
|
||||||
|
activityTypeId, err := strconv.Atoi(activityTypeIdStr)
|
||||||
|
if err == nil {
|
||||||
|
request.ActivityTypeId = &activityTypeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url := req.Url; url != "" {
|
||||||
|
request.Url = &url
|
||||||
|
}
|
||||||
|
if articleIdStr := req.ArticleId; articleIdStr != "" {
|
||||||
|
articleId, err := strconv.Atoi(articleIdStr)
|
||||||
|
if err == nil {
|
||||||
|
request.ArticleId = &articleId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userIdStr := req.UserId; userIdStr != "" {
|
||||||
|
userId, err := strconv.Atoi(userIdStr)
|
||||||
|
if err == nil {
|
||||||
|
request.UserId = &userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ActivityLogsResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ActivityTypeId int `json:"activityTypeId"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
ArticleId *uint `json:"articleId"`
|
||||||
|
UserId *uint `json:"userId"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityStatsResponse struct {
|
||||||
|
TotalVisitorAllTime int64 `json:"totalVisitorAllTime"`
|
||||||
|
TotalVisitorToday int64 `json:"totalVisitorToday"`
|
||||||
|
TotalViewAllTime int64 `json:"totalViewAllTime"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/activity_logs/mapper"
|
||||||
|
"web-medols-be/app/module/activity_logs/repository"
|
||||||
|
"web-medols-be/app/module/activity_logs/request"
|
||||||
|
"web-medols-be/app/module/activity_logs/response"
|
||||||
|
"web-medols-be/app/module/articles/service"
|
||||||
|
usersRepository "web-medols-be/app/module/users/repository"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
utilSvc "web-medols-be/utils/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ActivityLogsService
|
||||||
|
type activityLogsService struct {
|
||||||
|
Repo repository.ActivityLogsRepository
|
||||||
|
UsersRepo usersRepository.UsersRepository
|
||||||
|
ArticleService service.ArticlesService
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityLogsService define interface of IActivityLogsService
|
||||||
|
type ActivityLogsService interface {
|
||||||
|
All(clientId *uuid.UUID, req request.ActivityLogsQueryRequest) (activityLogs []*response.ActivityLogsResponse, paging paginator.Pagination, err error)
|
||||||
|
Show(clientId *uuid.UUID, id uint) (activityLogs *response.ActivityLogsResponse, err error)
|
||||||
|
Save(clientId *uuid.UUID, req request.ActivityLogsCreateRequest, authToken *string) (activityLogs *entity.ActivityLogs, err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, req request.ActivityLogsUpdateRequest) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) error
|
||||||
|
GetActivityStats(clientId *uuid.UUID) (activityStats *response.ActivityStatsResponse, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewActivityLogsService init ActivityLogsService
|
||||||
|
func NewActivityLogsService(repo repository.ActivityLogsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, articleService service.ArticlesService) ActivityLogsService {
|
||||||
|
|
||||||
|
return &activityLogsService{
|
||||||
|
Repo: repo,
|
||||||
|
Log: log,
|
||||||
|
UsersRepo: usersRepo,
|
||||||
|
ArticleService: articleService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All implement interface of ActivityLogsService
|
||||||
|
func (_i *activityLogsService) All(clientId *uuid.UUID, req request.ActivityLogsQueryRequest) (activityLogss []*response.ActivityLogsResponse, paging paginator.Pagination, err error) {
|
||||||
|
results, paging, err := _i.Repo.GetAll(clientId, req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
activityLogss = append(activityLogss, mapper.ActivityLogsResponseMapper(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsService) Show(clientId *uuid.UUID, id uint) (activityLogs *response.ActivityLogsResponse, err error) {
|
||||||
|
result, err := _i.Repo.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapper.ActivityLogsResponseMapper(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsService) Save(clientId *uuid.UUID, req request.ActivityLogsCreateRequest, authToken *string) (activityLogs *entity.ActivityLogs, err error) {
|
||||||
|
_i.Log.Info().Interface("data", req).Msg("")
|
||||||
|
|
||||||
|
newReq := req.ToEntity()
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
newReq.ClientId = clientId
|
||||||
|
}
|
||||||
|
|
||||||
|
if authToken != nil {
|
||||||
|
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, *authToken)
|
||||||
|
newReq.UserId = &createdBy.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := _i.Repo.Create(newReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update article
|
||||||
|
err = _i.ArticleService.UpdateActivityCount(clientId, *req.ArticleId, req.ActivityTypeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsService) Update(clientId *uuid.UUID, id uint, req request.ActivityLogsUpdateRequest) (err error) {
|
||||||
|
_i.Log.Info().Interface("data", req).Msg("")
|
||||||
|
newReq := req.ToEntity()
|
||||||
|
if clientId != nil {
|
||||||
|
newReq.ClientId = clientId
|
||||||
|
}
|
||||||
|
return _i.Repo.Update(clientId, id, newReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsService) Delete(clientId *uuid.UUID, id uint) error {
|
||||||
|
return _i.Repo.Delete(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *activityLogsService) GetActivityStats(clientId *uuid.UUID) (activityStats *response.ActivityStatsResponse, err error) {
|
||||||
|
_i.Log.Info().Interface("GetActivityStats", "checker").Msg("")
|
||||||
|
countUniqueVisitorAllTime, err := _i.Repo.CountUniqueVisitorAllTime(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
countUniqueVisitorToday, err := _i.Repo.CountUniqueVisitorToday(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
countTotalViewAllTime, err := _i.Repo.CountTotalViewAllTime(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
getActivityStats := &response.ActivityStatsResponse{
|
||||||
|
TotalVisitorAllTime: countUniqueVisitorAllTime,
|
||||||
|
TotalVisitorToday: countUniqueVisitorToday,
|
||||||
|
TotalViewAllTime: countTotalViewAllTime,
|
||||||
|
}
|
||||||
|
return getActivityStats, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package advertisement
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"web-medols-be/app/module/advertisement/controller"
|
||||||
|
"web-medols-be/app/module/advertisement/repository"
|
||||||
|
"web-medols-be/app/module/advertisement/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// struct of AdvertisementRouter
|
||||||
|
type AdvertisementRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller *controller.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// register bulky of Advertisement module
|
||||||
|
var NewAdvertisementModule = fx.Options(
|
||||||
|
// register repository of Advertisement module
|
||||||
|
fx.Provide(repository.NewAdvertisementRepository),
|
||||||
|
|
||||||
|
// register service of Advertisement module
|
||||||
|
fx.Provide(service.NewAdvertisementService),
|
||||||
|
|
||||||
|
// register controller of Advertisement module
|
||||||
|
fx.Provide(controller.NewController),
|
||||||
|
|
||||||
|
// register router of Advertisement module
|
||||||
|
fx.Provide(NewAdvertisementRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// init AdvertisementRouter
|
||||||
|
func NewAdvertisementRouter(fiber *fiber.App, controller *controller.Controller) *AdvertisementRouter {
|
||||||
|
return &AdvertisementRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register routes of Advertisement module
|
||||||
|
func (_i *AdvertisementRouter) RegisterAdvertisementRoutes() {
|
||||||
|
// define controllers
|
||||||
|
advertisementController := _i.Controller.Advertisement
|
||||||
|
|
||||||
|
// define routes
|
||||||
|
_i.App.Route("/advertisement", func(router fiber.Router) {
|
||||||
|
router.Get("/", advertisementController.All)
|
||||||
|
router.Get("/:id", advertisementController.Show)
|
||||||
|
router.Post("/", advertisementController.Save)
|
||||||
|
router.Post("/upload/:id", advertisementController.Upload)
|
||||||
|
router.Get("/viewer/:filename", advertisementController.Viewer)
|
||||||
|
router.Put("/:id", advertisementController.Update)
|
||||||
|
router.Put("/publish/:id", advertisementController.UpdatePublish)
|
||||||
|
router.Delete("/:id", advertisementController.Delete)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"web-medols-be/app/middleware"
|
||||||
|
"web-medols-be/app/module/advertisement/request"
|
||||||
|
"web-medols-be/app/module/advertisement/service"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
|
utilRes "web-medols-be/utils/response"
|
||||||
|
utilVal "web-medols-be/utils/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type advertisementController struct {
|
||||||
|
advertisementService service.AdvertisementService
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdvertisementController interface {
|
||||||
|
All(c *fiber.Ctx) error
|
||||||
|
Show(c *fiber.Ctx) error
|
||||||
|
Save(c *fiber.Ctx) error
|
||||||
|
Upload(c *fiber.Ctx) error
|
||||||
|
Update(c *fiber.Ctx) error
|
||||||
|
UpdatePublish(c *fiber.Ctx) error
|
||||||
|
Delete(c *fiber.Ctx) error
|
||||||
|
Viewer(c *fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdvertisementController(advertisementService service.AdvertisementService, log zerolog.Logger) AdvertisementController {
|
||||||
|
return &advertisementController{
|
||||||
|
advertisementService: advertisementService,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All get all Advertisement
|
||||||
|
// @Summary Get all Advertisement
|
||||||
|
// @Description API for getting all Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param req query request.AdvertisementQueryRequest false "query parameters"
|
||||||
|
// @Param req query paginator.Pagination false "pagination parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /advertisement [get]
|
||||||
|
func (_i *advertisementController) All(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
reqContext := request.AdvertisementQueryRequestContext{
|
||||||
|
Title: c.Query("title"),
|
||||||
|
Description: c.Query("description"),
|
||||||
|
RedirectLink: c.Query("redirectLink"),
|
||||||
|
Placement: c.Query("placement"),
|
||||||
|
StatusId: c.Query("statusId"),
|
||||||
|
}
|
||||||
|
req := reqContext.ToParamRequest()
|
||||||
|
req.Pagination = paginate
|
||||||
|
|
||||||
|
advertisementData, paging, err := _i.advertisementService.All(clientId, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Advertisement list successfully retrieved"},
|
||||||
|
Data: advertisementData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show get one Advertisement
|
||||||
|
// @Summary Get one Advertisement
|
||||||
|
// @Description API for getting one Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @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/{id} [get]
|
||||||
|
func (_i *advertisementController) Show(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
advertisementData, err := _i.advertisementService.Show(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Advertisement successfully retrieved"},
|
||||||
|
Data: advertisementData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save create Advertisement
|
||||||
|
// @Summary Create Advertisement
|
||||||
|
// @Description API for create Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param payload body request.AdvertisementCreateRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /advertisement [post]
|
||||||
|
func (_i *advertisementController) Save(c *fiber.Ctx) error {
|
||||||
|
req := new(request.AdvertisementCreateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
dataResult, err := _i.advertisementService.Save(clientId, *req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Advertisement successfully created"},
|
||||||
|
Data: dataResult,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload Advertisement
|
||||||
|
// @Summary Upload Advertisement
|
||||||
|
// @Description API for Upload File Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @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
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.advertisementService.Upload(clientId, c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Advertisement successfully upload"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update update Advertisement
|
||||||
|
// @Summary update Advertisement
|
||||||
|
// @Description API for update Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @Param payload body request.AdvertisementUpdateRequest true "Required payload"
|
||||||
|
// @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/{id} [put]
|
||||||
|
func (_i *advertisementController) Update(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.AdvertisementUpdateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.advertisementService.Update(clientId, uint(id), *req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Advertisement successfully updated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePublish Advertisement
|
||||||
|
// @Summary Update Publish Advertisement
|
||||||
|
// @Description API for Update Publish Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param id path int true "Advertisement ID"
|
||||||
|
// @Param isPublish query bool true "Advertisement Publish Status"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /advertisement/publish/{id} [put]
|
||||||
|
func (_i *advertisementController) UpdatePublish(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isPublish, err := strconv.ParseBool(c.Query("isPublish"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.advertisementService.UpdatePublish(clientId, uint(id), isPublish)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Advertisement successfully publish updated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete delete Advertisement
|
||||||
|
// @Summary delete Advertisement
|
||||||
|
// @Description API for delete Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
|
||||||
|
// @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/{id} [delete]
|
||||||
|
func (_i *advertisementController) Delete(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.advertisementService.Delete(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Advertisement successfully deleted"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewer Advertisement
|
||||||
|
// @Summary Viewer Advertisement
|
||||||
|
// @Description API for Viewer Advertisement
|
||||||
|
// @Tags Advertisement
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string false "Insert the X-Client-Key"
|
||||||
|
// @Param filename path string true "Content File Name"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /advertisement/viewer/{filename} [get]
|
||||||
|
func (_i *advertisementController) Viewer(c *fiber.Ctx) error {
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
return _i.advertisementService.Viewer(clientId, c)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/module/advertisement/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
Advertisement AdvertisementController
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(AdvertisementService service.AdvertisementService, log zerolog.Logger) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
Advertisement: NewAdvertisementController(AdvertisementService, log),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
res "web-medols-be/app/module/advertisement/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AdvertisementResponseMapper(advertisementReq *entity.Advertisement, host string) (advertisementRes *res.AdvertisementResponse) {
|
||||||
|
if advertisementReq != nil {
|
||||||
|
advertisementRes = &res.AdvertisementResponse{
|
||||||
|
ID: advertisementReq.ID,
|
||||||
|
Title: advertisementReq.Title,
|
||||||
|
Description: advertisementReq.Description,
|
||||||
|
RedirectLink: advertisementReq.RedirectLink,
|
||||||
|
Placement: advertisementReq.Placement,
|
||||||
|
StatusId: advertisementReq.StatusId,
|
||||||
|
IsActive: advertisementReq.IsActive,
|
||||||
|
IsPublish: advertisementReq.IsPublish,
|
||||||
|
CreatedAt: advertisementReq.CreatedAt,
|
||||||
|
UpdatedAt: advertisementReq.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if advertisementReq.ContentFilePath != nil {
|
||||||
|
advertisementRes.ContentFileUrl = host + "/advertisement/viewer/" + *advertisementReq.ContentFileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return advertisementRes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"strings"
|
||||||
|
"web-medols-be/app/database"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/advertisement/request"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
utilSvc "web-medols-be/utils/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type advertisementRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdvertisementRepository define interface of IAdvertisementRepository
|
||||||
|
type AdvertisementRepository interface {
|
||||||
|
GetAll(clientId *uuid.UUID, req request.AdvertisementQueryRequest) (advertisements []*entity.Advertisement, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (advertisement *entity.Advertisement, err error)
|
||||||
|
FindByFilename(clientId *uuid.UUID, contentFilename string) (advertisement *entity.Advertisement, err error)
|
||||||
|
Create(advertisement *entity.Advertisement) (advertisementReturn *entity.Advertisement, err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, advertisement *entity.Advertisement) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdvertisementRepository(db *database.Database, logger zerolog.Logger) AdvertisementRepository {
|
||||||
|
return &advertisementRepository{
|
||||||
|
DB: db,
|
||||||
|
Log: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement interface of IAdvertisementRepository
|
||||||
|
func (_i *advertisementRepository) GetAll(clientId *uuid.UUID, req request.AdvertisementQueryRequest) (advertisements []*entity.Advertisement, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.Advertisement{})
|
||||||
|
|
||||||
|
// Add ClientId filter
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
|
||||||
|
if req.Title != nil && *req.Title != "" {
|
||||||
|
title := strings.ToLower(*req.Title)
|
||||||
|
query = query.Where("LOWER(title) LIKE ?", "%"+strings.ToLower(title)+"%")
|
||||||
|
}
|
||||||
|
if req.Description != nil && *req.Description != "" {
|
||||||
|
description := strings.ToLower(*req.Description)
|
||||||
|
query = query.Where("LOWER(description) LIKE ?", "%"+strings.ToLower(description)+"%")
|
||||||
|
}
|
||||||
|
if req.RedirectLink != nil && *req.RedirectLink != "" {
|
||||||
|
redirectLink := strings.ToLower(*req.RedirectLink)
|
||||||
|
query = query.Where("LOWER(redirect_link) LIKE ?", "%"+strings.ToLower(redirectLink)+"%")
|
||||||
|
}
|
||||||
|
if req.Placement != nil && *req.Placement != "" {
|
||||||
|
placement := strings.ToLower(*req.Placement)
|
||||||
|
query = query.Where("LOWER(placement) LIKE ?", "%"+strings.ToLower(placement)+"%")
|
||||||
|
}
|
||||||
|
if req.StatusId != nil {
|
||||||
|
query = query.Where("status_id = ?", req.StatusId)
|
||||||
|
}
|
||||||
|
query.Count(&count)
|
||||||
|
|
||||||
|
if req.Pagination.SortBy != "" {
|
||||||
|
direction := "ASC"
|
||||||
|
if req.Pagination.Sort == "desc" {
|
||||||
|
direction = "DESC"
|
||||||
|
}
|
||||||
|
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Pagination.Count = count
|
||||||
|
req.Pagination = paginator.Paging(req.Pagination)
|
||||||
|
|
||||||
|
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&advertisements).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = *req.Pagination
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementRepository) FindOne(clientId *uuid.UUID, id uint) (advertisement *entity.Advertisement, err error) {
|
||||||
|
query := _i.DB.DB.Where("id = ?", id)
|
||||||
|
|
||||||
|
// Add ClientId filter
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.First(&advertisement).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return advertisement, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementRepository) FindByFilename(clientId *uuid.UUID, contentFilename string) (advertisement *entity.Advertisement, err error) {
|
||||||
|
query := _i.DB.DB.Where("content_file_name = ?", contentFilename)
|
||||||
|
|
||||||
|
// Add ClientId filter
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.First(&advertisement).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return advertisement, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementRepository) Create(advertisement *entity.Advertisement) (advertisementReturn *entity.Advertisement, err error) {
|
||||||
|
result := _i.DB.DB.Create(advertisement)
|
||||||
|
return advertisement, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementRepository) Update(clientId *uuid.UUID, id uint, advertisement *entity.Advertisement) (err error) {
|
||||||
|
advertisementMap, err := utilSvc.StructToMap(advertisement)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
query := _i.DB.DB.Model(&entity.Advertisement{}).Where(&entity.Advertisement{ID: id})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
return query.Updates(advertisementMap).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementRepository) Delete(clientId *uuid.UUID, id uint) error {
|
||||||
|
query := _i.DB.DB.Model(&entity.Advertisement{}).Where("id = ?", id)
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
return query.Delete(&entity.Advertisement{}).Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdvertisementGeneric interface {
|
||||||
|
ToEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdvertisementQueryRequest struct {
|
||||||
|
Title *string `json:"title"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
RedirectLink *string `json:"redirectLink"`
|
||||||
|
Placement *string `json:"placement"`
|
||||||
|
IsPublish *bool `json:"isPublish"`
|
||||||
|
StatusId *int `json:"statusId"`
|
||||||
|
Pagination *paginator.Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdvertisementCreateRequest struct {
|
||||||
|
Title string `json:"title" validate:"required"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
RedirectLink string `json:"redirectLink" validate:"required"`
|
||||||
|
Placement string `json:"placement" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req AdvertisementCreateRequest) ToEntity() *entity.Advertisement {
|
||||||
|
return &entity.Advertisement{
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
RedirectLink: req.RedirectLink,
|
||||||
|
Placement: req.Placement,
|
||||||
|
StatusId: 1,
|
||||||
|
IsPublish: true,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdvertisementUpdateRequest struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
Title string `json:"title" validate:"required"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
RedirectLink string `json:"redirectLink" validate:"required"`
|
||||||
|
Placement string `json:"placement" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req AdvertisementUpdateRequest) ToEntity() *entity.Advertisement {
|
||||||
|
return &entity.Advertisement{
|
||||||
|
ID: req.ID,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
RedirectLink: req.RedirectLink,
|
||||||
|
Placement: req.Placement,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdvertisementQueryRequestContext struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
RedirectLink string `json:"redirectLink"`
|
||||||
|
Placement string `json:"placement"`
|
||||||
|
StatusId string `json:"statusId"`
|
||||||
|
IsPublish string `json:"isPublish"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req AdvertisementQueryRequestContext) ToParamRequest() AdvertisementQueryRequest {
|
||||||
|
var request AdvertisementQueryRequest
|
||||||
|
|
||||||
|
if title := req.Title; title != "" {
|
||||||
|
request.Title = &title
|
||||||
|
}
|
||||||
|
if description := req.Description; description != "" {
|
||||||
|
request.Description = &description
|
||||||
|
}
|
||||||
|
if redirectLink := req.RedirectLink; redirectLink != "" {
|
||||||
|
request.RedirectLink = &redirectLink
|
||||||
|
}
|
||||||
|
if placement := req.Placement; placement != "" {
|
||||||
|
request.Placement = &placement
|
||||||
|
}
|
||||||
|
if isPublishStr := req.IsPublish; isPublishStr != "" {
|
||||||
|
isPublish, err := strconv.ParseBool(isPublishStr)
|
||||||
|
if err == nil {
|
||||||
|
request.IsPublish = &isPublish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if statusIdStr := req.StatusId; statusIdStr != "" {
|
||||||
|
statusId, err := strconv.Atoi(statusIdStr)
|
||||||
|
if err == nil {
|
||||||
|
request.StatusId = &statusId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type AdvertisementResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
RedirectLink string `json:"redirectLink"`
|
||||||
|
ContentFileUrl string `json:"contentFileUrl"`
|
||||||
|
Placement string `json:"placement"`
|
||||||
|
StatusId int `json:"statusId"`
|
||||||
|
IsPublish bool `json:"isPublish"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"mime"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/advertisement/mapper"
|
||||||
|
"web-medols-be/app/module/advertisement/repository"
|
||||||
|
"web-medols-be/app/module/advertisement/request"
|
||||||
|
"web-medols-be/app/module/advertisement/response"
|
||||||
|
usersRepository "web-medols-be/app/module/users/repository"
|
||||||
|
config "web-medols-be/config/config"
|
||||||
|
minioStorage "web-medols-be/config/config"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdvertisementService
|
||||||
|
type advertisementService struct {
|
||||||
|
Repo repository.AdvertisementRepository
|
||||||
|
UsersRepo usersRepository.UsersRepository
|
||||||
|
Log zerolog.Logger
|
||||||
|
Cfg *config.Config
|
||||||
|
MinioStorage *minioStorage.MinioStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdvertisementService define interface of IAdvertisementService
|
||||||
|
type AdvertisementService interface {
|
||||||
|
All(clientId *uuid.UUID, req request.AdvertisementQueryRequest) (advertisement []*response.AdvertisementResponse, paging paginator.Pagination, err error)
|
||||||
|
Show(clientId *uuid.UUID, id uint) (advertisement *response.AdvertisementResponse, err error)
|
||||||
|
Save(clientId *uuid.UUID, req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, err error)
|
||||||
|
Upload(clientId *uuid.UUID, c *fiber.Ctx, id uint) (err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, req request.AdvertisementUpdateRequest) (err error)
|
||||||
|
UpdatePublish(clientId *uuid.UUID, id uint, isPublish bool) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) error
|
||||||
|
Viewer(clientId *uuid.UUID, c *fiber.Ctx) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAdvertisementService init AdvertisementService
|
||||||
|
func NewAdvertisementService(repo repository.AdvertisementRepository, minioStorage *minioStorage.MinioStorage, usersRepo usersRepository.UsersRepository, log zerolog.Logger, cfg *config.Config) AdvertisementService {
|
||||||
|
|
||||||
|
return &advertisementService{
|
||||||
|
Repo: repo,
|
||||||
|
UsersRepo: usersRepo,
|
||||||
|
MinioStorage: minioStorage,
|
||||||
|
Log: log,
|
||||||
|
Cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All implement interface of AdvertisementService
|
||||||
|
func (_i *advertisementService) All(clientId *uuid.UUID, req request.AdvertisementQueryRequest) (advertisements []*response.AdvertisementResponse, paging paginator.Pagination, err error) {
|
||||||
|
results, paging, err := _i.Repo.GetAll(clientId, req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host := _i.Cfg.App.Domain
|
||||||
|
for _, result := range results {
|
||||||
|
advertisements = append(advertisements, mapper.AdvertisementResponseMapper(result, host))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementService) Show(clientId *uuid.UUID, id uint) (advertisement *response.AdvertisementResponse, err error) {
|
||||||
|
result, err := _i.Repo.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
host := _i.Cfg.App.Domain
|
||||||
|
return mapper.AdvertisementResponseMapper(result, host), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementService) Save(clientId *uuid.UUID, req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, err error) {
|
||||||
|
_i.Log.Info().Interface("data", req).Msg("")
|
||||||
|
newReq := req.ToEntity()
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
newReq.ClientId = clientId
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.Repo.Create(newReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementService) Upload(clientId *uuid.UUID, 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(clientId, 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(clientId, 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(clientId *uuid.UUID, id uint, req request.AdvertisementUpdateRequest) (err error) {
|
||||||
|
_i.Log.Info().Interface("data", req).Msg("")
|
||||||
|
newReq := req.ToEntity()
|
||||||
|
if clientId != nil {
|
||||||
|
newReq.ClientId = clientId
|
||||||
|
}
|
||||||
|
return _i.Repo.Update(clientId, id, newReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementService) UpdatePublish(clientId *uuid.UUID, id uint, isPublish bool) (err error) {
|
||||||
|
_i.Log.Info().Str("timestamp", time.Now().
|
||||||
|
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
|
||||||
|
Interface("ids", id).Msg("")
|
||||||
|
_i.Log.Info().Str("timestamp", time.Now().
|
||||||
|
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
|
||||||
|
Interface("isPublish", isPublish).Msg("")
|
||||||
|
|
||||||
|
result, err := _i.Repo.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.IsPublish = isPublish
|
||||||
|
|
||||||
|
_i.Log.Info().Str("timestamp", time.Now().
|
||||||
|
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
|
||||||
|
Interface("result", result).Msg("")
|
||||||
|
|
||||||
|
return _i.Repo.Update(clientId, id, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementService) Delete(clientId *uuid.UUID, id uint) error {
|
||||||
|
result, err := _i.Repo.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.IsActive = false
|
||||||
|
if clientId != nil {
|
||||||
|
result.ClientId = clientId
|
||||||
|
}
|
||||||
|
return _i.Repo.Update(clientId, id, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *advertisementService) Viewer(clientId *uuid.UUID, c *fiber.Ctx) (err error) {
|
||||||
|
filename := c.Params("filename")
|
||||||
|
result, err := _i.Repo.FindByFilename(clientId, filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
|
||||||
|
objectName := *result.ContentFilePath
|
||||||
|
|
||||||
|
// Create minio connection.
|
||||||
|
minioClient, err := _i.MinioStorage.ConnectMinio()
|
||||||
|
if err != nil {
|
||||||
|
// Return status 500 and minio connection error.
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
defer fileContent.Close()
|
||||||
|
|
||||||
|
// Tentukan Content-Type berdasarkan ekstensi file
|
||||||
|
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/octet-stream" // fallback jika tidak ada tipe MIME yang cocok
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Type", contentType)
|
||||||
|
|
||||||
|
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileExtension(filename string) string {
|
||||||
|
// split file name
|
||||||
|
parts := strings.Split(filename, ".")
|
||||||
|
|
||||||
|
// jika tidak ada ekstensi, kembalikan string kosong
|
||||||
|
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ambil ekstensi terakhir
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package approval_workflow_steps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/controller"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/repository"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApprovalWorkflowStepsRouter struct of ApprovalWorkflowStepsRouter
|
||||||
|
type ApprovalWorkflowStepsRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller controller.ApprovalWorkflowStepsController
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApprovalWorkflowStepsModule register bulky of ApprovalWorkflowSteps module
|
||||||
|
var NewApprovalWorkflowStepsModule = fx.Options(
|
||||||
|
// register repository of ApprovalWorkflowSteps module
|
||||||
|
fx.Provide(repository.NewApprovalWorkflowStepsRepository),
|
||||||
|
|
||||||
|
// register service of ApprovalWorkflowSteps module
|
||||||
|
fx.Provide(service.NewApprovalWorkflowStepsService),
|
||||||
|
|
||||||
|
// register controller of ApprovalWorkflowSteps module
|
||||||
|
fx.Provide(controller.NewApprovalWorkflowStepsController),
|
||||||
|
|
||||||
|
// register router of ApprovalWorkflowSteps module
|
||||||
|
fx.Provide(NewApprovalWorkflowStepsRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewApprovalWorkflowStepsRouter init ApprovalWorkflowStepsRouter
|
||||||
|
func NewApprovalWorkflowStepsRouter(fiber *fiber.App, controller controller.ApprovalWorkflowStepsController) *ApprovalWorkflowStepsRouter {
|
||||||
|
return &ApprovalWorkflowStepsRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterApprovalWorkflowStepsRoutes register routes of ApprovalWorkflowSteps module
|
||||||
|
func (_i *ApprovalWorkflowStepsRouter) RegisterApprovalWorkflowStepsRoutes() {
|
||||||
|
// define controllers
|
||||||
|
approvalWorkflowStepsController := _i.Controller
|
||||||
|
|
||||||
|
// define routes
|
||||||
|
_i.App.Route("/approval-workflow-steps", func(router fiber.Router) {
|
||||||
|
router.Get("/", approvalWorkflowStepsController.All)
|
||||||
|
router.Get("/:id", approvalWorkflowStepsController.Show)
|
||||||
|
router.Post("/", approvalWorkflowStepsController.Save)
|
||||||
|
router.Put("/:id", approvalWorkflowStepsController.Update)
|
||||||
|
router.Delete("/:id", approvalWorkflowStepsController.Delete)
|
||||||
|
router.Get("/workflow/:workflowId", approvalWorkflowStepsController.GetByWorkflow)
|
||||||
|
router.Get("/role/:roleId", approvalWorkflowStepsController.GetByRole)
|
||||||
|
router.Post("/bulk", approvalWorkflowStepsController.BulkSave)
|
||||||
|
router.Put("/workflow/:workflowId/reorder", approvalWorkflowStepsController.Reorder)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,443 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/middleware"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/request"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/service"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
utilRes "web-medols-be/utils/response"
|
||||||
|
utilVal "web-medols-be/utils/validator"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper functions for parsing query parameters
|
||||||
|
func parseUintPointer(s string) *uint {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val, err := strconv.ParseUint(s, 10, 32); err == nil {
|
||||||
|
uval := uint(val)
|
||||||
|
return &uval
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIntPointer(s string) *int {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(s); err == nil {
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStringPointer(s string) *string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBoolPointer(s string) *bool {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val, err := strconv.ParseBool(s); err == nil {
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type approvalWorkflowStepsController struct {
|
||||||
|
approvalWorkflowStepsService service.ApprovalWorkflowStepsService
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowStepsController interface {
|
||||||
|
All(c *fiber.Ctx) error
|
||||||
|
Show(c *fiber.Ctx) error
|
||||||
|
Save(c *fiber.Ctx) error
|
||||||
|
Update(c *fiber.Ctx) error
|
||||||
|
Delete(c *fiber.Ctx) error
|
||||||
|
GetByWorkflow(c *fiber.Ctx) error
|
||||||
|
GetByRole(c *fiber.Ctx) error
|
||||||
|
BulkSave(c *fiber.Ctx) error
|
||||||
|
Reorder(c *fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalWorkflowStepsController(approvalWorkflowStepsService service.ApprovalWorkflowStepsService, log zerolog.Logger) ApprovalWorkflowStepsController {
|
||||||
|
return &approvalWorkflowStepsController{
|
||||||
|
approvalWorkflowStepsService: approvalWorkflowStepsService,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All ApprovalWorkflowSteps
|
||||||
|
// @Summary Get all ApprovalWorkflowSteps
|
||||||
|
// @Description API for getting all ApprovalWorkflowSteps
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param workflowId query int false "Workflow ID filter"
|
||||||
|
// @Param stepOrder query int false "Step order filter"
|
||||||
|
// @Param stepName query string false "Step name filter"
|
||||||
|
// @Param userLevelId query int false "User level ID filter"
|
||||||
|
// @Param isOptional query bool false "Is optional filter"
|
||||||
|
// @Param isActive query bool false "Is active filter"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps [get]
|
||||||
|
func (_i *approvalWorkflowStepsController) All(c *fiber.Ctx) error {
|
||||||
|
_, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := request.GetApprovalWorkflowStepsRequest{
|
||||||
|
WorkflowID: parseUintPointer(c.Query("workflowId")),
|
||||||
|
StepOrder: parseIntPointer(c.Query("stepOrder")),
|
||||||
|
StepName: parseStringPointer(c.Query("stepName")),
|
||||||
|
UserLevelID: parseUintPointer(c.Query("userLevelId")),
|
||||||
|
IsOptional: parseBoolPointer(c.Query("isOptional")),
|
||||||
|
IsActive: parseBoolPointer(c.Query("isActive")),
|
||||||
|
Page: 1,
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
_i.Log.Info().Interface("clientId", clientId).Msg("")
|
||||||
|
|
||||||
|
approvalWorkflowStepsData, paging, err := _i.approvalWorkflowStepsService.GetAll(clientId, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps list successfully retrieved"},
|
||||||
|
Data: approvalWorkflowStepsData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show ApprovalWorkflowSteps
|
||||||
|
// @Summary Get one ApprovalWorkflowSteps
|
||||||
|
// @Description API for getting one ApprovalWorkflowSteps
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflowSteps ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps/{id} [get]
|
||||||
|
func (_i *approvalWorkflowStepsController) Show(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
approvalWorkflowStepsData, err := _i.approvalWorkflowStepsService.FindOne(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps successfully retrieved"},
|
||||||
|
Data: approvalWorkflowStepsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save ApprovalWorkflowSteps
|
||||||
|
// @Summary Save ApprovalWorkflowSteps
|
||||||
|
// @Description API for saving ApprovalWorkflowSteps
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param payload body request.CreateApprovalWorkflowStepsRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps [post]
|
||||||
|
func (_i *approvalWorkflowStepsController) Save(c *fiber.Ctx) error {
|
||||||
|
req := new(request.CreateApprovalWorkflowStepsRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entity
|
||||||
|
step := &entity.ApprovalWorkflowSteps{
|
||||||
|
WorkflowId: req.WorkflowID,
|
||||||
|
StepOrder: req.StepOrder,
|
||||||
|
StepName: req.StepName,
|
||||||
|
RequiredUserLevelId: req.ApproverRoleID,
|
||||||
|
CanSkip: &req.IsOptional,
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalWorkflowStepsData, err := _i.approvalWorkflowStepsService.Create(clientId, step)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps successfully created"},
|
||||||
|
Data: approvalWorkflowStepsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update ApprovalWorkflowSteps
|
||||||
|
// @Summary Update ApprovalWorkflowSteps
|
||||||
|
// @Description API for updating ApprovalWorkflowSteps
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflowSteps ID"
|
||||||
|
// @Param payload body request.UpdateApprovalWorkflowStepsRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps/{id} [put]
|
||||||
|
func (_i *approvalWorkflowStepsController) Update(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.UpdateApprovalWorkflowStepsRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entity
|
||||||
|
step := &entity.ApprovalWorkflowSteps{}
|
||||||
|
if req.StepOrder != nil {
|
||||||
|
step.StepOrder = *req.StepOrder
|
||||||
|
}
|
||||||
|
if req.StepName != nil {
|
||||||
|
step.StepName = *req.StepName
|
||||||
|
}
|
||||||
|
if req.ApproverRoleID != nil {
|
||||||
|
step.RequiredUserLevelId = *req.ApproverRoleID
|
||||||
|
}
|
||||||
|
if req.IsOptional != nil {
|
||||||
|
step.CanSkip = req.IsOptional
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowStepsService.Update(clientId, uint(id), step)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps successfully updated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete ApprovalWorkflowSteps
|
||||||
|
// @Summary Delete ApprovalWorkflowSteps
|
||||||
|
// @Description API for deleting ApprovalWorkflowSteps
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflowSteps ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps/{id} [delete]
|
||||||
|
func (_i *approvalWorkflowStepsController) Delete(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowStepsService.Delete(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps successfully deleted"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByWorkflow ApprovalWorkflowSteps
|
||||||
|
// @Summary Get ApprovalWorkflowSteps by Workflow ID
|
||||||
|
// @Description API for getting ApprovalWorkflowSteps by Workflow ID
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param workflowId path int true "Workflow ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps/workflow/{workflowId} [get]
|
||||||
|
func (_i *approvalWorkflowStepsController) GetByWorkflow(c *fiber.Ctx) error {
|
||||||
|
workflowId, err := strconv.Atoi(c.Params("workflowId"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid workflow ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
approvalWorkflowStepsData, err := _i.approvalWorkflowStepsService.GetByWorkflowID(clientId, uint(workflowId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps by workflow successfully retrieved"},
|
||||||
|
Data: approvalWorkflowStepsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByRole ApprovalWorkflowSteps
|
||||||
|
// @Summary Get ApprovalWorkflowSteps by Role ID
|
||||||
|
// @Description API for getting ApprovalWorkflowSteps by Role ID
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param roleId path int true "Role ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps/role/{roleId} [get]
|
||||||
|
func (_i *approvalWorkflowStepsController) GetByRole(c *fiber.Ctx) error {
|
||||||
|
roleId, err := strconv.Atoi(c.Params("roleId"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid role ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
approvalWorkflowStepsData, err := _i.approvalWorkflowStepsService.GetByWorkflowID(clientId, uint(roleId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps by role successfully retrieved"},
|
||||||
|
Data: approvalWorkflowStepsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkSave ApprovalWorkflowSteps
|
||||||
|
// @Summary Bulk create ApprovalWorkflowSteps
|
||||||
|
// @Description API for bulk creating ApprovalWorkflowSteps
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param payload body request.BulkCreateApprovalWorkflowStepsRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps/bulk [post]
|
||||||
|
func (_i *approvalWorkflowStepsController) BulkSave(c *fiber.Ctx) error {
|
||||||
|
req := new(request.BulkCreateApprovalWorkflowStepsRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entities
|
||||||
|
var steps []*entity.ApprovalWorkflowSteps
|
||||||
|
for _, stepReq := range req.Steps {
|
||||||
|
step := &entity.ApprovalWorkflowSteps{
|
||||||
|
WorkflowId: stepReq.WorkflowID,
|
||||||
|
StepOrder: stepReq.StepOrder,
|
||||||
|
StepName: stepReq.StepName,
|
||||||
|
RequiredUserLevelId: stepReq.ApproverRoleID,
|
||||||
|
CanSkip: &stepReq.IsOptional,
|
||||||
|
}
|
||||||
|
steps = append(steps, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalWorkflowStepsData, err := _i.approvalWorkflowStepsService.BulkCreate(clientId, req.WorkflowID, steps)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps successfully bulk created"},
|
||||||
|
Data: approvalWorkflowStepsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder ApprovalWorkflowSteps
|
||||||
|
// @Summary Reorder ApprovalWorkflowSteps
|
||||||
|
// @Description API for reordering ApprovalWorkflowSteps
|
||||||
|
// @Tags ApprovalWorkflowSteps
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param workflowId path int true "Workflow ID"
|
||||||
|
// @Param payload body request.ReorderApprovalWorkflowStepsRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflow-steps/workflow/{workflowId}/reorder [put]
|
||||||
|
func (_i *approvalWorkflowStepsController) Reorder(c *fiber.Ctx) error {
|
||||||
|
workflowId, err := strconv.Atoi(c.Params("workflowId"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid workflow ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ReorderApprovalWorkflowStepsRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to expected format
|
||||||
|
stepOrders := req.ToStepOrders()
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowStepsService.ReorderSteps(clientId, uint(workflowId), stepOrders)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflowSteps successfully reordered"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
res "web-medols-be/app/module/approval_workflow_steps/response"
|
||||||
|
usersRepository "web-medols-be/app/module/users/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApprovalWorkflowStepsResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
approvalWorkflowStepsReq *entity.ApprovalWorkflowSteps,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (approvalWorkflowStepsRes *res.ApprovalWorkflowStepsResponse) {
|
||||||
|
|
||||||
|
if approvalWorkflowStepsReq != nil {
|
||||||
|
// Convert boolean pointers to boolean values
|
||||||
|
isOptional := false
|
||||||
|
if approvalWorkflowStepsReq.CanSkip != nil {
|
||||||
|
isOptional = *approvalWorkflowStepsReq.CanSkip
|
||||||
|
}
|
||||||
|
|
||||||
|
autoApprove := false
|
||||||
|
if approvalWorkflowStepsReq.AutoApproveAfterHours != nil {
|
||||||
|
autoApprove = *approvalWorkflowStepsReq.AutoApproveAfterHours > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalWorkflowStepsRes = &res.ApprovalWorkflowStepsResponse{
|
||||||
|
ID: approvalWorkflowStepsReq.ID,
|
||||||
|
WorkflowID: approvalWorkflowStepsReq.WorkflowId,
|
||||||
|
StepName: approvalWorkflowStepsReq.StepName,
|
||||||
|
StepOrder: approvalWorkflowStepsReq.StepOrder,
|
||||||
|
ApproverRoleID: approvalWorkflowStepsReq.RequiredUserLevelId,
|
||||||
|
IsOptional: isOptional,
|
||||||
|
RequiresComment: false, // Default value
|
||||||
|
AutoApprove: autoApprove,
|
||||||
|
TimeoutHours: approvalWorkflowStepsReq.AutoApproveAfterHours,
|
||||||
|
CreatedAt: approvalWorkflowStepsReq.CreatedAt,
|
||||||
|
UpdatedAt: approvalWorkflowStepsReq.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvalWorkflowStepsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApprovalWorkflowStepsSummaryResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
approvalWorkflowStepsReq *entity.ApprovalWorkflowSteps,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (approvalWorkflowStepsRes *res.ApprovalWorkflowStepsSummaryResponse) {
|
||||||
|
|
||||||
|
if approvalWorkflowStepsReq != nil {
|
||||||
|
// Convert boolean pointers to boolean values
|
||||||
|
isOptional := false
|
||||||
|
if approvalWorkflowStepsReq.CanSkip != nil {
|
||||||
|
isOptional = *approvalWorkflowStepsReq.CanSkip
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalWorkflowStepsRes = &res.ApprovalWorkflowStepsSummaryResponse{
|
||||||
|
ID: approvalWorkflowStepsReq.ID,
|
||||||
|
WorkflowID: approvalWorkflowStepsReq.WorkflowId,
|
||||||
|
StepName: approvalWorkflowStepsReq.StepName,
|
||||||
|
StepOrder: approvalWorkflowStepsReq.StepOrder,
|
||||||
|
ApproverRoleID: approvalWorkflowStepsReq.RequiredUserLevelId,
|
||||||
|
IsOptional: isOptional,
|
||||||
|
RequiresComment: false, // Default value
|
||||||
|
TimeoutHours: approvalWorkflowStepsReq.AutoApproveAfterHours,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvalWorkflowStepsRes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,373 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"web-medols-be/app/database"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/request"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type approvalWorkflowStepsRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApprovalWorkflowStepsRepository define interface of IApprovalWorkflowStepsRepository
|
||||||
|
type ApprovalWorkflowStepsRepository interface {
|
||||||
|
// Basic CRUD
|
||||||
|
GetAll(clientId *uuid.UUID, req request.GetApprovalWorkflowStepsRequest) (steps []*entity.ApprovalWorkflowSteps, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (step *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
Create(clientId *uuid.UUID, step *entity.ApprovalWorkflowSteps) (stepReturn *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
Update(id uint, step *entity.ApprovalWorkflowSteps) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Workflow-specific methods
|
||||||
|
GetByWorkflowId(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error)
|
||||||
|
GetActiveByWorkflowId(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error)
|
||||||
|
FindByWorkflowAndStep(clientId *uuid.UUID, workflowId uint, stepOrder int) (step *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
GetNextStep(clientId *uuid.UUID, workflowId uint, currentStep int) (step *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
GetPreviousStep(clientId *uuid.UUID, workflowId uint, currentStep int) (step *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
|
||||||
|
// Step management methods
|
||||||
|
ReorderSteps(clientId *uuid.UUID, workflowId uint, stepOrders []map[string]interface{}) (err error)
|
||||||
|
GetMaxStepOrder(clientId *uuid.UUID, workflowId uint) (maxOrder int, err error)
|
||||||
|
GetStepsByUserLevel(clientId *uuid.UUID, userLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error)
|
||||||
|
|
||||||
|
// Validation methods
|
||||||
|
ValidateStepSequence(clientId *uuid.UUID, workflowId uint) (isValid bool, errors []string, err error)
|
||||||
|
CheckStepDependencies(clientId *uuid.UUID, stepId uint) (canDelete bool, dependencies []string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalWorkflowStepsRepository(db *database.Database, log zerolog.Logger) ApprovalWorkflowStepsRepository {
|
||||||
|
return &approvalWorkflowStepsRepository{
|
||||||
|
DB: db,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic CRUD implementations
|
||||||
|
func (_i *approvalWorkflowStepsRepository) GetAll(clientId *uuid.UUID, req request.GetApprovalWorkflowStepsRequest) (steps []*entity.ApprovalWorkflowSteps, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters from request
|
||||||
|
if req.WorkflowID != nil && *req.WorkflowID > 0 {
|
||||||
|
query = query.Where("workflow_id = ?", *req.WorkflowID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.IsActive != nil {
|
||||||
|
query = query.Where("is_active = ?", *req.IsActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.UserLevelID != nil && *req.UserLevelID > 0 {
|
||||||
|
query = query.Where("required_user_level_id = ?", *req.UserLevelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.StepOrder != nil && *req.StepOrder > 0 {
|
||||||
|
query = query.Where("step_order = ?", *req.StepOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.StepName != nil && *req.StepName != "" {
|
||||||
|
query = query.Where("step_name ILIKE ?", "%"+*req.StepName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
query = query.Order("workflow_id ASC, step_order ASC")
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
page := req.Page
|
||||||
|
limit := req.Limit
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
err = query.Offset(offset).Limit(limit).Find(&steps).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Count: count,
|
||||||
|
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) FindOne(clientId *uuid.UUID, id uint) (step *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
|
||||||
|
err = query.First(&step, id).Error
|
||||||
|
return step, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) Create(clientId *uuid.UUID, step *entity.ApprovalWorkflowSteps) (stepReturn *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
step.ClientId = clientId
|
||||||
|
err = _i.DB.DB.Create(&step).Error
|
||||||
|
return step, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) Update(id uint, step *entity.ApprovalWorkflowSteps) (err error) {
|
||||||
|
err = _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{}).Where("id = ?", id).Updates(step).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.Delete(&entity.ApprovalWorkflowSteps{}, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workflow-specific methods
|
||||||
|
func (_i *approvalWorkflowStepsRepository) GetByWorkflowId(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("workflow_id = ?", workflowId)
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
query = query.Order("step_order ASC")
|
||||||
|
|
||||||
|
err = query.Find(&steps).Error
|
||||||
|
return steps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) GetActiveByWorkflowId(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("workflow_id = ? AND is_active = ?", workflowId, true)
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
query = query.Order("step_order ASC")
|
||||||
|
|
||||||
|
err = query.Find(&steps).Error
|
||||||
|
return steps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) FindByWorkflowAndStep(clientId *uuid.UUID, workflowId uint, stepOrder int) (step *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("workflow_id = ? AND step_order = ?", workflowId, stepOrder)
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
|
||||||
|
err = query.First(&step).Error
|
||||||
|
return step, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) GetNextStep(clientId *uuid.UUID, workflowId uint, currentStep int) (step *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("workflow_id = ? AND step_order > ? AND is_active = ?", workflowId, currentStep, true)
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
query = query.Order("step_order ASC")
|
||||||
|
|
||||||
|
err = query.First(&step).Error
|
||||||
|
return step, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) GetPreviousStep(clientId *uuid.UUID, workflowId uint, currentStep int) (step *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("workflow_id = ? AND step_order < ? AND is_active = ?", workflowId, currentStep, true)
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
query = query.Order("step_order DESC")
|
||||||
|
|
||||||
|
err = query.First(&step).Error
|
||||||
|
return step, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step management methods
|
||||||
|
func (_i *approvalWorkflowStepsRepository) ReorderSteps(clientId *uuid.UUID, workflowId uint, stepOrders []map[string]interface{}) (err error) {
|
||||||
|
// Start transaction
|
||||||
|
tx := _i.DB.DB.Begin()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := tx.Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update each step order
|
||||||
|
for _, stepOrder := range stepOrders {
|
||||||
|
stepId := stepOrder["id"]
|
||||||
|
newOrder := stepOrder["step_order"]
|
||||||
|
|
||||||
|
query := tx.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.Where("id = ? AND workflow_id = ?", stepId, workflowId).Update("step_order", newOrder).Error
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) GetMaxStepOrder(clientId *uuid.UUID, workflowId uint) (maxOrder int, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("workflow_id = ?")
|
||||||
|
query = query.Select("COALESCE(MAX(step_order), 0) as max_order")
|
||||||
|
|
||||||
|
err = query.Scan(&maxOrder).Error
|
||||||
|
return maxOrder, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) GetStepsByUserLevel(clientId *uuid.UUID, userLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("required_user_level_id = ? AND is_active = ?", userLevelId, true)
|
||||||
|
query = query.Preload("Workflow").Preload("RequiredUserLevel")
|
||||||
|
query = query.Order("workflow_id ASC, step_order ASC")
|
||||||
|
|
||||||
|
err = query.Find(&steps).Error
|
||||||
|
return steps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation methods
|
||||||
|
func (_i *approvalWorkflowStepsRepository) ValidateStepSequence(clientId *uuid.UUID, workflowId uint) (isValid bool, errors []string, err error) {
|
||||||
|
errors = make([]string, 0)
|
||||||
|
|
||||||
|
// Get all steps for the workflow
|
||||||
|
steps, err := _i.GetActiveByWorkflowId(clientId, workflowId)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(steps) == 0 {
|
||||||
|
errors = append(errors, "Workflow must have at least one step")
|
||||||
|
return false, errors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for sequential step orders starting from 1
|
||||||
|
expectedOrder := 1
|
||||||
|
for _, step := range steps {
|
||||||
|
if step.StepOrder != expectedOrder {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step order %d is missing or out of sequence", expectedOrder))
|
||||||
|
}
|
||||||
|
expectedOrder++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate step orders
|
||||||
|
stepOrderMap := make(map[int]bool)
|
||||||
|
for _, step := range steps {
|
||||||
|
if stepOrderMap[step.StepOrder] {
|
||||||
|
errors = append(errors, fmt.Sprintf("Duplicate step order found: %d", step.StepOrder))
|
||||||
|
}
|
||||||
|
stepOrderMap[step.StepOrder] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = len(errors) == 0
|
||||||
|
return isValid, errors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsRepository) CheckStepDependencies(clientId *uuid.UUID, stepId uint) (canDelete bool, dependencies []string, err error) {
|
||||||
|
dependencies = make([]string, 0)
|
||||||
|
|
||||||
|
// Check if step is referenced in any active approval flows
|
||||||
|
var activeFlowCount int64
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join with workflow steps to check current step
|
||||||
|
query = query.Joins("JOIN approval_workflow_steps ON article_approval_flows.workflow_id = approval_workflow_steps.workflow_id AND article_approval_flows.current_step = approval_workflow_steps.step_order")
|
||||||
|
query = query.Where("approval_workflow_steps.id = ? AND article_approval_flows.status_id IN (1, 4)", stepId) // pending or revision_requested
|
||||||
|
|
||||||
|
err = query.Count(&activeFlowCount).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, dependencies, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if activeFlowCount > 0 {
|
||||||
|
dependencies = append(dependencies, fmt.Sprintf("%d active approval flows are currently at this step", activeFlowCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if step is referenced in approval step logs
|
||||||
|
var logCount int64
|
||||||
|
logQuery := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
logQuery = logQuery.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a simplified check - in reality, you might want to join with the step to check step_order
|
||||||
|
logQuery = logQuery.Where("step_order IN (SELECT step_order FROM approval_workflow_steps WHERE id = ?)", stepId)
|
||||||
|
|
||||||
|
err = logQuery.Count(&logCount).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, dependencies, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if logCount > 0 {
|
||||||
|
dependencies = append(dependencies, fmt.Sprintf("%d approval step logs reference this step", logCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
canDelete = len(dependencies) == 0
|
||||||
|
return canDelete, dependencies, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
type CreateApprovalWorkflowStepsRequest struct {
|
||||||
|
WorkflowID uint `json:"workflowId" validate:"required"`
|
||||||
|
StepOrder int `json:"stepOrder" validate:"required,min=1"`
|
||||||
|
StepName string `json:"stepName" validate:"required,min=3,max=100"`
|
||||||
|
Description *string `json:"description" validate:"omitempty,max=500"`
|
||||||
|
ApproverRoleID uint `json:"approverRoleId" validate:"required"`
|
||||||
|
IsOptional bool `json:"isOptional"`
|
||||||
|
RequiresComment bool `json:"requiresComment"`
|
||||||
|
AutoApprove bool `json:"autoApprove"`
|
||||||
|
TimeoutHours *int `json:"timeoutHours" validate:"omitempty,min=1,max=720"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateApprovalWorkflowStepsRequest struct {
|
||||||
|
StepOrder *int `json:"stepOrder" validate:"omitempty,min=1"`
|
||||||
|
StepName *string `json:"stepName" validate:"omitempty,min=3,max=100"`
|
||||||
|
Description *string `json:"description" validate:"omitempty,max=500"`
|
||||||
|
ApproverRoleID *uint `json:"approverRoleId" validate:"omitempty"`
|
||||||
|
IsOptional *bool `json:"isOptional"`
|
||||||
|
RequiresComment *bool `json:"requiresComment"`
|
||||||
|
AutoApprove *bool `json:"autoApprove"`
|
||||||
|
TimeoutHours *int `json:"timeoutHours" validate:"omitempty,min=1,max=720"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetApprovalWorkflowStepsRequest struct {
|
||||||
|
WorkflowID *uint `json:"workflowId" form:"workflowId"`
|
||||||
|
RoleID *uint `json:"roleId" form:"roleId"`
|
||||||
|
UserLevelID *uint `json:"userLevelId" form:"userLevelId"`
|
||||||
|
StepOrder *int `json:"stepOrder" form:"stepOrder"`
|
||||||
|
StepName *string `json:"stepName" form:"stepName"`
|
||||||
|
IsOptional *bool `json:"isOptional" form:"isOptional"`
|
||||||
|
IsActive *bool `json:"isActive" form:"isActive"`
|
||||||
|
Page int `json:"page" form:"page" validate:"min=1"`
|
||||||
|
Limit int `json:"limit" form:"limit" validate:"min=1,max=100"`
|
||||||
|
SortBy *string `json:"sortBy" form:"sortBy"`
|
||||||
|
SortOrder *string `json:"sortOrder" form:"sortOrder" validate:"omitempty,oneof=asc desc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkCreateApprovalWorkflowStepsRequest struct {
|
||||||
|
WorkflowID uint `json:"workflowId" validate:"required"`
|
||||||
|
Steps []CreateApprovalWorkflowStepsRequest `json:"steps" validate:"required,min=1,max=20,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReorderApprovalWorkflowStepsRequest struct {
|
||||||
|
StepOrders []struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
StepOrder int `json:"stepOrder" validate:"required,min=1"`
|
||||||
|
} `json:"stepOrders" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReorderApprovalWorkflowStepsRequest) ToStepOrders() []struct {
|
||||||
|
ID uint
|
||||||
|
StepOrder int
|
||||||
|
} {
|
||||||
|
result := make([]struct {
|
||||||
|
ID uint
|
||||||
|
StepOrder int
|
||||||
|
}, len(r.StepOrders))
|
||||||
|
|
||||||
|
for i, step := range r.StepOrders {
|
||||||
|
result[i] = struct {
|
||||||
|
ID uint
|
||||||
|
StepOrder int
|
||||||
|
}{
|
||||||
|
ID: step.ID,
|
||||||
|
StepOrder: step.StepOrder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalWorkflowStepsResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
WorkflowID uint `json:"workflowId"`
|
||||||
|
StepOrder int `json:"stepOrder"`
|
||||||
|
StepName string `json:"stepName"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
ApproverRoleID uint `json:"approverRoleId"`
|
||||||
|
ApproverRoleName *string `json:"approverRoleName,omitempty"`
|
||||||
|
IsOptional bool `json:"isOptional"`
|
||||||
|
RequiresComment bool `json:"requiresComment"`
|
||||||
|
AutoApprove bool `json:"autoApprove"`
|
||||||
|
TimeoutHours *int `json:"timeoutHours"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowStepsSummaryResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
WorkflowID uint `json:"workflowId"`
|
||||||
|
StepOrder int `json:"stepOrder"`
|
||||||
|
StepName string `json:"stepName"`
|
||||||
|
ApproverRoleID uint `json:"approverRoleId"`
|
||||||
|
IsOptional bool `json:"isOptional"`
|
||||||
|
RequiresComment bool `json:"requiresComment"`
|
||||||
|
TimeoutHours *int `json:"timeoutHours"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowStepsStatsResponse struct {
|
||||||
|
TotalSteps int `json:"totalSteps"`
|
||||||
|
OptionalSteps int `json:"optionalSteps"`
|
||||||
|
MandatorySteps int `json:"mandatorySteps"`
|
||||||
|
StepsWithTimeout int `json:"stepsWithTimeout"`
|
||||||
|
AverageTimeoutHours float64 `json:"averageTimeoutHours"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,319 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/repository"
|
||||||
|
"web-medols-be/app/module/approval_workflow_steps/request"
|
||||||
|
workflowRepo "web-medols-be/app/module/approval_workflows/repository"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type approvalWorkflowStepsService struct {
|
||||||
|
ApprovalWorkflowStepsRepository repository.ApprovalWorkflowStepsRepository
|
||||||
|
ApprovalWorkflowsRepository workflowRepo.ApprovalWorkflowsRepository
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApprovalWorkflowStepsService define interface of IApprovalWorkflowStepsService
|
||||||
|
type ApprovalWorkflowStepsService interface {
|
||||||
|
// Basic CRUD
|
||||||
|
GetAll(clientId *uuid.UUID, req request.GetApprovalWorkflowStepsRequest) (steps []*entity.ApprovalWorkflowSteps, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (step *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
Create(clientId *uuid.UUID, step *entity.ApprovalWorkflowSteps) (stepReturn *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, step *entity.ApprovalWorkflowSteps) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Workflow steps management
|
||||||
|
GetByWorkflowID(clientId *uuid.UUID, workflowID uint) (steps []*entity.ApprovalWorkflowSteps, err error)
|
||||||
|
// GetByRoleID(clientId *uuid.UUID, roleID uint) (steps []*entity.ApprovalWorkflowSteps, err error) // Not implemented yet
|
||||||
|
BulkCreate(clientId *uuid.UUID, workflowID uint, steps []*entity.ApprovalWorkflowSteps) (stepsReturn []*entity.ApprovalWorkflowSteps, err error)
|
||||||
|
ReorderSteps(clientId *uuid.UUID, workflowID uint, stepOrders []struct {
|
||||||
|
ID uint
|
||||||
|
StepOrder int
|
||||||
|
}) (err error)
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
ValidateStep(clientId *uuid.UUID, step *entity.ApprovalWorkflowSteps) (isValid bool, errors []string, err error)
|
||||||
|
CanDeleteStep(clientId *uuid.UUID, id uint) (canDelete bool, reason string, err error)
|
||||||
|
ValidateStepOrder(clientId *uuid.UUID, workflowID uint, stepOrder int, excludeID *uint) (isValid bool, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalWorkflowStepsService(
|
||||||
|
approvalWorkflowStepsRepository repository.ApprovalWorkflowStepsRepository,
|
||||||
|
approvalWorkflowsRepository workflowRepo.ApprovalWorkflowsRepository,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) ApprovalWorkflowStepsService {
|
||||||
|
return &approvalWorkflowStepsService{
|
||||||
|
ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository,
|
||||||
|
ApprovalWorkflowsRepository: approvalWorkflowsRepository,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) GetAll(clientId *uuid.UUID, req request.GetApprovalWorkflowStepsRequest) (steps []*entity.ApprovalWorkflowSteps, paging paginator.Pagination, err error) {
|
||||||
|
return _i.ApprovalWorkflowStepsRepository.GetAll(clientId, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) FindOne(clientId *uuid.UUID, id uint) (step *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
return _i.ApprovalWorkflowStepsRepository.FindOne(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) Create(clientId *uuid.UUID, step *entity.ApprovalWorkflowSteps) (stepReturn *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
// Validate workflow exists
|
||||||
|
workflow, err := _i.ApprovalWorkflowsRepository.FindOne(clientId, step.WorkflowId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("workflow not found: %w", err)
|
||||||
|
}
|
||||||
|
if workflow == nil {
|
||||||
|
return nil, errors.New("workflow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate step order is unique within workflow
|
||||||
|
isValid, err := _i.ValidateStepOrder(clientId, step.WorkflowId, step.StepOrder, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isValid {
|
||||||
|
return nil, errors.New("step order already exists in this workflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate step data
|
||||||
|
isValid, validationErrors, err := _i.ValidateStep(clientId, step)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isValid {
|
||||||
|
return nil, fmt.Errorf("validation failed: %v", validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ApprovalWorkflowStepsRepository.Create(clientId, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) Update(clientId *uuid.UUID, id uint, step *entity.ApprovalWorkflowSteps) (err error) {
|
||||||
|
// Check if step exists
|
||||||
|
existingStep, err := _i.ApprovalWorkflowStepsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existingStep == nil {
|
||||||
|
return errors.New("step not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If step order is being changed, validate it's unique
|
||||||
|
if step.StepOrder != 0 && step.StepOrder != existingStep.StepOrder {
|
||||||
|
isValid, err := _i.ValidateStepOrder(clientId, existingStep.WorkflowId, step.StepOrder, &id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isValid {
|
||||||
|
return errors.New("step order already exists in this workflow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ApprovalWorkflowStepsRepository.Update(id, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
// Check if step can be deleted
|
||||||
|
canDelete, reason, err := _i.CanDeleteStep(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !canDelete {
|
||||||
|
return fmt.Errorf("cannot delete step: %s", reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ApprovalWorkflowStepsRepository.Delete(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) GetByWorkflowID(clientId *uuid.UUID, workflowID uint) (steps []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
return _i.ApprovalWorkflowStepsRepository.GetByWorkflowId(clientId, workflowID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByRoleID method is not implemented in repository yet
|
||||||
|
// func (_i *approvalWorkflowStepsService) GetByRoleID(clientId *uuid.UUID, roleID uint) (steps []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
// return _i.ApprovalWorkflowStepsRepository.GetByRoleID(clientId, roleID)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) BulkCreate(clientId *uuid.UUID, workflowID uint, steps []*entity.ApprovalWorkflowSteps) (stepsReturn []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
// Validate workflow exists
|
||||||
|
workflow, err := _i.ApprovalWorkflowsRepository.FindOne(clientId, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("workflow not found: %w", err)
|
||||||
|
}
|
||||||
|
if workflow == nil {
|
||||||
|
return nil, errors.New("workflow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all steps
|
||||||
|
stepOrders := make(map[int]bool)
|
||||||
|
for i, step := range steps {
|
||||||
|
step.WorkflowId = workflowID
|
||||||
|
|
||||||
|
// Check for duplicate step orders within the batch
|
||||||
|
if stepOrders[step.StepOrder] {
|
||||||
|
return nil, fmt.Errorf("duplicate step order %d in batch", step.StepOrder)
|
||||||
|
}
|
||||||
|
stepOrders[step.StepOrder] = true
|
||||||
|
|
||||||
|
// Validate step order is unique in database
|
||||||
|
isValid, err := _i.ValidateStepOrder(clientId, workflowID, step.StepOrder, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isValid {
|
||||||
|
return nil, fmt.Errorf("step order %d already exists in workflow", step.StepOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate step data
|
||||||
|
var errors []string
|
||||||
|
if step.RequiredUserLevelId == 0 {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step %d: RequiredUserLevelId is required", i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if step.AutoApproveAfterHours != nil && *step.AutoApproveAfterHours <= 0 {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step %d: AutoApproveAfterHours must be positive", i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if step.AutoApproveAfterHours != nil && *step.AutoApproveAfterHours > 720 {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step %d: AutoApproveAfterHours cannot exceed 720 hours (30 days)", i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(step.StepName) > 500 {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step %d: StepName cannot exceed 500 characters", i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return nil, fmt.Errorf("validation failed for step %d: %v", step.StepOrder, errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkCreate method is not implemented in repository yet
|
||||||
|
// return _i.ApprovalWorkflowStepsRepository.BulkCreate(clientId, steps)
|
||||||
|
return nil, fmt.Errorf("BulkCreate method not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) ReorderSteps(clientId *uuid.UUID, workflowID uint, stepOrders []struct {
|
||||||
|
ID uint
|
||||||
|
StepOrder int
|
||||||
|
}) (err error) {
|
||||||
|
// Validate workflow exists
|
||||||
|
workflow, err := _i.ApprovalWorkflowsRepository.FindOne(clientId, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("workflow not found: %w", err)
|
||||||
|
}
|
||||||
|
if workflow == nil {
|
||||||
|
return errors.New("workflow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all steps belong to the workflow and orders are unique
|
||||||
|
orders := make(map[int]bool)
|
||||||
|
for _, stepOrder := range stepOrders {
|
||||||
|
// Check step exists and belongs to workflow
|
||||||
|
step, err := _i.ApprovalWorkflowStepsRepository.FindOne(clientId, stepOrder.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if step == nil {
|
||||||
|
return fmt.Errorf("step with ID %d not found", stepOrder.ID)
|
||||||
|
}
|
||||||
|
if step.WorkflowId != workflowID {
|
||||||
|
return fmt.Errorf("step with ID %d does not belong to workflow %d", stepOrder.ID, workflowID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate orders
|
||||||
|
if orders[stepOrder.StepOrder] {
|
||||||
|
return fmt.Errorf("duplicate step order %d", stepOrder.StepOrder)
|
||||||
|
}
|
||||||
|
orders[stepOrder.StepOrder] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert stepOrders to the format expected by repository
|
||||||
|
stepOrderMaps := make([]map[string]interface{}, len(stepOrders))
|
||||||
|
for i, stepOrder := range stepOrders {
|
||||||
|
stepOrderMaps[i] = map[string]interface{}{
|
||||||
|
"id": stepOrder.ID,
|
||||||
|
"step_order": stepOrder.StepOrder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ApprovalWorkflowStepsRepository.ReorderSteps(clientId, workflowID, stepOrderMaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) ValidateStep(clientId *uuid.UUID, step *entity.ApprovalWorkflowSteps) (isValid bool, errors []string, err error) {
|
||||||
|
var validationErrors []string
|
||||||
|
|
||||||
|
// Validate step name
|
||||||
|
if step.StepName == "" {
|
||||||
|
validationErrors = append(validationErrors, "step name is required")
|
||||||
|
}
|
||||||
|
if len(step.StepName) < 3 {
|
||||||
|
validationErrors = append(validationErrors, "step name must be at least 3 characters")
|
||||||
|
}
|
||||||
|
if len(step.StepName) > 500 {
|
||||||
|
validationErrors = append(validationErrors, "step name must not exceed 500 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate step order
|
||||||
|
if step.StepOrder < 1 {
|
||||||
|
validationErrors = append(validationErrors, "step order must be at least 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required user level ID
|
||||||
|
if step.RequiredUserLevelId == 0 {
|
||||||
|
validationErrors = append(validationErrors, "required user level ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate auto approve after hours if provided
|
||||||
|
if step.AutoApproveAfterHours != nil && *step.AutoApproveAfterHours < 1 {
|
||||||
|
validationErrors = append(validationErrors, "auto approve after hours must be at least 1")
|
||||||
|
}
|
||||||
|
if step.AutoApproveAfterHours != nil && *step.AutoApproveAfterHours > 720 {
|
||||||
|
validationErrors = append(validationErrors, "auto approve after hours must not exceed 720 (30 days)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(validationErrors) == 0, validationErrors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) CanDeleteStep(clientId *uuid.UUID, id uint) (canDelete bool, reason string, err error) {
|
||||||
|
// Check if step exists
|
||||||
|
step, err := _i.ApprovalWorkflowStepsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if step == nil {
|
||||||
|
return false, "step not found", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are any active approval flows using this step
|
||||||
|
// This would require checking article_approval_step_logs table
|
||||||
|
// For now, we'll allow deletion but this should be implemented
|
||||||
|
// based on business requirements
|
||||||
|
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowStepsService) ValidateStepOrder(clientId *uuid.UUID, workflowID uint, stepOrder int, excludeID *uint) (isValid bool, err error) {
|
||||||
|
existingStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, workflowID, stepOrder)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no existing step found, order is valid
|
||||||
|
if existingStep == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If excludeID is provided and matches existing step, order is valid (updating same step)
|
||||||
|
if excludeID != nil && existingStep.ID == *excludeID {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order already exists for different step
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package approval_workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"web-medols-be/app/module/approval_workflows/controller"
|
||||||
|
"web-medols-be/app/module/approval_workflows/repository"
|
||||||
|
"web-medols-be/app/module/approval_workflows/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApprovalWorkflowsRouter struct of ApprovalWorkflowsRouter
|
||||||
|
type ApprovalWorkflowsRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller controller.ApprovalWorkflowsController
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApprovalWorkflowsModule register bulky of ApprovalWorkflows module
|
||||||
|
var NewApprovalWorkflowsModule = fx.Options(
|
||||||
|
// register repository of ApprovalWorkflows module
|
||||||
|
fx.Provide(repository.NewApprovalWorkflowsRepository),
|
||||||
|
|
||||||
|
// register service of ApprovalWorkflows module
|
||||||
|
fx.Provide(service.NewApprovalWorkflowsService),
|
||||||
|
|
||||||
|
// register controller of ApprovalWorkflows module
|
||||||
|
fx.Provide(controller.NewApprovalWorkflowsController),
|
||||||
|
|
||||||
|
// register router of ApprovalWorkflows module
|
||||||
|
fx.Provide(NewApprovalWorkflowsRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewApprovalWorkflowsRouter init ApprovalWorkflowsRouter
|
||||||
|
func NewApprovalWorkflowsRouter(fiber *fiber.App, controller controller.ApprovalWorkflowsController) *ApprovalWorkflowsRouter {
|
||||||
|
return &ApprovalWorkflowsRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterApprovalWorkflowsRoutes register routes of ApprovalWorkflows module
|
||||||
|
func (_i *ApprovalWorkflowsRouter) RegisterApprovalWorkflowsRoutes() {
|
||||||
|
// define controllers
|
||||||
|
approvalWorkflowsController := _i.Controller
|
||||||
|
|
||||||
|
// define routes
|
||||||
|
_i.App.Route("/approval-workflows", func(router fiber.Router) {
|
||||||
|
router.Get("/", approvalWorkflowsController.All)
|
||||||
|
router.Get("/default", approvalWorkflowsController.GetDefault)
|
||||||
|
router.Get("/:id", approvalWorkflowsController.Show)
|
||||||
|
router.Get("/:id/with-steps", approvalWorkflowsController.GetWithSteps)
|
||||||
|
router.Post("/", approvalWorkflowsController.Save)
|
||||||
|
router.Post("/with-steps", approvalWorkflowsController.SaveWithSteps)
|
||||||
|
router.Put("/:id", approvalWorkflowsController.Update)
|
||||||
|
router.Put("/:id/with-steps", approvalWorkflowsController.UpdateWithSteps)
|
||||||
|
router.Put("/:id/set-default", approvalWorkflowsController.SetDefault)
|
||||||
|
router.Put("/:id/activate", approvalWorkflowsController.Activate)
|
||||||
|
router.Put("/:id/deactivate", approvalWorkflowsController.Deactivate)
|
||||||
|
router.Delete("/:id", approvalWorkflowsController.Delete)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,481 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"strconv"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/middleware"
|
||||||
|
"web-medols-be/app/module/approval_workflows/request"
|
||||||
|
"web-medols-be/app/module/approval_workflows/service"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
utilRes "web-medols-be/utils/response"
|
||||||
|
utilVal "web-medols-be/utils/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type approvalWorkflowsController struct {
|
||||||
|
approvalWorkflowsService service.ApprovalWorkflowsService
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsController interface {
|
||||||
|
All(c *fiber.Ctx) error
|
||||||
|
Show(c *fiber.Ctx) error
|
||||||
|
Save(c *fiber.Ctx) error
|
||||||
|
Update(c *fiber.Ctx) error
|
||||||
|
Delete(c *fiber.Ctx) error
|
||||||
|
GetDefault(c *fiber.Ctx) error
|
||||||
|
SetDefault(c *fiber.Ctx) error
|
||||||
|
Activate(c *fiber.Ctx) error
|
||||||
|
Deactivate(c *fiber.Ctx) error
|
||||||
|
GetWithSteps(c *fiber.Ctx) error
|
||||||
|
SaveWithSteps(c *fiber.Ctx) error
|
||||||
|
UpdateWithSteps(c *fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalWorkflowsController(approvalWorkflowsService service.ApprovalWorkflowsService, log zerolog.Logger) ApprovalWorkflowsController {
|
||||||
|
return &approvalWorkflowsController{
|
||||||
|
approvalWorkflowsService: approvalWorkflowsService,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All ApprovalWorkflows
|
||||||
|
// @Summary Get all ApprovalWorkflows
|
||||||
|
// @Description API for getting all ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param req query request.ApprovalWorkflowsQueryRequest false "query parameters"
|
||||||
|
// @Param req query paginator.Pagination false "pagination parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows [get]
|
||||||
|
func (_i *approvalWorkflowsController) All(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqContext := request.ApprovalWorkflowsQueryRequestContext{
|
||||||
|
Name: c.Query("name"),
|
||||||
|
Description: c.Query("description"),
|
||||||
|
IsActive: c.Query("isActive"),
|
||||||
|
IsDefault: c.Query("isDefault"),
|
||||||
|
}
|
||||||
|
req := reqContext.ToParamRequest()
|
||||||
|
req.Pagination = paginate
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
_i.Log.Info().Interface("clientId", clientId).Msg("")
|
||||||
|
|
||||||
|
approvalWorkflowsData, paging, err := _i.approvalWorkflowsService.GetAll(clientId, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows list successfully retrieved"},
|
||||||
|
Data: approvalWorkflowsData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show ApprovalWorkflows
|
||||||
|
// @Summary Get one ApprovalWorkflows
|
||||||
|
// @Description API for getting one ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id} [get]
|
||||||
|
func (_i *approvalWorkflowsController) Show(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
approvalWorkflowsData, err := _i.approvalWorkflowsService.FindOne(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows successfully retrieved"},
|
||||||
|
Data: approvalWorkflowsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save ApprovalWorkflows
|
||||||
|
// @Summary Save ApprovalWorkflows
|
||||||
|
// @Description API for saving ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param payload body request.ApprovalWorkflowsCreateRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows [post]
|
||||||
|
func (_i *approvalWorkflowsController) Save(c *fiber.Ctx) error {
|
||||||
|
req := new(request.ApprovalWorkflowsCreateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entity
|
||||||
|
workflow := req.ToEntity()
|
||||||
|
steps := req.ToStepsEntity()
|
||||||
|
|
||||||
|
approvalWorkflowsData, err := _i.approvalWorkflowsService.Create(clientId, workflow, steps)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows successfully created"},
|
||||||
|
Data: approvalWorkflowsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update ApprovalWorkflows
|
||||||
|
// @Summary Update ApprovalWorkflows
|
||||||
|
// @Description API for updating ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Param payload body request.ApprovalWorkflowsUpdateRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id} [put]
|
||||||
|
func (_i *approvalWorkflowsController) Update(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ApprovalWorkflowsUpdateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entity
|
||||||
|
workflow := req.ToEntity()
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowsService.Update(clientId, uint(id), workflow)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows successfully updated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete ApprovalWorkflows
|
||||||
|
// @Summary Delete ApprovalWorkflows
|
||||||
|
// @Description API for deleting ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id} [delete]
|
||||||
|
func (_i *approvalWorkflowsController) Delete(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowsService.Delete(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows successfully deleted"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault ApprovalWorkflows
|
||||||
|
// @Summary Get default ApprovalWorkflows
|
||||||
|
// @Description API for getting default ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/default [get]
|
||||||
|
func (_i *approvalWorkflowsController) GetDefault(c *fiber.Ctx) error {
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
approvalWorkflowsData, err := _i.approvalWorkflowsService.GetDefault(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Default ApprovalWorkflows successfully retrieved"},
|
||||||
|
Data: approvalWorkflowsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefault ApprovalWorkflows
|
||||||
|
// @Summary Set default ApprovalWorkflows
|
||||||
|
// @Description API for setting default ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id}/set-default [put]
|
||||||
|
func (_i *approvalWorkflowsController) SetDefault(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowsService.SetDefault(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows successfully set as default"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate ApprovalWorkflows
|
||||||
|
// @Summary Activate ApprovalWorkflows
|
||||||
|
// @Description API for activating ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id}/activate [put]
|
||||||
|
func (_i *approvalWorkflowsController) Activate(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowsService.ActivateWorkflow(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows successfully activated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate ApprovalWorkflows
|
||||||
|
// @Summary Deactivate ApprovalWorkflows
|
||||||
|
// @Description API for deactivating ApprovalWorkflows
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id}/deactivate [put]
|
||||||
|
func (_i *approvalWorkflowsController) Deactivate(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowsService.DeactivateWorkflow(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows successfully deactivated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWithSteps ApprovalWorkflows
|
||||||
|
// @Summary Get ApprovalWorkflows with steps
|
||||||
|
// @Description API for getting ApprovalWorkflows with steps
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id}/with-steps [get]
|
||||||
|
func (_i *approvalWorkflowsController) GetWithSteps(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
workflowData, stepsData, err := _i.approvalWorkflowsService.GetWorkflowWithSteps(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine workflow and steps data
|
||||||
|
responseData := map[string]interface{}{
|
||||||
|
"workflow": workflowData,
|
||||||
|
"steps": stepsData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows with steps successfully retrieved"},
|
||||||
|
Data: responseData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveWithSteps ApprovalWorkflows
|
||||||
|
// @Summary Create ApprovalWorkflows with steps
|
||||||
|
// @Description API for creating ApprovalWorkflows with steps
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param req body request.ApprovalWorkflowsWithStepsCreateRequest true "ApprovalWorkflows with steps data"
|
||||||
|
// @Success 201 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/with-steps [post]
|
||||||
|
func (_i *approvalWorkflowsController) SaveWithSteps(c *fiber.Ctx) error {
|
||||||
|
req := new(request.ApprovalWorkflowsWithStepsCreateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entities
|
||||||
|
workflow := req.ToEntity()
|
||||||
|
steps := req.ToStepsEntity()
|
||||||
|
|
||||||
|
approvalWorkflowsData, err := _i.approvalWorkflowsService.CreateWorkflowWithSteps(clientId, workflow, steps)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows with steps successfully created"},
|
||||||
|
Data: approvalWorkflowsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWithSteps ApprovalWorkflows
|
||||||
|
// @Summary Update ApprovalWorkflows with steps
|
||||||
|
// @Description API for updating ApprovalWorkflows with steps
|
||||||
|
// @Tags ApprovalWorkflows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ApprovalWorkflows ID"
|
||||||
|
// @Param req body request.ApprovalWorkflowsWithStepsUpdateRequest true "ApprovalWorkflows with steps data"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /approval-workflows/{id}/with-steps [put]
|
||||||
|
func (_i *approvalWorkflowsController) UpdateWithSteps(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ApprovalWorkflowsWithStepsUpdateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entities
|
||||||
|
workflow := &entity.ApprovalWorkflows{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: &req.Description,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
IsDefault: req.IsDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := make([]*entity.ApprovalWorkflowSteps, len(req.Steps))
|
||||||
|
for i, stepReq := range req.Steps {
|
||||||
|
steps[i] = stepReq.ToEntity(uint(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.approvalWorkflowsService.UpdateWorkflowWithSteps(clientId, uint(id), workflow, steps)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ApprovalWorkflows with steps successfully updated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
approvalWorkflowStepsMapper "web-medols-be/app/module/approval_workflow_steps/mapper"
|
||||||
|
approvalWorkflowStepsRepository "web-medols-be/app/module/approval_workflow_steps/repository"
|
||||||
|
approvalWorkflowStepsResponse "web-medols-be/app/module/approval_workflow_steps/response"
|
||||||
|
res "web-medols-be/app/module/approval_workflows/response"
|
||||||
|
usersRepository "web-medols-be/app/module/users/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApprovalWorkflowsResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
approvalWorkflowsReq *entity.ApprovalWorkflows,
|
||||||
|
approvalWorkflowStepsRepo approvalWorkflowStepsRepository.ApprovalWorkflowStepsRepository,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (approvalWorkflowsRes *res.ApprovalWorkflowsResponse) {
|
||||||
|
|
||||||
|
// Get workflow steps using GetAll with filter
|
||||||
|
var workflowStepsArr []*approvalWorkflowStepsResponse.ApprovalWorkflowStepsResponse
|
||||||
|
if len(approvalWorkflowsReq.Steps) > 0 {
|
||||||
|
for _, step := range approvalWorkflowsReq.Steps {
|
||||||
|
workflowStepsArr = append(workflowStepsArr, approvalWorkflowStepsMapper.ApprovalWorkflowStepsResponseMapper(
|
||||||
|
log,
|
||||||
|
clientId,
|
||||||
|
&step,
|
||||||
|
usersRepo,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if approvalWorkflowsReq != nil {
|
||||||
|
// Convert boolean pointer to boolean value
|
||||||
|
isActive := false
|
||||||
|
if approvalWorkflowsReq.IsActive != nil {
|
||||||
|
isActive = *approvalWorkflowsReq.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
approvalWorkflowsRes = &res.ApprovalWorkflowsResponse{
|
||||||
|
ID: approvalWorkflowsReq.ID,
|
||||||
|
Name: approvalWorkflowsReq.Name,
|
||||||
|
Description: approvalWorkflowsReq.Description,
|
||||||
|
IsActive: isActive,
|
||||||
|
CreatedBy: 0, // Default value since entity doesn't have CreatedBy field
|
||||||
|
CreatedAt: approvalWorkflowsReq.CreatedAt,
|
||||||
|
UpdatedAt: approvalWorkflowsReq.UpdatedAt,
|
||||||
|
Steps: workflowStepsArr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvalWorkflowsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApprovalWorkflowsWithStepsResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
approvalWorkflowsReq *entity.ApprovalWorkflows,
|
||||||
|
approvalWorkflowStepsRepo approvalWorkflowStepsRepository.ApprovalWorkflowStepsRepository,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (approvalWorkflowsRes *res.ApprovalWorkflowsWithStepsResponse) {
|
||||||
|
|
||||||
|
// Get workflow steps with detailed information
|
||||||
|
var workflowStepsArr []*approvalWorkflowStepsResponse.ApprovalWorkflowStepsResponse
|
||||||
|
if len(approvalWorkflowsReq.Steps) > 0 {
|
||||||
|
for _, step := range approvalWorkflowsReq.Steps {
|
||||||
|
workflowStepsArr = append(workflowStepsArr, approvalWorkflowStepsMapper.ApprovalWorkflowStepsResponseMapper(
|
||||||
|
log,
|
||||||
|
clientId,
|
||||||
|
&step,
|
||||||
|
usersRepo,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if approvalWorkflowsReq != nil {
|
||||||
|
// Convert boolean pointer to boolean value
|
||||||
|
isActive := false
|
||||||
|
if approvalWorkflowsReq.IsActive != nil {
|
||||||
|
isActive = *approvalWorkflowsReq.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalWorkflowsRes = &res.ApprovalWorkflowsWithStepsResponse{
|
||||||
|
ID: approvalWorkflowsReq.ID,
|
||||||
|
Name: approvalWorkflowsReq.Name,
|
||||||
|
Description: approvalWorkflowsReq.Description,
|
||||||
|
IsActive: isActive,
|
||||||
|
CreatedBy: 0, // Default value since entity doesn't have CreatedBy field
|
||||||
|
CreatedAt: approvalWorkflowsReq.CreatedAt,
|
||||||
|
UpdatedAt: approvalWorkflowsReq.UpdatedAt,
|
||||||
|
Steps: workflowStepsArr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvalWorkflowsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApprovalWorkflowsSummaryResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
approvalWorkflowsReq *entity.ApprovalWorkflows,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (approvalWorkflowsRes *res.ApprovalWorkflowsSummaryResponse) {
|
||||||
|
|
||||||
|
if approvalWorkflowsReq != nil {
|
||||||
|
// Convert boolean pointer to boolean value
|
||||||
|
isActive := false
|
||||||
|
if approvalWorkflowsReq.IsActive != nil {
|
||||||
|
isActive = *approvalWorkflowsReq.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalWorkflowsRes = &res.ApprovalWorkflowsSummaryResponse{
|
||||||
|
ID: approvalWorkflowsReq.ID,
|
||||||
|
Name: approvalWorkflowsReq.Name,
|
||||||
|
Description: approvalWorkflowsReq.Description,
|
||||||
|
IsActive: isActive,
|
||||||
|
StepCount: 0, // Default value, should be calculated if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvalWorkflowsRes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/database"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
approvalWorkflowStepsEntity "web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/approval_workflows/request"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type approvalWorkflowsRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApprovalWorkflowsRepository define interface of IApprovalWorkflowsRepository
|
||||||
|
type ApprovalWorkflowsRepository interface {
|
||||||
|
GetAll(clientId *uuid.UUID, req request.ApprovalWorkflowsQueryRequest) (workflows []*entity.ApprovalWorkflows, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (workflow *entity.ApprovalWorkflows, err error)
|
||||||
|
FindDefault(clientId *uuid.UUID) (workflow *entity.ApprovalWorkflows, err error)
|
||||||
|
GetDefault(clientId *uuid.UUID) (workflow *entity.ApprovalWorkflows, err error)
|
||||||
|
GetWorkflowSteps(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error)
|
||||||
|
Create(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows) (workflowReturn *entity.ApprovalWorkflows, err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, workflow *entity.ApprovalWorkflows) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
SetDefault(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalWorkflowsRepository(db *database.Database, log zerolog.Logger) ApprovalWorkflowsRepository {
|
||||||
|
return &approvalWorkflowsRepository{
|
||||||
|
DB: db,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement interface of IApprovalWorkflowsRepository
|
||||||
|
func (_i *approvalWorkflowsRepository) GetAll(clientId *uuid.UUID, req request.ApprovalWorkflowsQueryRequest) (workflows []*entity.ApprovalWorkflows, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflows{})
|
||||||
|
|
||||||
|
// Add client filter
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("approval_workflows.is_active = ?", true)
|
||||||
|
|
||||||
|
if req.Name != nil && *req.Name != "" {
|
||||||
|
name := strings.ToLower(*req.Name)
|
||||||
|
query = query.Where("LOWER(approval_workflows.name) LIKE ?", "%"+strings.ToLower(name)+"%")
|
||||||
|
}
|
||||||
|
if req.Description != nil && *req.Description != "" {
|
||||||
|
description := strings.ToLower(*req.Description)
|
||||||
|
query = query.Where("LOWER(approval_workflows.description) LIKE ?", "%"+strings.ToLower(description)+"%")
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
query = query.Where("approval_workflows.is_active = ?", req.IsActive)
|
||||||
|
}
|
||||||
|
if req.IsDefault != nil {
|
||||||
|
query = query.Where("approval_workflows.is_default = ?", req.IsDefault)
|
||||||
|
}
|
||||||
|
query.Count(&count)
|
||||||
|
|
||||||
|
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 := "approval_workflows.is_default DESC, approval_workflows.created_at"
|
||||||
|
query.Order(fmt.Sprintf("%s %s", sortBy, direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Pagination.Count = count
|
||||||
|
req.Pagination = paginator.Paging(req.Pagination)
|
||||||
|
|
||||||
|
err = query.Preload("Steps").Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&workflows).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = *req.Pagination
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsRepository) FindOne(clientId *uuid.UUID, id uint) (workflow *entity.ApprovalWorkflows, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
query = query.Preload("Steps")
|
||||||
|
|
||||||
|
err = query.First(&workflow, id).Error
|
||||||
|
return workflow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsRepository) FindDefault(clientId *uuid.UUID) (workflow *entity.ApprovalWorkflows, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("is_active = ? AND is_default = ?", true, true)
|
||||||
|
query = query.Preload("Steps")
|
||||||
|
|
||||||
|
err = query.First(&workflow).Error
|
||||||
|
return workflow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsRepository) Create(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows) (workflowReturn *entity.ApprovalWorkflows, err error) {
|
||||||
|
workflow.ClientId = clientId
|
||||||
|
err = _i.DB.DB.Create(&workflow).Error
|
||||||
|
return workflow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsRepository) Update(clientId *uuid.UUID, id uint, workflow *entity.ApprovalWorkflows) (err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.Where("id = ?", id).Updates(workflow).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsRepository) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive := false
|
||||||
|
err = query.Where("id = ?", id).Update("is_active", isActive).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsRepository) SetDefault(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
// First, unset all default workflows
|
||||||
|
query := _i.DB.DB.Model(&entity.ApprovalWorkflows{})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
err = query.Update("is_default", false).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then set the specified workflow as default
|
||||||
|
query = _i.DB.DB.Model(&entity.ApprovalWorkflows{})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
err = query.Where("id = ?", id).Update("is_default", true).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault gets the default workflow for a client
|
||||||
|
func (_i *approvalWorkflowsRepository) GetDefault(clientId *uuid.UUID) (workflow *entity.ApprovalWorkflows, err error) {
|
||||||
|
return _i.FindDefault(clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkflowSteps gets all steps for a specific workflow
|
||||||
|
func (_i *approvalWorkflowsRepository) GetWorkflowSteps(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
query := _i.DB.DB.Model(&approvalWorkflowStepsEntity.ApprovalWorkflowSteps{})
|
||||||
|
|
||||||
|
// Join with approval_workflows to check client_id
|
||||||
|
query = query.Joins("JOIN approval_workflows ON approval_workflow_steps.workflow_id = approval_workflows.id")
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("approval_workflows.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("approval_workflow_steps.workflow_id = ?", workflowId)
|
||||||
|
query = query.Where("approval_workflows.is_active = ?", true)
|
||||||
|
query = query.Order("approval_workflow_steps.step_order ASC")
|
||||||
|
|
||||||
|
// Preload the RequiredUserLevel relation
|
||||||
|
query = query.Preload("RequiredUserLevel")
|
||||||
|
|
||||||
|
err = query.Find(&steps).Error
|
||||||
|
return steps, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalWorkflowsGeneric interface {
|
||||||
|
ToEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsQueryRequest struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
IsActive *bool `json:"isActive"`
|
||||||
|
IsDefault *bool `json:"isDefault"`
|
||||||
|
Pagination *paginator.Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsCreateRequest struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
IsActive *bool `json:"isActive"`
|
||||||
|
IsDefault *bool `json:"isDefault"`
|
||||||
|
RequiresApproval *bool `json:"requiresApproval"`
|
||||||
|
AutoPublish *bool `json:"autoPublish"`
|
||||||
|
Steps []ApprovalWorkflowStepRequest `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ApprovalWorkflowsCreateRequest) ToEntity() *entity.ApprovalWorkflows {
|
||||||
|
return &entity.ApprovalWorkflows{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: &req.Description,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
IsDefault: req.IsDefault,
|
||||||
|
RequiresApproval: req.RequiresApproval,
|
||||||
|
AutoPublish: req.AutoPublish,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ApprovalWorkflowsCreateRequest) ToStepsEntity() []*entity.ApprovalWorkflowSteps {
|
||||||
|
steps := make([]*entity.ApprovalWorkflowSteps, len(req.Steps))
|
||||||
|
for i, stepReq := range req.Steps {
|
||||||
|
steps[i] = &entity.ApprovalWorkflowSteps{
|
||||||
|
StepOrder: stepReq.StepOrder,
|
||||||
|
StepName: stepReq.StepName,
|
||||||
|
RequiredUserLevelId: stepReq.RequiredUserLevelId,
|
||||||
|
CanSkip: stepReq.CanSkip,
|
||||||
|
AutoApproveAfterHours: stepReq.AutoApproveAfterHours,
|
||||||
|
IsActive: stepReq.IsActive,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return steps
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsUpdateRequest struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
IsActive *bool `json:"isActive"`
|
||||||
|
IsDefault *bool `json:"isDefault"`
|
||||||
|
RequiresApproval *bool `json:"requiresApproval"`
|
||||||
|
AutoPublish *bool `json:"autoPublish"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ApprovalWorkflowsUpdateRequest) ToEntity() *entity.ApprovalWorkflows {
|
||||||
|
return &entity.ApprovalWorkflows{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: &req.Description,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
IsDefault: req.IsDefault,
|
||||||
|
RequiresApproval: req.RequiresApproval,
|
||||||
|
AutoPublish: req.AutoPublish,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowStepRequest struct {
|
||||||
|
StepOrder int `json:"stepOrder" validate:"required"`
|
||||||
|
StepName string `json:"stepName" validate:"required"`
|
||||||
|
RequiredUserLevelId uint `json:"requiredUserLevelId" validate:"required"`
|
||||||
|
CanSkip *bool `json:"canSkip"`
|
||||||
|
AutoApproveAfterHours *int `json:"autoApproveAfterHours"`
|
||||||
|
IsActive *bool `json:"isActive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.ApprovalWorkflowSteps {
|
||||||
|
return &entity.ApprovalWorkflowSteps{
|
||||||
|
WorkflowId: workflowId,
|
||||||
|
StepOrder: req.StepOrder,
|
||||||
|
StepName: req.StepName,
|
||||||
|
RequiredUserLevelId: req.RequiredUserLevelId,
|
||||||
|
CanSkip: req.CanSkip,
|
||||||
|
AutoApproveAfterHours: req.AutoApproveAfterHours,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsWithStepsCreateRequest struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
IsActive *bool `json:"isActive"`
|
||||||
|
IsDefault *bool `json:"isDefault"`
|
||||||
|
RequiresApproval *bool `json:"requiresApproval"`
|
||||||
|
AutoPublish *bool `json:"autoPublish"`
|
||||||
|
Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ApprovalWorkflowsWithStepsCreateRequest) ToEntity() *entity.ApprovalWorkflows {
|
||||||
|
return &entity.ApprovalWorkflows{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: &req.Description,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
IsDefault: req.IsDefault,
|
||||||
|
RequiresApproval: req.RequiresApproval,
|
||||||
|
AutoPublish: req.AutoPublish,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ApprovalWorkflowsWithStepsCreateRequest) ToStepsEntity() []*entity.ApprovalWorkflowSteps {
|
||||||
|
steps := make([]*entity.ApprovalWorkflowSteps, len(req.Steps))
|
||||||
|
for i, stepReq := range req.Steps {
|
||||||
|
steps[i] = &entity.ApprovalWorkflowSteps{
|
||||||
|
StepOrder: stepReq.StepOrder,
|
||||||
|
StepName: stepReq.StepName,
|
||||||
|
RequiredUserLevelId: stepReq.RequiredUserLevelId,
|
||||||
|
CanSkip: stepReq.CanSkip,
|
||||||
|
AutoApproveAfterHours: stepReq.AutoApproveAfterHours,
|
||||||
|
IsActive: stepReq.IsActive,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return steps
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsWithStepsUpdateRequest struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
IsActive *bool `json:"isActive"`
|
||||||
|
IsDefault *bool `json:"isDefault"`
|
||||||
|
RequiresApproval *bool `json:"requiresApproval"`
|
||||||
|
AutoPublish *bool `json:"autoPublish"`
|
||||||
|
Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsQueryRequestContext struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IsActive string `json:"isActive"`
|
||||||
|
IsDefault string `json:"isDefault"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ApprovalWorkflowsQueryRequestContext) ToParamRequest() ApprovalWorkflowsQueryRequest {
|
||||||
|
var name *string
|
||||||
|
var description *string
|
||||||
|
var isActive *bool
|
||||||
|
var isDefault *bool
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
name = &req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Description != "" {
|
||||||
|
description = &req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.IsActive != "" {
|
||||||
|
if parsedIsActive, err := strconv.ParseBool(req.IsActive); err == nil {
|
||||||
|
isActive = &parsedIsActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.IsDefault != "" {
|
||||||
|
if parsedIsDefault, err := strconv.ParseBool(req.IsDefault); err == nil {
|
||||||
|
isDefault = &parsedIsDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApprovalWorkflowsQueryRequest{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
IsActive: isActive,
|
||||||
|
IsDefault: isDefault,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
approvalWorkflowStepsResponse "web-medols-be/app/module/approval_workflow_steps/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalWorkflowsResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
CreatedBy uint `json:"createdBy"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Steps []*approvalWorkflowStepsResponse.ApprovalWorkflowStepsResponse `json:"steps,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsWithStepsResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
CreatedBy uint `json:"createdBy"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Steps []*approvalWorkflowStepsResponse.ApprovalWorkflowStepsResponse `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsSummaryResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
StepCount int `json:"stepCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkflowsStatsResponse struct {
|
||||||
|
TotalWorkflows int `json:"totalWorkflows"`
|
||||||
|
ActiveWorkflows int `json:"activeWorkflows"`
|
||||||
|
InactiveWorkflows int `json:"inactiveWorkflows"`
|
||||||
|
TotalSteps int `json:"totalSteps"`
|
||||||
|
AverageStepsPerFlow int `json:"averageStepsPerFlow"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,319 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/approval_workflows/repository"
|
||||||
|
"web-medols-be/app/module/approval_workflows/request"
|
||||||
|
stepRepo "web-medols-be/app/module/approval_workflow_steps/repository"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type approvalWorkflowsService struct {
|
||||||
|
ApprovalWorkflowsRepository repository.ApprovalWorkflowsRepository
|
||||||
|
ApprovalWorkflowStepsRepository stepRepo.ApprovalWorkflowStepsRepository
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApprovalWorkflowsService define interface of IApprovalWorkflowsService
|
||||||
|
type ApprovalWorkflowsService interface {
|
||||||
|
// Basic CRUD
|
||||||
|
GetAll(clientId *uuid.UUID, req request.ApprovalWorkflowsQueryRequest) (workflows []*entity.ApprovalWorkflows, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (workflow *entity.ApprovalWorkflows, err error)
|
||||||
|
Create(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (workflowReturn *entity.ApprovalWorkflows, err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, workflow *entity.ApprovalWorkflows) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Workflow management
|
||||||
|
GetDefault(clientId *uuid.UUID) (workflow *entity.ApprovalWorkflows, err error)
|
||||||
|
SetDefault(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
ActivateWorkflow(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
DeactivateWorkflow(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Workflow with steps
|
||||||
|
GetWorkflowWithSteps(clientId *uuid.UUID, id uint) (workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps, err error)
|
||||||
|
CreateWorkflowWithSteps(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (workflowReturn *entity.ApprovalWorkflows, err error)
|
||||||
|
UpdateWorkflowWithSteps(clientId *uuid.UUID, id uint, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (err error)
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
ValidateWorkflow(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (isValid bool, errors []string, err error)
|
||||||
|
CanDeleteWorkflow(clientId *uuid.UUID, id uint) (canDelete bool, reason string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalWorkflowsService(
|
||||||
|
approvalWorkflowsRepository repository.ApprovalWorkflowsRepository,
|
||||||
|
approvalWorkflowStepsRepository stepRepo.ApprovalWorkflowStepsRepository,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) ApprovalWorkflowsService {
|
||||||
|
return &approvalWorkflowsService{
|
||||||
|
ApprovalWorkflowsRepository: approvalWorkflowsRepository,
|
||||||
|
ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic CRUD implementations
|
||||||
|
func (_i *approvalWorkflowsService) GetAll(clientId *uuid.UUID, req request.ApprovalWorkflowsQueryRequest) (workflows []*entity.ApprovalWorkflows, paging paginator.Pagination, err error) {
|
||||||
|
return _i.ApprovalWorkflowsRepository.GetAll(clientId, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) FindOne(clientId *uuid.UUID, id uint) (workflow *entity.ApprovalWorkflows, err error) {
|
||||||
|
return _i.ApprovalWorkflowsRepository.FindOne(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) Create(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (workflowReturn *entity.ApprovalWorkflows, err error) {
|
||||||
|
// Validate workflow and steps
|
||||||
|
isValid, validationErrors, err := _i.ValidateWorkflow(clientId, workflow, steps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Validation failed: %v", validationErrors))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create workflow
|
||||||
|
workflowReturn, err = _i.ApprovalWorkflowsRepository.Create(clientId, workflow)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create steps
|
||||||
|
for i, step := range steps {
|
||||||
|
step.WorkflowId = workflowReturn.ID
|
||||||
|
step.StepOrder = i + 1
|
||||||
|
_, err = _i.ApprovalWorkflowStepsRepository.Create(clientId, step)
|
||||||
|
if err != nil {
|
||||||
|
// Rollback workflow creation if step creation fails
|
||||||
|
_i.ApprovalWorkflowsRepository.Delete(clientId, workflowReturn.ID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflowReturn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) Update(clientId *uuid.UUID, id uint, workflow *entity.ApprovalWorkflows) (err error) {
|
||||||
|
// Check if workflow exists
|
||||||
|
existingWorkflow, err := _i.ApprovalWorkflowsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingWorkflow == nil {
|
||||||
|
return errors.New("workflow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ApprovalWorkflowsRepository.Update(clientId, id, workflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
// Check if workflow can be deleted
|
||||||
|
canDelete, reason, err := _i.CanDeleteWorkflow(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canDelete {
|
||||||
|
return errors.New(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ApprovalWorkflowsRepository.Delete(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workflow management
|
||||||
|
func (_i *approvalWorkflowsService) GetDefault(clientId *uuid.UUID) (workflow *entity.ApprovalWorkflows, err error) {
|
||||||
|
return _i.ApprovalWorkflowsRepository.FindDefault(clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) SetDefault(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
// Check if workflow exists and is active
|
||||||
|
workflow, err := _i.ApprovalWorkflowsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if workflow == nil {
|
||||||
|
return errors.New("workflow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if workflow.IsActive == nil || !*workflow.IsActive {
|
||||||
|
return errors.New("cannot set inactive workflow as default")
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ApprovalWorkflowsRepository.SetDefault(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) ActivateWorkflow(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
// Validate workflow before activation
|
||||||
|
workflow, err := _i.ApprovalWorkflowsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if workflow == nil {
|
||||||
|
return errors.New("workflow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get workflow steps and validate
|
||||||
|
steps, err := _i.ApprovalWorkflowStepsRepository.GetByWorkflowId(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid, validationErrors, err := _i.ValidateWorkflow(clientId, workflow, steps)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
return errors.New(fmt.Sprintf("Cannot activate invalid workflow: %v", validationErrors))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate workflow
|
||||||
|
isActive := true
|
||||||
|
updateData := &entity.ApprovalWorkflows{IsActive: &isActive}
|
||||||
|
return _i.ApprovalWorkflowsRepository.Update(clientId, id, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) DeactivateWorkflow(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
// Check if this is the default workflow
|
||||||
|
defaultWorkflow, err := _i.ApprovalWorkflowsRepository.FindDefault(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultWorkflow != nil && defaultWorkflow.ID == id {
|
||||||
|
return errors.New("cannot deactivate default workflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if workflow is being used in active approval flows
|
||||||
|
canDelete, reason, err := _i.CanDeleteWorkflow(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canDelete {
|
||||||
|
return errors.New(fmt.Sprintf("Cannot deactivate workflow: %s", reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate workflow
|
||||||
|
isActive := false
|
||||||
|
updateData := &entity.ApprovalWorkflows{IsActive: &isActive}
|
||||||
|
return _i.ApprovalWorkflowsRepository.Update(clientId, id, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workflow with steps
|
||||||
|
func (_i *approvalWorkflowsService) GetWorkflowWithSteps(clientId *uuid.UUID, id uint) (workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
workflow, err = _i.ApprovalWorkflowsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
steps, err = _i.ApprovalWorkflowStepsRepository.GetByWorkflowId(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflow, steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) CreateWorkflowWithSteps(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (workflowReturn *entity.ApprovalWorkflows, err error) {
|
||||||
|
return _i.Create(clientId, workflow, steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) UpdateWorkflowWithSteps(clientId *uuid.UUID, id uint, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (err error) {
|
||||||
|
// Update workflow
|
||||||
|
err = _i.Update(clientId, id, workflow)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing steps
|
||||||
|
existingSteps, err := _i.ApprovalWorkflowStepsRepository.GetByWorkflowId(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing steps (simplified approach - in production, you might want to update/merge)
|
||||||
|
for _, existingStep := range existingSteps {
|
||||||
|
err = _i.ApprovalWorkflowStepsRepository.Delete(clientId, existingStep.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new steps
|
||||||
|
for i, step := range steps {
|
||||||
|
step.WorkflowId = id
|
||||||
|
step.StepOrder = i + 1
|
||||||
|
_, err = _i.ApprovalWorkflowStepsRepository.Create(clientId, step)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
func (_i *approvalWorkflowsService) ValidateWorkflow(clientId *uuid.UUID, workflow *entity.ApprovalWorkflows, steps []*entity.ApprovalWorkflowSteps) (isValid bool, errors []string, err error) {
|
||||||
|
errors = make([]string, 0)
|
||||||
|
|
||||||
|
// Validate workflow
|
||||||
|
if workflow.Name == "" {
|
||||||
|
errors = append(errors, "Workflow name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate steps
|
||||||
|
if len(steps) == 0 {
|
||||||
|
errors = append(errors, "Workflow must have at least one step")
|
||||||
|
} else {
|
||||||
|
// Check for duplicate step orders
|
||||||
|
stepOrderMap := make(map[int]bool)
|
||||||
|
for i, step := range steps {
|
||||||
|
expectedOrder := i + 1
|
||||||
|
if step.StepOrder != 0 && step.StepOrder != expectedOrder {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step %d has incorrect order %d, expected %d", i+1, step.StepOrder, expectedOrder))
|
||||||
|
}
|
||||||
|
|
||||||
|
if stepOrderMap[step.StepOrder] {
|
||||||
|
errors = append(errors, fmt.Sprintf("Duplicate step order: %d", step.StepOrder))
|
||||||
|
}
|
||||||
|
stepOrderMap[step.StepOrder] = true
|
||||||
|
|
||||||
|
if step.StepName == "" {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step %d name is required", i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if step.RequiredUserLevelId == 0 {
|
||||||
|
errors = append(errors, fmt.Sprintf("Step %d must have a required user level", i+1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = len(errors) == 0
|
||||||
|
return isValid, errors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *approvalWorkflowsService) CanDeleteWorkflow(clientId *uuid.UUID, id uint) (canDelete bool, reason string, err error) {
|
||||||
|
// Check if workflow is default
|
||||||
|
defaultWorkflow, err := _i.ApprovalWorkflowsRepository.FindDefault(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultWorkflow != nil && defaultWorkflow.ID == id {
|
||||||
|
return false, "Cannot delete default workflow", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if workflow is being used in active approval flows
|
||||||
|
// This would require a method in ArticleApprovalFlowsRepository
|
||||||
|
// For now, we'll assume it can be deleted
|
||||||
|
// TODO: Implement check for active approval flows
|
||||||
|
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package article_approval_flows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/controller"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/repository"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArticleApprovalFlowsRouter struct of ArticleApprovalFlowsRouter
|
||||||
|
type ArticleApprovalFlowsRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller controller.ArticleApprovalFlowsController
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArticleApprovalFlowsModule register bulky of ArticleApprovalFlows module
|
||||||
|
var NewArticleApprovalFlowsModule = fx.Options(
|
||||||
|
// register repository of ArticleApprovalFlows module
|
||||||
|
fx.Provide(repository.NewArticleApprovalFlowsRepository),
|
||||||
|
|
||||||
|
// register service of ArticleApprovalFlows module
|
||||||
|
fx.Provide(service.NewArticleApprovalFlowsService),
|
||||||
|
|
||||||
|
// register controller of ArticleApprovalFlows module
|
||||||
|
fx.Provide(controller.NewArticleApprovalFlowsController),
|
||||||
|
|
||||||
|
// register router of ArticleApprovalFlows module
|
||||||
|
fx.Provide(NewArticleApprovalFlowsRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewArticleApprovalFlowsRouter init ArticleApprovalFlowsRouter
|
||||||
|
func NewArticleApprovalFlowsRouter(fiber *fiber.App, controller controller.ArticleApprovalFlowsController) *ArticleApprovalFlowsRouter {
|
||||||
|
return &ArticleApprovalFlowsRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterArticleApprovalFlowsRoutes register routes of ArticleApprovalFlows module
|
||||||
|
func (_i *ArticleApprovalFlowsRouter) RegisterArticleApprovalFlowsRoutes() {
|
||||||
|
// define controllers
|
||||||
|
articleApprovalFlowsController := _i.Controller
|
||||||
|
|
||||||
|
// define routes
|
||||||
|
_i.App.Route("/article-approval-flows", func(router fiber.Router) {
|
||||||
|
router.Get("/", articleApprovalFlowsController.All)
|
||||||
|
router.Get("/my-queue", articleApprovalFlowsController.GetMyApprovalQueue)
|
||||||
|
router.Get("/pending", articleApprovalFlowsController.GetPendingApprovals)
|
||||||
|
router.Get("/history", articleApprovalFlowsController.GetApprovalHistory)
|
||||||
|
router.Get("/dashboard-stats", articleApprovalFlowsController.GetDashboardStats)
|
||||||
|
router.Get("/workload-stats", articleApprovalFlowsController.GetWorkloadStats)
|
||||||
|
router.Get("/analytics", articleApprovalFlowsController.GetApprovalAnalytics)
|
||||||
|
router.Get("/:id", articleApprovalFlowsController.Show)
|
||||||
|
router.Post("/submit", articleApprovalFlowsController.SubmitForApproval)
|
||||||
|
router.Put("/:id/approve", articleApprovalFlowsController.Approve)
|
||||||
|
router.Put("/:id/reject", articleApprovalFlowsController.Reject)
|
||||||
|
router.Put("/:id/request-revision", articleApprovalFlowsController.RequestRevision)
|
||||||
|
router.Put("/:id/resubmit", articleApprovalFlowsController.Resubmit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,664 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"web-medols-be/app/middleware"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/request"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/service"
|
||||||
|
usersRepository "web-medols-be/app/module/users/repository"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
utilSvc "web-medols-be/utils/service"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
|
utilRes "web-medols-be/utils/response"
|
||||||
|
utilVal "web-medols-be/utils/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type articleApprovalFlowsController struct {
|
||||||
|
articleApprovalFlowsService service.ArticleApprovalFlowsService
|
||||||
|
UsersRepo usersRepository.UsersRepository
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalFlowsController interface {
|
||||||
|
All(c *fiber.Ctx) error
|
||||||
|
Show(c *fiber.Ctx) error
|
||||||
|
SubmitForApproval(c *fiber.Ctx) error
|
||||||
|
Approve(c *fiber.Ctx) error
|
||||||
|
Reject(c *fiber.Ctx) error
|
||||||
|
RequestRevision(c *fiber.Ctx) error
|
||||||
|
Resubmit(c *fiber.Ctx) error
|
||||||
|
GetMyApprovalQueue(c *fiber.Ctx) error
|
||||||
|
GetPendingApprovals(c *fiber.Ctx) error
|
||||||
|
GetApprovalHistory(c *fiber.Ctx) error
|
||||||
|
GetDashboardStats(c *fiber.Ctx) error
|
||||||
|
GetWorkloadStats(c *fiber.Ctx) error
|
||||||
|
GetApprovalAnalytics(c *fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArticleApprovalFlowsController(articleApprovalFlowsService service.ArticleApprovalFlowsService, usersRepo usersRepository.UsersRepository, log zerolog.Logger) ArticleApprovalFlowsController {
|
||||||
|
return &articleApprovalFlowsController{
|
||||||
|
articleApprovalFlowsService: articleApprovalFlowsService,
|
||||||
|
UsersRepo: usersRepo,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All ArticleApprovalFlows
|
||||||
|
// @Summary Get all ArticleApprovalFlows
|
||||||
|
// @Description API for getting all ArticleApprovalFlows
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param req query request.ArticleApprovalFlowsQueryRequest false "query parameters"
|
||||||
|
// @Param req query paginator.Pagination false "pagination parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows [get]
|
||||||
|
func (_i *articleApprovalFlowsController) All(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqContext := request.ArticleApprovalFlowsQueryRequestContext{
|
||||||
|
ArticleId: c.Query("articleId"),
|
||||||
|
WorkflowId: c.Query("workflowId"),
|
||||||
|
StatusId: c.Query("statusId"),
|
||||||
|
SubmittedBy: c.Query("submittedBy"),
|
||||||
|
CurrentStep: c.Query("currentStep"),
|
||||||
|
DateFrom: c.Query("dateFrom"),
|
||||||
|
DateTo: c.Query("dateTo"),
|
||||||
|
}
|
||||||
|
req := reqContext.ToParamRequest()
|
||||||
|
req.Pagination = paginate
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
_i.Log.Info().Interface("clientId", clientId).Msg("")
|
||||||
|
|
||||||
|
articleApprovalFlowsData, paging, err := _i.articleApprovalFlowsService.GetAll(clientId, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalFlows list successfully retrieved"},
|
||||||
|
Data: articleApprovalFlowsData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show ArticleApprovalFlows
|
||||||
|
// @Summary Get one ArticleApprovalFlows
|
||||||
|
// @Description API for getting one ArticleApprovalFlows
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ArticleApprovalFlows ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/{id} [get]
|
||||||
|
func (_i *articleApprovalFlowsController) Show(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
articleApprovalFlowsData, err := _i.articleApprovalFlowsService.FindOne(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalFlows successfully retrieved"},
|
||||||
|
Data: articleApprovalFlowsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitForApproval ArticleApprovalFlows
|
||||||
|
// @Summary Submit article for approval
|
||||||
|
// @Description API for submitting article for approval
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param req body request.SubmitForApprovalRequest true "Submit for approval data"
|
||||||
|
// @Success 201 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/submit [post]
|
||||||
|
func (_i *articleApprovalFlowsController) SubmitForApproval(c *fiber.Ctx) error {
|
||||||
|
req := new(request.SubmitForApprovalRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var workflowId *uint
|
||||||
|
if req.WorkflowId != nil {
|
||||||
|
workflowIdVal := uint(*req.WorkflowId)
|
||||||
|
workflowId = &workflowIdVal
|
||||||
|
}
|
||||||
|
articleApprovalFlowsData, err := _i.articleApprovalFlowsService.SubmitArticleForApproval(clientId, uint(req.ArticleId), user.ID, workflowId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Article successfully submitted for approval"},
|
||||||
|
Data: articleApprovalFlowsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve ArticleApprovalFlows
|
||||||
|
// @Summary Approve article
|
||||||
|
// @Description API for approving article
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param id path int true "ArticleApprovalFlows ID"
|
||||||
|
// @Param req body request.ApprovalActionRequest true "Approval action data"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/{id}/approve [put]
|
||||||
|
func (_i *articleApprovalFlowsController) Approve(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ApprovalActionRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.articleApprovalFlowsService.ApproveStep(clientId, uint(id), user.ID, req.Message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Article successfully approved"},
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject ArticleApprovalFlows
|
||||||
|
// @Summary Reject article
|
||||||
|
// @Description API for rejecting article
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param id path int true "ArticleApprovalFlows ID"
|
||||||
|
// @Param req body request.RejectionRequest true "Rejection data"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/{id}/reject [put]
|
||||||
|
func (_i *articleApprovalFlowsController) Reject(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.RejectionRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.articleApprovalFlowsService.RejectArticle(clientId, uint(id), user.ID, req.Reason)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Article successfully rejected"},
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestRevision ArticleApprovalFlows
|
||||||
|
// @Summary Request revision for article
|
||||||
|
// @Description API for requesting revision for article
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param id path int true "ArticleApprovalFlows ID"
|
||||||
|
// @Param req body request.RevisionRequest true "Revision request data"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/{id}/request-revision [put]
|
||||||
|
func (_i *articleApprovalFlowsController) RequestRevision(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.RevisionRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.articleApprovalFlowsService.RequestRevision(clientId, uint(id), user.ID, req.Message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Revision successfully requested"},
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resubmit ArticleApprovalFlows
|
||||||
|
// @Summary Resubmit article after revision
|
||||||
|
// @Description API for resubmitting article after revision
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param id path int true "ArticleApprovalFlows ID"
|
||||||
|
// @Param req body request.ResubmitRequest true "Resubmit data"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/{id}/resubmit [put]
|
||||||
|
func (_i *articleApprovalFlowsController) Resubmit(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ResubmitRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.articleApprovalFlowsService.ResubmitAfterRevision(clientId, uint(id), user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Article successfully resubmitted"},
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyApprovalQueue ArticleApprovalFlows
|
||||||
|
// @Summary Get my approval queue
|
||||||
|
// @Description API for getting my approval queue
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param includePreview query bool false "Include article preview"
|
||||||
|
// @Param urgentOnly query bool false "Show only urgent articles"
|
||||||
|
// @Param req query paginator.Pagination false "pagination parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/my-queue [get]
|
||||||
|
func (_i *articleApprovalFlowsController) GetMyApprovalQueue(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user level ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional parameters
|
||||||
|
includePreview := c.QueryBool("includePreview", false)
|
||||||
|
urgentOnly := c.QueryBool("urgentOnly", false)
|
||||||
|
|
||||||
|
approvalQueueData, paging, err := _i.articleApprovalFlowsService.GetMyApprovalQueue(clientId, user.UserLevelId, paginate.Page, paginate.Limit, includePreview, urgentOnly)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"My approval queue successfully retrieved"},
|
||||||
|
Data: approvalQueueData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingApprovals ArticleApprovalFlows
|
||||||
|
// @Summary Get pending approvals
|
||||||
|
// @Description API for getting pending approvals
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param req query paginator.Pagination false "pagination parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/pending [get]
|
||||||
|
func (_i *articleApprovalFlowsController) GetPendingApprovals(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user level ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
filters := make(map[string]interface{})
|
||||||
|
pendingApprovalsData, paging, err := _i.articleApprovalFlowsService.GetPendingApprovals(clientId, user.UserLevelId, paginate.Page, paginate.Limit, filters)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Pending approvals successfully retrieved"},
|
||||||
|
Data: pendingApprovalsData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApprovalHistory ArticleApprovalFlows
|
||||||
|
// @Summary Get approval history
|
||||||
|
// @Description API for getting approval history
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param articleId query int false "Article ID filter"
|
||||||
|
// @Param userId query int false "User ID filter"
|
||||||
|
// @Param req query paginator.Pagination false "pagination parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/history [get]
|
||||||
|
func (_i *articleApprovalFlowsController) GetApprovalHistory(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
articleId := 0
|
||||||
|
if articleIdStr := c.Query("articleId"); articleIdStr != "" {
|
||||||
|
articleId, err = strconv.Atoi(articleIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid articleId format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// userId parameter is not used in the current implementation
|
||||||
|
// userId := 0
|
||||||
|
// if userIdStr := c.Query("userId"); userIdStr != "" {
|
||||||
|
// userId, err := strconv.Atoi(userIdStr)
|
||||||
|
// if err != nil {
|
||||||
|
// return utilRes.ErrorBadRequest(c, "Invalid userId format")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
approvalHistoryData, paging, err := _i.articleApprovalFlowsService.GetApprovalHistory(clientId, uint(articleId), paginate.Page, paginate.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Approval history successfully retrieved"},
|
||||||
|
Data: approvalHistoryData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardStats ArticleApprovalFlows
|
||||||
|
// @Summary Get dashboard statistics
|
||||||
|
// @Description API for getting dashboard statistics
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/dashboard-stats [get]
|
||||||
|
func (_i *articleApprovalFlowsController) GetDashboardStats(c *fiber.Ctx) error {
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Get Authorization token from header and extract user level ID
|
||||||
|
authToken := c.Get("Authorization")
|
||||||
|
if authToken == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Authorization token required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
|
if user == nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement GetDashboardStats method in service
|
||||||
|
_ = clientId // suppress unused variable warning
|
||||||
|
_ = user.UserLevelId // suppress unused variable warning
|
||||||
|
// dashboardStatsData, err := _i.articleApprovalFlowsService.GetDashboardStats(clientId, user.UserLevelId)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Dashboard statistics successfully retrieved"},
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkloadStats ArticleApprovalFlows
|
||||||
|
// @Summary Get workload statistics
|
||||||
|
// @Description API for getting workload statistics
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/workload-stats [get]
|
||||||
|
func (_i *articleApprovalFlowsController) GetWorkloadStats(c *fiber.Ctx) error {
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// TODO: Implement GetWorkloadStats method in service
|
||||||
|
_ = clientId // suppress unused variable warning
|
||||||
|
// workloadStatsData, err := _i.articleApprovalFlowsService.GetWorkloadStats(clientId)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Workload statistics successfully retrieved"},
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApprovalAnalytics ArticleApprovalFlows
|
||||||
|
// @Summary Get approval analytics
|
||||||
|
// @Description API for getting approval analytics
|
||||||
|
// @Tags ArticleApprovalFlows
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||||
|
// @Param period query string false "Period filter (daily, weekly, monthly)"
|
||||||
|
// @Param startDate query string false "Start date filter (YYYY-MM-DD)"
|
||||||
|
// @Param endDate query string false "End date filter (YYYY-MM-DD)"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-flows/analytics [get]
|
||||||
|
func (_i *articleApprovalFlowsController) GetApprovalAnalytics(c *fiber.Ctx) error {
|
||||||
|
// period := c.Query("period", "monthly")
|
||||||
|
// startDate := c.Query("startDate")
|
||||||
|
// endDate := c.Query("endDate")
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// TODO: Implement GetApprovalAnalytics method in service
|
||||||
|
_ = clientId // suppress unused variable warning
|
||||||
|
// analyticsData, err := _i.articleApprovalFlowsService.GetApprovalAnalytics(clientId, period, startDate, endDate)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Approval analytics successfully retrieved"},
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
articleApprovalStepLogsResponse "web-medols-be/app/module/article_approval_step_logs/response"
|
||||||
|
articleApprovalStepLogsRepository "web-medols-be/app/module/article_approval_step_logs/repository"
|
||||||
|
approvalWorkflowsRepository "web-medols-be/app/module/approval_workflows/repository"
|
||||||
|
approvalWorkflowsResponse "web-medols-be/app/module/approval_workflows/response"
|
||||||
|
articlesRepository "web-medols-be/app/module/articles/repository"
|
||||||
|
articlesResponse "web-medols-be/app/module/articles/response"
|
||||||
|
usersRepository "web-medols-be/app/module/users/repository"
|
||||||
|
res "web-medols-be/app/module/article_approval_flows/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ArticleApprovalFlowsResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
host string,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
articleApprovalFlowsReq *entity.ArticleApprovalFlows,
|
||||||
|
articlesRepo articlesRepository.ArticlesRepository,
|
||||||
|
approvalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository,
|
||||||
|
articleApprovalStepLogsRepo articleApprovalStepLogsRepository.ArticleApprovalStepLogsRepository,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (articleApprovalFlowsRes *res.ArticleApprovalFlowsResponse) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Get article information
|
||||||
|
var articleRes *articlesResponse.ArticlesResponse
|
||||||
|
if articleApprovalFlowsReq.ArticleId != 0 {
|
||||||
|
article, _ := articlesRepo.FindOne(clientId, articleApprovalFlowsReq.ArticleId)
|
||||||
|
if article != nil {
|
||||||
|
// Note: This would need additional repositories for full mapping
|
||||||
|
// For now, we'll create a basic response
|
||||||
|
articleRes = &articlesResponse.ArticlesResponse{
|
||||||
|
ID: article.ID,
|
||||||
|
Title: article.Title,
|
||||||
|
Slug: article.Slug,
|
||||||
|
Description: article.Description,
|
||||||
|
StatusId: article.StatusId,
|
||||||
|
IsActive: article.IsActive,
|
||||||
|
CreatedAt: article.CreatedAt,
|
||||||
|
UpdatedAt: article.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get approval workflow information
|
||||||
|
var approvalWorkflowRes *approvalWorkflowsResponse.ApprovalWorkflowsResponse
|
||||||
|
if articleApprovalFlowsReq.WorkflowId != 0 {
|
||||||
|
approvalWorkflow, _ := approvalWorkflowsRepo.FindOne(clientId, articleApprovalFlowsReq.WorkflowId)
|
||||||
|
if approvalWorkflow != nil {
|
||||||
|
// Note: This would need additional repositories for full mapping
|
||||||
|
// For now, we'll create a basic response
|
||||||
|
approvalWorkflowRes = &approvalWorkflowsResponse.ApprovalWorkflowsResponse{
|
||||||
|
ID: approvalWorkflow.ID,
|
||||||
|
Name: approvalWorkflow.Name,
|
||||||
|
Description: approvalWorkflow.Description,
|
||||||
|
IsActive: *approvalWorkflow.IsActive,
|
||||||
|
CreatedAt: approvalWorkflow.CreatedAt,
|
||||||
|
UpdatedAt: approvalWorkflow.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get step logs
|
||||||
|
var stepLogsArr []*articleApprovalStepLogsResponse.ArticleApprovalStepLogsResponse
|
||||||
|
|
||||||
|
if articleApprovalFlowsReq != nil {
|
||||||
|
// Map status ID to status string
|
||||||
|
statusStr := "pending"
|
||||||
|
switch articleApprovalFlowsReq.StatusId {
|
||||||
|
case 1:
|
||||||
|
statusStr = "pending"
|
||||||
|
case 2:
|
||||||
|
statusStr = "approved"
|
||||||
|
case 3:
|
||||||
|
statusStr = "rejected"
|
||||||
|
case 4:
|
||||||
|
statusStr = "revision_requested"
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalFlowsRes = &res.ArticleApprovalFlowsResponse{
|
||||||
|
ID: articleApprovalFlowsReq.ID,
|
||||||
|
ArticleID: articleApprovalFlowsReq.ArticleId,
|
||||||
|
WorkflowID: articleApprovalFlowsReq.WorkflowId,
|
||||||
|
CurrentStep: articleApprovalFlowsReq.CurrentStep,
|
||||||
|
Status: statusStr,
|
||||||
|
SubmittedBy: articleApprovalFlowsReq.SubmittedById,
|
||||||
|
SubmittedAt: articleApprovalFlowsReq.SubmittedAt,
|
||||||
|
CompletedAt: articleApprovalFlowsReq.CompletedAt,
|
||||||
|
RejectionReason: articleApprovalFlowsReq.RejectionReason,
|
||||||
|
RevisionReason: articleApprovalFlowsReq.RevisionMessage,
|
||||||
|
CreatedAt: articleApprovalFlowsReq.CreatedAt,
|
||||||
|
UpdatedAt: articleApprovalFlowsReq.UpdatedAt,
|
||||||
|
Article: articleRes,
|
||||||
|
Workflow: approvalWorkflowRes,
|
||||||
|
StepLogs: stepLogsArr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return articleApprovalFlowsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticleApprovalFlowsDetailResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
host string,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
articleApprovalFlowsReq *entity.ArticleApprovalFlows,
|
||||||
|
articlesRepo articlesRepository.ArticlesRepository,
|
||||||
|
approvalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository,
|
||||||
|
articleApprovalStepLogsRepo articleApprovalStepLogsRepository.ArticleApprovalStepLogsRepository,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (articleApprovalFlowsRes *res.ArticleApprovalFlowsDetailResponse) {
|
||||||
|
|
||||||
|
submittedByName := ""
|
||||||
|
if articleApprovalFlowsReq.SubmittedById != 0 {
|
||||||
|
findUser, _ := usersRepo.FindOne(clientId, articleApprovalFlowsReq.SubmittedById)
|
||||||
|
if findUser != nil {
|
||||||
|
submittedByName = findUser.Fullname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if articleApprovalFlowsReq.SubmittedById != 0 {
|
||||||
|
findUser, _ := usersRepo.FindOne(clientId, articleApprovalFlowsReq.SubmittedById)
|
||||||
|
if findUser != nil {
|
||||||
|
submittedByName = findUser.Fullname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get article information with full details
|
||||||
|
var articleRes *articlesResponse.ArticlesResponse
|
||||||
|
if articleApprovalFlowsReq.ArticleId != 0 {
|
||||||
|
article, _ := articlesRepo.FindOne(clientId, articleApprovalFlowsReq.ArticleId)
|
||||||
|
if article != nil {
|
||||||
|
// Note: This would need additional repositories for full mapping
|
||||||
|
// For now, we'll create a basic response
|
||||||
|
articleRes = &articlesResponse.ArticlesResponse{
|
||||||
|
ID: article.ID,
|
||||||
|
Title: article.Title,
|
||||||
|
Slug: article.Slug,
|
||||||
|
Description: article.Description,
|
||||||
|
StatusId: article.StatusId,
|
||||||
|
IsActive: article.IsActive,
|
||||||
|
CreatedAt: article.CreatedAt,
|
||||||
|
UpdatedAt: article.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get approval workflow information with full details
|
||||||
|
var approvalWorkflowRes *approvalWorkflowsResponse.ApprovalWorkflowsWithStepsResponse
|
||||||
|
if articleApprovalFlowsReq.WorkflowId != 0 {
|
||||||
|
approvalWorkflow, _ := approvalWorkflowsRepo.FindOne(clientId, articleApprovalFlowsReq.WorkflowId)
|
||||||
|
if approvalWorkflow != nil {
|
||||||
|
// Note: This would need additional repositories for full mapping
|
||||||
|
// For now, we'll create a basic response
|
||||||
|
approvalWorkflowRes = &approvalWorkflowsResponse.ApprovalWorkflowsWithStepsResponse{
|
||||||
|
ID: approvalWorkflow.ID,
|
||||||
|
Name: approvalWorkflow.Name,
|
||||||
|
Description: approvalWorkflow.Description,
|
||||||
|
IsActive: *approvalWorkflow.IsActive,
|
||||||
|
CreatedAt: approvalWorkflow.CreatedAt,
|
||||||
|
UpdatedAt: approvalWorkflow.UpdatedAt,
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get step logs with detailed information
|
||||||
|
var stepLogsArr []*articleApprovalStepLogsResponse.ArticleApprovalStepLogsResponse
|
||||||
|
|
||||||
|
if articleApprovalFlowsReq != nil {
|
||||||
|
// Map status ID to status string
|
||||||
|
statusStr := "pending"
|
||||||
|
switch articleApprovalFlowsReq.StatusId {
|
||||||
|
case 1:
|
||||||
|
statusStr = "pending"
|
||||||
|
case 2:
|
||||||
|
statusStr = "approved"
|
||||||
|
case 3:
|
||||||
|
statusStr = "rejected"
|
||||||
|
case 4:
|
||||||
|
statusStr = "revision_requested"
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalFlowsRes = &res.ArticleApprovalFlowsDetailResponse{
|
||||||
|
ID: articleApprovalFlowsReq.ID,
|
||||||
|
ArticleID: articleApprovalFlowsReq.ArticleId,
|
||||||
|
WorkflowID: articleApprovalFlowsReq.WorkflowId,
|
||||||
|
CurrentStep: articleApprovalFlowsReq.CurrentStep,
|
||||||
|
Status: statusStr,
|
||||||
|
SubmittedBy: articleApprovalFlowsReq.SubmittedById,
|
||||||
|
SubmittedByName: submittedByName,
|
||||||
|
SubmittedAt: articleApprovalFlowsReq.SubmittedAt,
|
||||||
|
CompletedAt: articleApprovalFlowsReq.CompletedAt,
|
||||||
|
RejectionReason: articleApprovalFlowsReq.RejectionReason,
|
||||||
|
RevisionReason: articleApprovalFlowsReq.RevisionMessage,
|
||||||
|
CreatedAt: articleApprovalFlowsReq.CreatedAt,
|
||||||
|
UpdatedAt: articleApprovalFlowsReq.UpdatedAt,
|
||||||
|
Article: articleRes,
|
||||||
|
Workflow: approvalWorkflowRes,
|
||||||
|
StepLogs: stepLogsArr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return articleApprovalFlowsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticleApprovalFlowsSummaryResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
articleApprovalFlowsReq *entity.ArticleApprovalFlows,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (articleApprovalFlowsRes *res.ArticleApprovalFlowsSummaryResponse) {
|
||||||
|
|
||||||
|
submittedByName := ""
|
||||||
|
if articleApprovalFlowsReq.SubmittedById != 0 {
|
||||||
|
findUser, _ := usersRepo.FindOne(clientId, articleApprovalFlowsReq.SubmittedById)
|
||||||
|
if findUser != nil {
|
||||||
|
submittedByName = findUser.Fullname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if articleApprovalFlowsReq != nil {
|
||||||
|
// Map status ID to status string
|
||||||
|
statusStr := "pending"
|
||||||
|
switch articleApprovalFlowsReq.StatusId {
|
||||||
|
case 1:
|
||||||
|
statusStr = "pending"
|
||||||
|
case 2:
|
||||||
|
statusStr = "approved"
|
||||||
|
case 3:
|
||||||
|
statusStr = "rejected"
|
||||||
|
case 4:
|
||||||
|
statusStr = "revision_requested"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate days in approval
|
||||||
|
daysInApproval := int(time.Since(articleApprovalFlowsReq.SubmittedAt).Hours() / 24)
|
||||||
|
|
||||||
|
articleApprovalFlowsRes = &res.ArticleApprovalFlowsSummaryResponse{
|
||||||
|
ID: articleApprovalFlowsReq.ID,
|
||||||
|
ArticleID: articleApprovalFlowsReq.ArticleId,
|
||||||
|
CurrentStep: articleApprovalFlowsReq.CurrentStep,
|
||||||
|
Status: statusStr,
|
||||||
|
SubmittedBy: articleApprovalFlowsReq.SubmittedById,
|
||||||
|
SubmittedByName: submittedByName,
|
||||||
|
SubmittedAt: articleApprovalFlowsReq.SubmittedAt,
|
||||||
|
DaysInApproval: daysInApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return articleApprovalFlowsRes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,420 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/request"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type articleApprovalFlowsRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArticleApprovalFlowsRepository define interface of IArticleApprovalFlowsRepository
|
||||||
|
type ArticleApprovalFlowsRepository interface {
|
||||||
|
// Basic CRUD
|
||||||
|
GetAll(clientId *uuid.UUID, req request.ArticleApprovalFlowsQueryRequest) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (flow *entity.ArticleApprovalFlows, err error)
|
||||||
|
FindActiveByArticleId(articleId uint) (flow *entity.ArticleApprovalFlows, err error)
|
||||||
|
FindByArticleId(clientId *uuid.UUID, articleId uint) (flow *entity.ArticleApprovalFlows, err error)
|
||||||
|
Create(clientId *uuid.UUID, flow *entity.ArticleApprovalFlows) (flowReturn *entity.ArticleApprovalFlows, err error)
|
||||||
|
Update(id uint, flow *entity.ArticleApprovalFlows) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Approval Queue Methods
|
||||||
|
GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, page, limit int, filters map[string]interface{}) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error)
|
||||||
|
GetPendingApprovalsByUserLevel(clientId *uuid.UUID, userLevelId uint, page, limit int) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error)
|
||||||
|
GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, page, limit int, includePreview bool, urgentOnly bool) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error)
|
||||||
|
|
||||||
|
// Statistics Methods
|
||||||
|
GetPendingCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error)
|
||||||
|
GetOverdueCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error)
|
||||||
|
GetApprovedCountByPeriod(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (count int64, err error)
|
||||||
|
GetRejectedCountByPeriod(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (count int64, err error)
|
||||||
|
GetRevisionRequestCountByPeriod(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (count int64, err error)
|
||||||
|
|
||||||
|
// Workload Methods
|
||||||
|
GetLevelWorkload(clientId *uuid.UUID, userLevelId uint) (workload map[string]interface{}, err error)
|
||||||
|
GetTeamWorkloadComparison(clientId *uuid.UUID, userLevelId uint) (comparison []map[string]interface{}, err error)
|
||||||
|
GetWorkflowBottlenecks(clientId *uuid.UUID) (bottlenecks []map[string]interface{}, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArticleApprovalFlowsRepository(db *database.Database, log zerolog.Logger) ArticleApprovalFlowsRepository {
|
||||||
|
return &articleApprovalFlowsRepository{
|
||||||
|
DB: db,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic CRUD implementations
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetAll(clientId *uuid.UUID, req request.ArticleApprovalFlowsQueryRequest) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters from request
|
||||||
|
if req.ArticleId != nil && *req.ArticleId > 0 {
|
||||||
|
query = query.Where("article_id = ?", *req.ArticleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.StatusId != nil && *req.StatusId > 0 {
|
||||||
|
query = query.Where("status_id = ?", *req.StatusId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SubmittedBy != nil && *req.SubmittedBy > 0 {
|
||||||
|
query = query.Where("submitted_by_id = ?", *req.SubmittedBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.CurrentStep != nil && *req.CurrentStep > 0 {
|
||||||
|
query = query.Where("current_step = ?", *req.CurrentStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DateFrom != nil {
|
||||||
|
query = query.Where("submitted_at >= ?", *req.DateFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.WorkflowId != nil {
|
||||||
|
query = query.Where("workflow_id = ?", *req.WorkflowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DateTo != nil {
|
||||||
|
query = query.Where("submitted_at <= ?", *req.DateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preload related data
|
||||||
|
query = query.Preload("Article").Preload("Workflow").Preload("SubmittedBy").Preload("StepLogs")
|
||||||
|
query = query.Order("submitted_at DESC")
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
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(&flows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Count: int64(count),
|
||||||
|
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return flows, paging, nil
|
||||||
|
}
|
||||||
|
func (_i *articleApprovalFlowsRepository) FindOne(clientId *uuid.UUID, id uint) (flow *entity.ArticleApprovalFlows, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("Article").Preload("Workflow").Preload("SubmittedBy").Preload("StepLogs")
|
||||||
|
|
||||||
|
err = query.First(&flow, id).Error
|
||||||
|
return flow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) FindActiveByArticleId(articleId uint) (flow *entity.ArticleApprovalFlows, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
query = query.Where("article_id = ? AND status_id IN (1, 4)", articleId) // pending or revision_requested
|
||||||
|
query = query.Preload("Article").Preload("Workflow").Preload("SubmittedBy").Preload("StepLogs")
|
||||||
|
|
||||||
|
err = query.First(&flow).Error
|
||||||
|
return flow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) Create(clientId *uuid.UUID, flow *entity.ArticleApprovalFlows) (flowReturn *entity.ArticleApprovalFlows, err error) {
|
||||||
|
flow.ClientId = clientId
|
||||||
|
err = _i.DB.DB.Create(&flow).Error
|
||||||
|
return flow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) Update(id uint, flow *entity.ArticleApprovalFlows) (err error) {
|
||||||
|
err = _i.DB.DB.Model(&entity.ArticleApprovalFlows{}).Where("id = ?", id).Updates(flow).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.Delete(&entity.ArticleApprovalFlows{}, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approval Queue Methods
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, page, limit int, filters map[string]interface{}) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_flows.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join with workflow steps to find current step requirements
|
||||||
|
query = query.Joins("JOIN approval_workflows ON article_approval_flows.workflow_id = approval_workflows.id")
|
||||||
|
query = query.Joins("JOIN approval_workflow_steps ON approval_workflows.id = approval_workflow_steps.workflow_id AND article_approval_flows.current_step = approval_workflow_steps.step_order")
|
||||||
|
query = query.Where("approval_workflow_steps.required_user_level_id = ?", userLevelId)
|
||||||
|
query = query.Where("article_approval_flows.status_id IN (1, 4)") // pending or revision_requested
|
||||||
|
query = query.Where("article_approval_flows.current_step > 0") // exclude completed flows (current_step = 0)
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
_i.Log.Info().
|
||||||
|
Interface("clientId", clientId).
|
||||||
|
Uint("userLevelId", userLevelId).
|
||||||
|
Msg("Getting pending approvals for user level")
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if categoryId, ok := filters["category_id"]; ok {
|
||||||
|
query = query.Joins("JOIN articles ON article_approval_flows.article_id = articles.id")
|
||||||
|
query = query.Where("articles.category_id = ?", categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeId, ok := filters["type_id"]; ok {
|
||||||
|
query = query.Joins("JOIN articles ON article_approval_flows.article_id = articles.id")
|
||||||
|
query = query.Where("articles.type_id = ?", typeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search, ok := filters["search"]; ok {
|
||||||
|
query = query.Joins("JOIN articles ON article_approval_flows.article_id = articles.id")
|
||||||
|
query = query.Where("articles.title ILIKE ? OR articles.description ILIKE ?", fmt.Sprintf("%%%s%%", search), fmt.Sprintf("%%%s%%", search))
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("Article").Preload("Workflow").Preload("SubmittedBy").Preload("StepLogs")
|
||||||
|
query = query.Order("article_approval_flows.submitted_at ASC")
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
_i.Log.Info().
|
||||||
|
Int64("count", count).
|
||||||
|
Msg("Found pending approvals count")
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
err = query.Offset(offset).Limit(limit).Find(&flows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
_i.Log.Info().
|
||||||
|
Int("flowsCount", len(flows)).
|
||||||
|
Msg("Retrieved flows from database")
|
||||||
|
|
||||||
|
paging = paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Count: int64(count),
|
||||||
|
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return flows, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, page, limit int, includePreview bool, urgentOnly bool) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) {
|
||||||
|
filters := make(map[string]interface{})
|
||||||
|
if urgentOnly {
|
||||||
|
// Add logic for urgent articles (e.g., submitted more than 24 hours ago)
|
||||||
|
filters["urgent"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.GetPendingApprovals(clientId, userLevelId, page, limit, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics Methods
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetPendingCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_flows.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Joins("JOIN approval_workflows ON article_approval_flows.workflow_id = approval_workflows.id")
|
||||||
|
query = query.Joins("JOIN approval_workflow_steps ON approval_workflows.id = approval_workflow_steps.workflow_id AND article_approval_flows.current_step = approval_workflow_steps.step_order")
|
||||||
|
query = query.Where("approval_workflow_steps.required_user_level_id = ?", userLevelId)
|
||||||
|
query = query.Where("article_approval_flows.status_id IN (1, 4)") // pending or revision_requested
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetOverdueCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_flows.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Joins("JOIN approval_workflows ON article_approval_flows.workflow_id = approval_workflows.id")
|
||||||
|
query = query.Joins("JOIN approval_workflow_steps ON approval_workflows.id = approval_workflow_steps.workflow_id AND article_approval_flows.current_step = approval_workflow_steps.step_order")
|
||||||
|
query = query.Where("approval_workflow_steps.required_user_level_id = ?", userLevelId)
|
||||||
|
query = query.Where("article_approval_flows.status_id IN (1, 4)") // pending or revision_requested
|
||||||
|
query = query.Where("article_approval_flows.submitted_at < ?", time.Now().Add(-24*time.Hour)) // overdue after 24 hours
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetApprovedCountByPeriod(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (count int64, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_step_logs.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("user_level_id = ? AND action = 'approve' AND processed_at BETWEEN ? AND ?", userLevelId, startDate, endDate)
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetRejectedCountByPeriod(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (count int64, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_step_logs.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("user_level_id = ? AND action = 'reject' AND processed_at BETWEEN ? AND ?", userLevelId, startDate, endDate)
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetRevisionRequestCountByPeriod(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (count int64, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_step_logs.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("user_level_id = ? AND action = 'request_revision' AND processed_at BETWEEN ? AND ?", userLevelId, startDate, endDate)
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workload Methods (simplified implementations)
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetLevelWorkload(clientId *uuid.UUID, userLevelId uint) (workload map[string]interface{}, err error) {
|
||||||
|
workload = make(map[string]interface{})
|
||||||
|
|
||||||
|
// Get pending count
|
||||||
|
pendingCount, err := _i.GetPendingCountByLevel(clientId, userLevelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
workload["pending_count"] = pendingCount
|
||||||
|
workload["level_id"] = userLevelId
|
||||||
|
|
||||||
|
return workload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetTeamWorkloadComparison(clientId *uuid.UUID, userLevelId uint) (comparison []map[string]interface{}, err error) {
|
||||||
|
// This would require more complex queries to get team members at the same level
|
||||||
|
// For now, return empty slice
|
||||||
|
comparison = make([]map[string]interface{}, 0)
|
||||||
|
return comparison, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetWorkflowBottlenecks(clientId *uuid.UUID) (bottlenecks []map[string]interface{}, err error) {
|
||||||
|
// This would require complex analysis of workflow steps
|
||||||
|
// For now, return empty slice
|
||||||
|
bottlenecks = make([]map[string]interface{}, 0)
|
||||||
|
return bottlenecks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByArticleId finds an approval flow by article ID for a specific client
|
||||||
|
func (_i *articleApprovalFlowsRepository) FindByArticleId(clientId *uuid.UUID, articleId uint) (flow *entity.ArticleApprovalFlows, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("article_id = ?", articleId)
|
||||||
|
query = query.Where("status_id IN (1, 2, 3, 4)") // active flows (pending, approved, rejected, revision_requested)
|
||||||
|
query = query.Order("created_at DESC")
|
||||||
|
|
||||||
|
err = query.First(&flow).Error
|
||||||
|
return flow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingApprovalsByUserLevel gets pending approvals for a specific user level with pagination
|
||||||
|
func (_i *articleApprovalFlowsRepository) GetPendingApprovalsByUserLevel(clientId *uuid.UUID, userLevelId uint, page, limit int) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalFlows{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_flows.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join with workflow steps to get approvals for this user level
|
||||||
|
query = query.Joins("JOIN approval_workflows ON article_approval_flows.workflow_id = approval_workflows.id")
|
||||||
|
query = query.Joins("JOIN approval_workflow_steps ON approval_workflows.id = approval_workflow_steps.workflow_id AND article_approval_flows.current_step = approval_workflow_steps.step_order")
|
||||||
|
query = query.Where("approval_workflow_steps.required_user_level_id = ?", userLevelId)
|
||||||
|
query = query.Where("article_approval_flows.status_id IN (1, 4)") // pending or revision_requested
|
||||||
|
query = query.Where("article_approval_flows.current_step > 0") // exclude completed flows (current_step = 0)
|
||||||
|
|
||||||
|
// Preload related data
|
||||||
|
query = query.Preload("Article").Preload("Workflow")
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
query = query.Offset(offset).Limit(limit)
|
||||||
|
query = query.Order("article_approval_flows.created_at ASC") // oldest first
|
||||||
|
|
||||||
|
err = query.Find(&flows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pagination response
|
||||||
|
paging = paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Count: count,
|
||||||
|
Offset: offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
return flows, paging, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleApprovalFlowsGeneric interface {
|
||||||
|
ToEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalFlowsQueryRequest struct {
|
||||||
|
ArticleId *int `json:"articleId"`
|
||||||
|
WorkflowId *int `json:"workflowId"`
|
||||||
|
StatusId *int `json:"statusId"`
|
||||||
|
SubmittedBy *int `json:"submittedBy"`
|
||||||
|
CurrentStep *int `json:"currentStep"`
|
||||||
|
DateFrom *time.Time `json:"dateFrom"`
|
||||||
|
DateTo *time.Time `json:"dateTo"`
|
||||||
|
Pagination *paginator.Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitForApprovalRequest struct {
|
||||||
|
ArticleId int `json:"articleId" validate:"required"`
|
||||||
|
WorkflowId *int `json:"workflowId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalActionRequest struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RejectionRequest struct {
|
||||||
|
Reason string `json:"reason" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RevisionRequest struct {
|
||||||
|
Message string `json:"message" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResubmitRequest struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalFlowsQueryRequestContext struct {
|
||||||
|
ArticleId string `json:"articleId"`
|
||||||
|
WorkflowId string `json:"workflowId"`
|
||||||
|
StatusId string `json:"statusId"`
|
||||||
|
SubmittedBy string `json:"submittedBy"`
|
||||||
|
CurrentStep string `json:"currentStep"`
|
||||||
|
DateFrom string `json:"dateFrom"`
|
||||||
|
DateTo string `json:"dateTo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ArticleApprovalFlowsQueryRequestContext) ToParamRequest() ArticleApprovalFlowsQueryRequest {
|
||||||
|
var articleId *int
|
||||||
|
var workflowId *int
|
||||||
|
var statusId *int
|
||||||
|
var submittedBy *int
|
||||||
|
var currentStep *int
|
||||||
|
var dateFrom *time.Time
|
||||||
|
var dateTo *time.Time
|
||||||
|
|
||||||
|
if req.ArticleId != "" {
|
||||||
|
if parsedArticleId, err := strconv.Atoi(req.ArticleId); err == nil {
|
||||||
|
articleId = &parsedArticleId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.WorkflowId != "" {
|
||||||
|
if parsedWorkflowId, err := strconv.Atoi(req.WorkflowId); err == nil {
|
||||||
|
workflowId = &parsedWorkflowId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.StatusId != "" {
|
||||||
|
if parsedStatusId, err := strconv.Atoi(req.StatusId); err == nil {
|
||||||
|
statusId = &parsedStatusId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SubmittedBy != "" {
|
||||||
|
if parsedSubmittedBy, err := strconv.Atoi(req.SubmittedBy); err == nil {
|
||||||
|
submittedBy = &parsedSubmittedBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.CurrentStep != "" {
|
||||||
|
if parsedCurrentStep, err := strconv.Atoi(req.CurrentStep); err == nil {
|
||||||
|
currentStep = &parsedCurrentStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DateFrom != "" {
|
||||||
|
if parsedDateFrom, err := time.Parse("2006-01-02", req.DateFrom); err == nil {
|
||||||
|
dateFrom = &parsedDateFrom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DateTo != "" {
|
||||||
|
if parsedDateTo, err := time.Parse("2006-01-02", req.DateTo); err == nil {
|
||||||
|
dateTo = &parsedDateTo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ArticleApprovalFlowsQueryRequest{
|
||||||
|
ArticleId: articleId,
|
||||||
|
WorkflowId: workflowId,
|
||||||
|
StatusId: statusId,
|
||||||
|
SubmittedBy: submittedBy,
|
||||||
|
CurrentStep: currentStep,
|
||||||
|
DateFrom: dateFrom,
|
||||||
|
DateTo: dateTo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
articlesResponse "web-medols-be/app/module/articles/response"
|
||||||
|
approvalWorkflowsResponse "web-medols-be/app/module/approval_workflows/response"
|
||||||
|
articleApprovalStepLogsResponse "web-medols-be/app/module/article_approval_step_logs/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleApprovalFlowsResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ArticleID uint `json:"articleId"`
|
||||||
|
WorkflowID uint `json:"workflowId"`
|
||||||
|
CurrentStep int `json:"currentStep"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SubmittedBy uint `json:"submittedBy"`
|
||||||
|
SubmittedAt time.Time `json:"submittedAt"`
|
||||||
|
CompletedAt *time.Time `json:"completedAt"`
|
||||||
|
RejectedAt *time.Time `json:"rejectedAt"`
|
||||||
|
RejectionReason *string `json:"rejectionReason"`
|
||||||
|
RevisionRequestedAt *time.Time `json:"revisionRequestedAt"`
|
||||||
|
RevisionReason *string `json:"revisionReason"`
|
||||||
|
ResubmittedAt *time.Time `json:"resubmittedAt"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Article *articlesResponse.ArticlesResponse `json:"article,omitempty"`
|
||||||
|
Workflow *approvalWorkflowsResponse.ApprovalWorkflowsResponse `json:"workflow,omitempty"`
|
||||||
|
StepLogs []*articleApprovalStepLogsResponse.ArticleApprovalStepLogsResponse `json:"stepLogs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalFlowsDetailResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ArticleID uint `json:"articleId"`
|
||||||
|
WorkflowID uint `json:"workflowId"`
|
||||||
|
CurrentStep int `json:"currentStep"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SubmittedBy uint `json:"submittedBy"`
|
||||||
|
SubmittedByName string `json:"submittedByName"`
|
||||||
|
SubmittedAt time.Time `json:"submittedAt"`
|
||||||
|
CompletedAt *time.Time `json:"completedAt"`
|
||||||
|
RejectedAt *time.Time `json:"rejectedAt"`
|
||||||
|
RejectionReason *string `json:"rejectionReason"`
|
||||||
|
RevisionRequestedAt *time.Time `json:"revisionRequestedAt"`
|
||||||
|
RevisionReason *string `json:"revisionReason"`
|
||||||
|
ResubmittedAt *time.Time `json:"resubmittedAt"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
|
// Relations with full details
|
||||||
|
Article *articlesResponse.ArticlesResponse `json:"article"`
|
||||||
|
Workflow *approvalWorkflowsResponse.ApprovalWorkflowsWithStepsResponse `json:"workflow"`
|
||||||
|
StepLogs []*articleApprovalStepLogsResponse.ArticleApprovalStepLogsResponse `json:"stepLogs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalFlowsSummaryResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ArticleID uint `json:"articleId"`
|
||||||
|
ArticleTitle string `json:"articleTitle"`
|
||||||
|
WorkflowName string `json:"workflowName"`
|
||||||
|
CurrentStep int `json:"currentStep"`
|
||||||
|
TotalSteps int `json:"totalSteps"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SubmittedBy uint `json:"submittedBy"`
|
||||||
|
SubmittedByName string `json:"submittedByName"`
|
||||||
|
SubmittedAt time.Time `json:"submittedAt"`
|
||||||
|
DaysInApproval int `json:"daysInApproval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalQueueResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ArticleID uint `json:"articleId"`
|
||||||
|
ArticleTitle string `json:"articleTitle"`
|
||||||
|
ArticleAuthor string `json:"articleAuthor"`
|
||||||
|
WorkflowName string `json:"workflowName"`
|
||||||
|
CurrentStepName string `json:"currentStepName"`
|
||||||
|
SubmittedAt time.Time `json:"submittedAt"`
|
||||||
|
DaysWaiting int `json:"daysWaiting"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
RequiresComment bool `json:"requiresComment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalDashboardStatsResponse struct {
|
||||||
|
PendingApprovals int `json:"pendingApprovals"`
|
||||||
|
MyPendingApprovals int `json:"myPendingApprovals"`
|
||||||
|
ApprovedToday int `json:"approvedToday"`
|
||||||
|
RejectedToday int `json:"rejectedToday"`
|
||||||
|
RevisionRequested int `json:"revisionRequested"`
|
||||||
|
OverdueApprovals int `json:"overdueApprovals"`
|
||||||
|
AverageApprovalTime int `json:"averageApprovalTime"` // in hours
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalWorkloadStatsResponse struct {
|
||||||
|
ApproverID uint `json:"approverId"`
|
||||||
|
ApproverName string `json:"approverName"`
|
||||||
|
PendingCount int `json:"pendingCount"`
|
||||||
|
ApprovedThisWeek int `json:"approvedThisWeek"`
|
||||||
|
RejectedThisWeek int `json:"rejectedThisWeek"`
|
||||||
|
AverageResponseTime int `json:"averageResponseTime"` // in hours
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalAnalyticsResponse struct {
|
||||||
|
Period string `json:"period"`
|
||||||
|
TotalSubmissions int `json:"totalSubmissions"`
|
||||||
|
TotalApproved int `json:"totalApproved"`
|
||||||
|
TotalRejected int `json:"totalRejected"`
|
||||||
|
TotalRevisions int `json:"totalRevisions"`
|
||||||
|
ApprovalRate float64 `json:"approvalRate"`
|
||||||
|
AverageApprovalTime float64 `json:"averageApprovalTime"` // in hours
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,788 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
approvalWorkflowStepsRepo "web-medols-be/app/module/approval_workflow_steps/repository"
|
||||||
|
approvalWorkflowsRepo "web-medols-be/app/module/approval_workflows/repository"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/repository"
|
||||||
|
"web-medols-be/app/module/article_approval_flows/request"
|
||||||
|
approvalStepLogsRepo "web-medols-be/app/module/article_approval_step_logs/repository"
|
||||||
|
articlesRepo "web-medols-be/app/module/articles/repository"
|
||||||
|
usersRepo "web-medols-be/app/module/users/repository"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type articleApprovalFlowsService struct {
|
||||||
|
ArticleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository
|
||||||
|
ApprovalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository
|
||||||
|
ApprovalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository
|
||||||
|
ArticleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository
|
||||||
|
ArticlesRepository articlesRepo.ArticlesRepository
|
||||||
|
UsersRepository usersRepo.UsersRepository
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArticleApprovalFlowsService define interface of IArticleApprovalFlowsService
|
||||||
|
type ArticleApprovalFlowsService interface {
|
||||||
|
// Basic CRUD
|
||||||
|
GetAll(clientId *uuid.UUID, req request.ArticleApprovalFlowsQueryRequest) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (flow *entity.ArticleApprovalFlows, err error)
|
||||||
|
Create(clientId *uuid.UUID, flow *entity.ArticleApprovalFlows) (flowReturn *entity.ArticleApprovalFlows, err error)
|
||||||
|
Update(id uint, flow *entity.ArticleApprovalFlows) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Article submission and approval workflow
|
||||||
|
SubmitArticleForApproval(clientId *uuid.UUID, articleId uint, submittedById uint, workflowId *uint) (flow *entity.ArticleApprovalFlows, err error)
|
||||||
|
ApproveStep(clientId *uuid.UUID, flowId uint, approvedById uint, message string) (err error)
|
||||||
|
RejectArticle(clientId *uuid.UUID, flowId uint, rejectedById uint, reason string) (err error)
|
||||||
|
RequestRevision(clientId *uuid.UUID, flowId uint, requestedById uint, revisionMessage string) (err error)
|
||||||
|
ResubmitAfterRevision(clientId *uuid.UUID, flowId uint, resubmittedById uint) (err error)
|
||||||
|
|
||||||
|
// Dashboard and queue methods
|
||||||
|
GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, page, limit int, filters map[string]interface{}) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error)
|
||||||
|
GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, page, limit int, includePreview bool, urgentOnly bool) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error)
|
||||||
|
GetApprovalHistory(clientId *uuid.UUID, articleId uint, page, limit int) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error)
|
||||||
|
|
||||||
|
// Statistics and analytics
|
||||||
|
GetPendingCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error)
|
||||||
|
GetOverdueCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error)
|
||||||
|
GetApprovalStatistics(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (stats map[string]interface{}, err error)
|
||||||
|
GetWorkloadAnalytics(clientId *uuid.UUID, userLevelId uint) (analytics map[string]interface{}, err error)
|
||||||
|
|
||||||
|
// Workflow management
|
||||||
|
CanUserApproveStep(clientId *uuid.UUID, flowId uint, userId uint, userLevelId uint) (canApprove bool, reason string, err error)
|
||||||
|
GetCurrentStepInfo(clientId *uuid.UUID, flowId uint) (stepInfo map[string]interface{}, err error)
|
||||||
|
GetNextStepPreview(clientId *uuid.UUID, flowId uint) (nextStep *entity.ApprovalWorkflowSteps, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArticleApprovalFlowsService(
|
||||||
|
articleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository,
|
||||||
|
approvalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository,
|
||||||
|
approvalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository,
|
||||||
|
articleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository,
|
||||||
|
articlesRepository articlesRepo.ArticlesRepository,
|
||||||
|
usersRepository usersRepo.UsersRepository,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) ArticleApprovalFlowsService {
|
||||||
|
return &articleApprovalFlowsService{
|
||||||
|
ArticleApprovalFlowsRepository: articleApprovalFlowsRepository,
|
||||||
|
ApprovalWorkflowsRepository: approvalWorkflowsRepository,
|
||||||
|
ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository,
|
||||||
|
ArticleApprovalStepLogsRepository: articleApprovalStepLogsRepository,
|
||||||
|
ArticlesRepository: articlesRepository,
|
||||||
|
UsersRepository: usersRepository,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic CRUD implementations
|
||||||
|
func (_i *articleApprovalFlowsService) GetAll(clientId *uuid.UUID, req request.ArticleApprovalFlowsQueryRequest) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.GetAll(clientId, req)
|
||||||
|
}
|
||||||
|
func (_i *articleApprovalFlowsService) FindOne(clientId *uuid.UUID, id uint) (flow *entity.ArticleApprovalFlows, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.FindOne(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) Create(clientId *uuid.UUID, flow *entity.ArticleApprovalFlows) (flowReturn *entity.ArticleApprovalFlows, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.Create(clientId, flow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) Update(id uint, flow *entity.ArticleApprovalFlows) (err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.Update(id, flow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.Delete(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Article submission and approval workflow
|
||||||
|
func (_i *articleApprovalFlowsService) SubmitArticleForApproval(clientId *uuid.UUID, articleId uint, submittedById uint, workflowId *uint) (flow *entity.ArticleApprovalFlows, err error) {
|
||||||
|
// Check if article already has an active approval flow
|
||||||
|
existingFlow, err := _i.ArticleApprovalFlowsRepository.FindActiveByArticleId(articleId)
|
||||||
|
if err == nil && existingFlow != nil {
|
||||||
|
return nil, errors.New("article already has an active approval flow")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get workflow (use default if not specified)
|
||||||
|
var workflow *entity.ApprovalWorkflows
|
||||||
|
if workflowId != nil {
|
||||||
|
workflow, err = _i.ApprovalWorkflowsRepository.FindOne(clientId, *workflowId)
|
||||||
|
} else {
|
||||||
|
workflow, err = _i.ApprovalWorkflowsRepository.FindDefault(clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if workflow == nil {
|
||||||
|
return nil, errors.New("no workflow found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if workflow.IsActive != nil && !*workflow.IsActive {
|
||||||
|
return nil, errors.New("workflow is not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get first step of workflow
|
||||||
|
firstStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, workflow.ID, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstStep == nil {
|
||||||
|
return nil, errors.New("workflow has no steps")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create approval flow
|
||||||
|
flow = &entity.ArticleApprovalFlows{
|
||||||
|
ArticleId: articleId,
|
||||||
|
WorkflowId: workflow.ID,
|
||||||
|
CurrentStep: 1,
|
||||||
|
StatusId: 1, // pending
|
||||||
|
SubmittedById: submittedById,
|
||||||
|
SubmittedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
flow, err = _i.ArticleApprovalFlowsRepository.Create(clientId, flow)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, articleId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.WorkflowId = &workflow.ID
|
||||||
|
currentArticle.CurrentApprovalStep = &flow.CurrentStep
|
||||||
|
currentArticle.StatusId = &[]int{1}[0] // pending approval
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, articleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process auto-skip logic based on user level
|
||||||
|
err = _i.processAutoSkipSteps(clientId, flow, submittedById)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return flow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAutoSkipSteps handles automatic step skipping based on user level
|
||||||
|
func (_i *articleApprovalFlowsService) processAutoSkipSteps(clientId *uuid.UUID, flow *entity.ArticleApprovalFlows, submittedById uint) error {
|
||||||
|
// Get user level of the submitter
|
||||||
|
userLevelId, err := _i.getUserLevelId(clientId, submittedById)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all workflow steps
|
||||||
|
steps, err := _i.ApprovalWorkflowStepsRepository.GetByWorkflowId(clientId, flow.WorkflowId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort steps by step order
|
||||||
|
sortStepsByOrder(steps)
|
||||||
|
|
||||||
|
// Process each step to determine if it should be auto-skipped
|
||||||
|
for _, step := range steps {
|
||||||
|
shouldSkip := _i.shouldSkipStep(userLevelId, step.RequiredUserLevelId)
|
||||||
|
|
||||||
|
if shouldSkip {
|
||||||
|
// Create skip log
|
||||||
|
stepLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovalFlowId: flow.ID,
|
||||||
|
StepOrder: step.StepOrder,
|
||||||
|
StepName: step.StepName,
|
||||||
|
ApprovedById: &submittedById,
|
||||||
|
Action: "auto_skip",
|
||||||
|
Message: &[]string{"Step auto-skipped due to user level"}[0],
|
||||||
|
ProcessedAt: time.Now(),
|
||||||
|
UserLevelId: step.RequiredUserLevelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update flow to next step (handle step order starting from 0)
|
||||||
|
nextStepOrder := step.StepOrder + 1
|
||||||
|
flow.CurrentStep = nextStepOrder
|
||||||
|
} else {
|
||||||
|
// Stop at first step that cannot be skipped
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update flow with final current step
|
||||||
|
err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.CurrentApprovalStep = &flow.CurrentStep
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all steps were skipped (workflow complete)
|
||||||
|
// Find the highest step order
|
||||||
|
maxStepOrder := 0
|
||||||
|
for _, step := range steps {
|
||||||
|
if step.StepOrder > maxStepOrder {
|
||||||
|
maxStepOrder = step.StepOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow.CurrentStep > maxStepOrder {
|
||||||
|
// All steps completed, mark as approved
|
||||||
|
flow.StatusId = 2 // approved
|
||||||
|
flow.CurrentStep = 0 // Set to 0 to indicate completion
|
||||||
|
flow.CompletedAt = &[]time.Time{time.Now()}[0]
|
||||||
|
|
||||||
|
err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.StatusId = &[]int{2}[0] // approved
|
||||||
|
currentArticle.CurrentApprovalStep = &[]int{0}[0] // Set to 0 to indicate completion
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserLevelId gets the user level ID for a given user
|
||||||
|
func (_i *articleApprovalFlowsService) getUserLevelId(clientId *uuid.UUID, userId uint) (uint, error) {
|
||||||
|
// Get user from database to retrieve user level
|
||||||
|
user, err := _i.UsersRepository.FindOne(clientId, userId)
|
||||||
|
if err != nil {
|
||||||
|
_i.Log.Error().Err(err).Uint("userId", userId).Msg("Failed to find user")
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.UserLevel == nil {
|
||||||
|
_i.Log.Error().Uint("userId", userId).Msg("User has no user level")
|
||||||
|
return 0, errors.New("user has no user level")
|
||||||
|
}
|
||||||
|
|
||||||
|
_i.Log.Info().
|
||||||
|
Uint("userId", userId).
|
||||||
|
Uint("userLevelId", user.UserLevel.ID).
|
||||||
|
Str("userLevelName", user.UserLevel.Name).
|
||||||
|
Msg("Retrieved user level from database")
|
||||||
|
|
||||||
|
return user.UserLevel.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldSkipStep determines if a step should be auto-skipped based on user level
|
||||||
|
func (_i *articleApprovalFlowsService) shouldSkipStep(userLevelId, requiredLevelId uint) bool {
|
||||||
|
// Get user level details to compare level numbers
|
||||||
|
// User level with lower level_number (higher authority) can skip steps requiring higher level_number
|
||||||
|
// For now, we'll use a simple comparison based on IDs
|
||||||
|
// In production, this should compare level_number fields
|
||||||
|
|
||||||
|
// Simple logic: if user level ID is less than required level ID, they can skip
|
||||||
|
// This assumes level 1 (ID=1) has higher authority than level 2 (ID=2), etc.
|
||||||
|
return userLevelId < requiredLevelId
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortStepsByOrder sorts workflow steps by their step order
|
||||||
|
func sortStepsByOrder(steps []*entity.ApprovalWorkflowSteps) {
|
||||||
|
// Simple bubble sort for step order
|
||||||
|
n := len(steps)
|
||||||
|
for i := 0; i < n-1; i++ {
|
||||||
|
for j := 0; j < n-i-1; j++ {
|
||||||
|
if steps[j].StepOrder > steps[j+1].StepOrder {
|
||||||
|
steps[j], steps[j+1] = steps[j+1], steps[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) ApproveStep(clientId *uuid.UUID, flowId uint, approvedById uint, message string) (err error) {
|
||||||
|
// Get approval flow
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow == nil {
|
||||||
|
return errors.New("approval flow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow.StatusId != 1 && flow.StatusId != 4 { // not pending or revision_requested
|
||||||
|
return errors.New("approval flow is not in pending state")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current step
|
||||||
|
currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentStep == nil {
|
||||||
|
return errors.New("current step not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create step log
|
||||||
|
stepLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovalFlowId: flow.ID,
|
||||||
|
StepOrder: flow.CurrentStep,
|
||||||
|
StepName: currentStep.StepName,
|
||||||
|
ApprovedById: &approvedById,
|
||||||
|
Action: "approve",
|
||||||
|
Message: &message,
|
||||||
|
ProcessedAt: time.Now(),
|
||||||
|
UserLevelId: currentStep.RequiredUserLevelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a next step
|
||||||
|
nextStep, err := _i.ApprovalWorkflowStepsRepository.GetNextStep(clientId, flow.WorkflowId, flow.CurrentStep)
|
||||||
|
if err != nil && err.Error() != "record not found" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if nextStep == nil || nextStep.ID == 0 {
|
||||||
|
// No next step - approval complete
|
||||||
|
flowUpdate := &entity.ArticleApprovalFlows{
|
||||||
|
StatusId: 2, // approved
|
||||||
|
CurrentStep: 0, // Set to 0 to indicate completion
|
||||||
|
CompletedAt: &[]time.Time{time.Now()}[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
_i.Log.Info().
|
||||||
|
Interface("flowUpdate :: ", flowUpdate).
|
||||||
|
Msg("Retrieved next step from database")
|
||||||
|
|
||||||
|
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.StatusId = &[]int{2}[0] // approved
|
||||||
|
currentArticle.CurrentApprovalStep = &[]int{0}[0] // Set to 0 to indicate completion
|
||||||
|
currentArticle.IsPublish = &[]bool{true}[0] // Set to true to indicate publication
|
||||||
|
currentArticle.IsDraft = &[]bool{false}[0] // Set to false to indicate publication
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Move to next step
|
||||||
|
flowUpdate := &entity.ArticleApprovalFlows{
|
||||||
|
CurrentStep: nextStep.StepOrder,
|
||||||
|
StatusId: 1, // pending
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.CurrentApprovalStep = &nextStep.StepOrder
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) RejectArticle(clientId *uuid.UUID, flowId uint, rejectedById uint, reason string) (err error) {
|
||||||
|
// Get approval flow
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow == nil {
|
||||||
|
return errors.New("approval flow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow.StatusId != 1 && flow.StatusId != 4 { // not pending or revision_requested
|
||||||
|
return errors.New("approval flow is not in pending state")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current step
|
||||||
|
currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create step log
|
||||||
|
stepLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovalFlowId: flow.ID,
|
||||||
|
StepOrder: flow.CurrentStep,
|
||||||
|
StepName: currentStep.StepName,
|
||||||
|
ApprovedById: &rejectedById,
|
||||||
|
Action: "reject",
|
||||||
|
Message: &reason,
|
||||||
|
ProcessedAt: time.Now(),
|
||||||
|
UserLevelId: currentStep.RequiredUserLevelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update approval flow status
|
||||||
|
flowUpdate := &entity.ArticleApprovalFlows{
|
||||||
|
StatusId: 3, // rejected
|
||||||
|
RejectionReason: &reason,
|
||||||
|
CompletedAt: &[]time.Time{time.Now()}[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.StatusId = &[]int{3}[0] // rejected
|
||||||
|
currentArticle.CurrentApprovalStep = nil
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) RequestRevision(clientId *uuid.UUID, flowId uint, requestedById uint, revisionMessage string) (err error) {
|
||||||
|
// Get approval flow
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow == nil {
|
||||||
|
return errors.New("approval flow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow.StatusId != 1 { // not pending
|
||||||
|
return errors.New("approval flow is not in pending state")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current step
|
||||||
|
currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create step log
|
||||||
|
stepLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovalFlowId: flow.ID,
|
||||||
|
StepOrder: flow.CurrentStep,
|
||||||
|
StepName: currentStep.StepName,
|
||||||
|
ApprovedById: &requestedById,
|
||||||
|
Action: "request_revision",
|
||||||
|
Message: &revisionMessage,
|
||||||
|
ProcessedAt: time.Now(),
|
||||||
|
UserLevelId: currentStep.RequiredUserLevelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update approval flow status
|
||||||
|
flowUpdate := &entity.ArticleApprovalFlows{
|
||||||
|
StatusId: 4, // revision_requested
|
||||||
|
RevisionRequested: &[]bool{true}[0],
|
||||||
|
RevisionMessage: &revisionMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.StatusId = &[]int{4}[0] // revision_requested
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) ResubmitAfterRevision(clientId *uuid.UUID, flowId uint, resubmittedById uint) (err error) {
|
||||||
|
// Get approval flow
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow == nil {
|
||||||
|
return errors.New("approval flow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow.StatusId != 4 { // not revision_requested
|
||||||
|
return errors.New("approval flow is not in revision requested state")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset approval flow to pending
|
||||||
|
flowUpdate := &entity.ArticleApprovalFlows{
|
||||||
|
StatusId: 1, // pending
|
||||||
|
RevisionRequested: &[]bool{false}[0],
|
||||||
|
RevisionMessage: nil,
|
||||||
|
CurrentStep: 1, // restart from first step
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current article data first
|
||||||
|
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the necessary fields
|
||||||
|
currentArticle.StatusId = &[]int{1}[0] // pending approval
|
||||||
|
currentArticle.CurrentApprovalStep = &[]int{1}[0]
|
||||||
|
|
||||||
|
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create resubmission log
|
||||||
|
stepLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovalFlowId: flow.ID,
|
||||||
|
StepOrder: 1,
|
||||||
|
StepName: "Resubmission",
|
||||||
|
ApprovedById: &resubmittedById,
|
||||||
|
Action: "resubmit",
|
||||||
|
Message: &[]string{"Article resubmitted after revision"}[0],
|
||||||
|
ProcessedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard and queue methods
|
||||||
|
func (_i *articleApprovalFlowsService) GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, page, limit int, filters map[string]interface{}) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.GetPendingApprovals(clientId, userLevelId, page, limit, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, page, limit int, includePreview bool, urgentOnly bool) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.GetMyApprovalQueue(clientId, userLevelId, page, limit, includePreview, urgentOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) GetApprovalHistory(clientId *uuid.UUID, articleId uint, page, limit int) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error) {
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.GetApprovalHistory(clientId, articleId, page, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics and analytics
|
||||||
|
func (_i *articleApprovalFlowsService) GetPendingCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.GetPendingCountByLevel(clientId, userLevelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) GetOverdueCountByLevel(clientId *uuid.UUID, userLevelId uint) (count int64, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.GetOverdueCountByLevel(clientId, userLevelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) GetApprovalStatistics(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (stats map[string]interface{}, err error) {
|
||||||
|
stats = make(map[string]interface{})
|
||||||
|
|
||||||
|
// Get approved count
|
||||||
|
approvedCount, err := _i.ArticleApprovalFlowsRepository.GetApprovedCountByPeriod(clientId, userLevelId, startDate, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rejected count
|
||||||
|
rejectedCount, err := _i.ArticleApprovalFlowsRepository.GetRejectedCountByPeriod(clientId, userLevelId, startDate, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get revision request count
|
||||||
|
revisionCount, err := _i.ArticleApprovalFlowsRepository.GetRevisionRequestCountByPeriod(clientId, userLevelId, startDate, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["approved_count"] = approvedCount
|
||||||
|
stats["rejected_count"] = rejectedCount
|
||||||
|
stats["revision_requested_count"] = revisionCount
|
||||||
|
stats["total_processed"] = approvedCount + rejectedCount + revisionCount
|
||||||
|
stats["period_start"] = startDate
|
||||||
|
stats["period_end"] = endDate
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) GetWorkloadAnalytics(clientId *uuid.UUID, userLevelId uint) (analytics map[string]interface{}, err error) {
|
||||||
|
return _i.ArticleApprovalFlowsRepository.GetLevelWorkload(clientId, userLevelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workflow management
|
||||||
|
func (_i *articleApprovalFlowsService) CanUserApproveStep(clientId *uuid.UUID, flowId uint, userId uint, userLevelId uint) (canApprove bool, reason string, err error) {
|
||||||
|
// Get approval flow
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow == nil {
|
||||||
|
return false, "approval flow not found", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow.StatusId != 1 && flow.StatusId != 4 { // not pending or revision_requested
|
||||||
|
return false, "approval flow is not in pending state", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current step
|
||||||
|
currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentStep == nil {
|
||||||
|
return false, "current step not found", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user level matches required level
|
||||||
|
if currentStep.RequiredUserLevelId != userLevelId {
|
||||||
|
return false, "user level does not match required level for this step", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user submitted the article (cannot approve own submission)
|
||||||
|
if flow.SubmittedById == userId {
|
||||||
|
return false, "cannot approve own submission", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) GetCurrentStepInfo(clientId *uuid.UUID, flowId uint) (stepInfo map[string]interface{}, err error) {
|
||||||
|
stepInfo = make(map[string]interface{})
|
||||||
|
|
||||||
|
// Get approval flow
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow == nil {
|
||||||
|
return nil, errors.New("approval flow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current step
|
||||||
|
currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stepInfo["current_step"] = flow.CurrentStep
|
||||||
|
stepInfo["step_name"] = currentStep.StepName
|
||||||
|
stepInfo["required_user_level_id"] = currentStep.RequiredUserLevelId
|
||||||
|
stepInfo["can_skip"] = currentStep.CanSkip
|
||||||
|
stepInfo["auto_approve_after_hours"] = currentStep.AutoApproveAfterHours
|
||||||
|
stepInfo["status"] = flow.StatusId
|
||||||
|
|
||||||
|
return stepInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalFlowsService) GetNextStepPreview(clientId *uuid.UUID, flowId uint) (nextStep *entity.ApprovalWorkflowSteps, err error) {
|
||||||
|
// Get approval flow
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow == nil {
|
||||||
|
return nil, errors.New("approval flow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next step
|
||||||
|
nextStep, err = _i.ApprovalWorkflowStepsRepository.GetNextStep(clientId, flow.WorkflowId, flow.CurrentStep)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextStep, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package article_approval_step_logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/controller"
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/repository"
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/service"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArticleApprovalStepLogsRouter struct of ArticleApprovalStepLogsRouter
|
||||||
|
type ArticleApprovalStepLogsRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller controller.ArticleApprovalStepLogsController
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArticleApprovalStepLogsModule register bulky of ArticleApprovalStepLogs module
|
||||||
|
var NewArticleApprovalStepLogsModule = fx.Options(
|
||||||
|
// register repository of ArticleApprovalStepLogs module
|
||||||
|
fx.Provide(repository.NewArticleApprovalStepLogsRepository),
|
||||||
|
|
||||||
|
// register service of ArticleApprovalStepLogs module
|
||||||
|
fx.Provide(service.NewArticleApprovalStepLogsService),
|
||||||
|
|
||||||
|
// register controller of ArticleApprovalStepLogs module
|
||||||
|
fx.Provide(controller.NewArticleApprovalStepLogsController),
|
||||||
|
|
||||||
|
// register router of ArticleApprovalStepLogs module
|
||||||
|
fx.Provide(NewArticleApprovalStepLogsRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewArticleApprovalStepLogsRouter init ArticleApprovalStepLogsRouter
|
||||||
|
func NewArticleApprovalStepLogsRouter(fiber *fiber.App, controller controller.ArticleApprovalStepLogsController) *ArticleApprovalStepLogsRouter {
|
||||||
|
return &ArticleApprovalStepLogsRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterArticleApprovalStepLogsRoutes register routes of ArticleApprovalStepLogs module
|
||||||
|
func (_i *ArticleApprovalStepLogsRouter) RegisterArticleApprovalStepLogsRoutes() {
|
||||||
|
// define controllers
|
||||||
|
articleApprovalStepLogsController := _i.Controller
|
||||||
|
|
||||||
|
// define routes
|
||||||
|
_i.App.Route("/article-approval-step-logs", func(router fiber.Router) {
|
||||||
|
router.Get("/", articleApprovalStepLogsController.All)
|
||||||
|
router.Get("/:id", articleApprovalStepLogsController.Show)
|
||||||
|
router.Post("/", articleApprovalStepLogsController.Save)
|
||||||
|
router.Put("/:id", articleApprovalStepLogsController.Update)
|
||||||
|
router.Delete("/:id", articleApprovalStepLogsController.Delete)
|
||||||
|
|
||||||
|
// Approval process management routes
|
||||||
|
router.Get("/flow/:flow_id", articleApprovalStepLogsController.GetByApprovalFlow)
|
||||||
|
router.Get("/step/:step_id", articleApprovalStepLogsController.GetByWorkflowStep)
|
||||||
|
router.Get("/approver/:user_id", articleApprovalStepLogsController.GetByApprover)
|
||||||
|
router.Get("/pending", articleApprovalStepLogsController.GetPendingApprovals)
|
||||||
|
router.Get("/overdue", articleApprovalStepLogsController.GetOverdueApprovals)
|
||||||
|
|
||||||
|
// Approval action routes
|
||||||
|
router.Post("/:id/process", articleApprovalStepLogsController.ProcessApproval)
|
||||||
|
router.Post("/bulk-process", articleApprovalStepLogsController.BulkProcessApproval)
|
||||||
|
router.Post("/:id/auto-approve", articleApprovalStepLogsController.AutoApprove)
|
||||||
|
|
||||||
|
// History and analytics routes
|
||||||
|
router.Get("/history", articleApprovalStepLogsController.GetApprovalHistory)
|
||||||
|
router.Get("/stats", articleApprovalStepLogsController.GetApprovalStats)
|
||||||
|
router.Get("/user/:user_id/workload", articleApprovalStepLogsController.GetUserWorkload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,678 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"web-medols-be/app/middleware"
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/request"
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/service"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
utilRes "web-medols-be/utils/response"
|
||||||
|
utilVal "web-medols-be/utils/validator"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type articleApprovalStepLogsController struct {
|
||||||
|
articleApprovalStepLogsService service.ArticleApprovalStepLogsService
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogsController interface {
|
||||||
|
// Basic CRUD
|
||||||
|
All(c *fiber.Ctx) error
|
||||||
|
Show(c *fiber.Ctx) error
|
||||||
|
Save(c *fiber.Ctx) error
|
||||||
|
Update(c *fiber.Ctx) error
|
||||||
|
Delete(c *fiber.Ctx) error
|
||||||
|
|
||||||
|
// Approval process management
|
||||||
|
GetByApprovalFlow(c *fiber.Ctx) error
|
||||||
|
GetByWorkflowStep(c *fiber.Ctx) error
|
||||||
|
GetByApprover(c *fiber.Ctx) error
|
||||||
|
GetPendingApprovals(c *fiber.Ctx) error
|
||||||
|
GetOverdueApprovals(c *fiber.Ctx) error
|
||||||
|
|
||||||
|
// Approval actions
|
||||||
|
ProcessApproval(c *fiber.Ctx) error
|
||||||
|
BulkProcessApproval(c *fiber.Ctx) error
|
||||||
|
AutoApprove(c *fiber.Ctx) error
|
||||||
|
|
||||||
|
// History and analytics
|
||||||
|
GetApprovalHistory(c *fiber.Ctx) error
|
||||||
|
GetApprovalStats(c *fiber.Ctx) error
|
||||||
|
GetUserWorkload(c *fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArticleApprovalStepLogsController(
|
||||||
|
articleApprovalStepLogsService service.ArticleApprovalStepLogsService,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) ArticleApprovalStepLogsController {
|
||||||
|
return &articleApprovalStepLogsController{
|
||||||
|
articleApprovalStepLogsService: articleApprovalStepLogsService,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All ArticleApprovalStepLogs
|
||||||
|
// @Summary Get all ArticleApprovalStepLogs
|
||||||
|
// @Description API for getting all ArticleApprovalStepLogs
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param req query request.ArticleApprovalStepLogsQueryRequest false "query parameters"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) All(c *fiber.Ctx) error {
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqContext := request.ArticleApprovalStepLogsQueryRequestContext{
|
||||||
|
ApprovalFlowId: c.Query("approvalFlowId"),
|
||||||
|
StepId: c.Query("stepId"),
|
||||||
|
ActionById: c.Query("actionById"),
|
||||||
|
ActionType: c.Query("actionType"),
|
||||||
|
StatusId: c.Query("statusId"),
|
||||||
|
ActionDateFrom: c.Query("actionDateFrom"),
|
||||||
|
ActionDateTo: c.Query("actionDateTo"),
|
||||||
|
UserLevelId: c.Query("userLevelId"),
|
||||||
|
IsUrgent: c.Query("isUrgent"),
|
||||||
|
Search: c.Query("search"),
|
||||||
|
OrderBy: c.Query("orderBy"),
|
||||||
|
OrderDirection: c.Query("orderDirection"),
|
||||||
|
}
|
||||||
|
req := reqContext.ToParamRequest()
|
||||||
|
req.Pagination = paginate
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
_i.Log.Info().Interface("clientId", clientId).Msg("")
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, paging, err := _i.articleApprovalStepLogsService.GetAll(clientId, *req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs list successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show ArticleApprovalStepLogs
|
||||||
|
// @Summary Get one ArticleApprovalStepLogs
|
||||||
|
// @Description API for getting one ArticleApprovalStepLogs
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ArticleApprovalStepLogs ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/{id} [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) Show(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.FindOne(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save ArticleApprovalStepLogs
|
||||||
|
// @Summary Save ArticleApprovalStepLogs
|
||||||
|
// @Description API for saving ArticleApprovalStepLogs
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param payload body request.ArticleApprovalStepLogsCreateRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs [post]
|
||||||
|
func (_i *articleApprovalStepLogsController) Save(c *fiber.Ctx) error {
|
||||||
|
req := new(request.ArticleApprovalStepLogsCreateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entity
|
||||||
|
entity := req.ToEntity()
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.Create(clientId, entity)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs successfully created"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update ArticleApprovalStepLogs
|
||||||
|
// @Summary Update ArticleApprovalStepLogs
|
||||||
|
// @Description API for updating ArticleApprovalStepLogs
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ArticleApprovalStepLogs ID"
|
||||||
|
// @Param payload body request.ArticleApprovalStepLogsUpdateRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/{id} [put]
|
||||||
|
func (_i *articleApprovalStepLogsController) Update(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ArticleApprovalStepLogsUpdateRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
// Convert request to entity
|
||||||
|
entity := req.ToEntity()
|
||||||
|
|
||||||
|
err = _i.articleApprovalStepLogsService.Update(clientId, uint(id), entity)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs successfully updated"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete ArticleApprovalStepLogs
|
||||||
|
// @Summary Delete ArticleApprovalStepLogs
|
||||||
|
// @Description API for deleting ArticleApprovalStepLogs
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "ArticleApprovalStepLogs ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/{id} [delete]
|
||||||
|
func (_i *articleApprovalStepLogsController) Delete(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.articleApprovalStepLogsService.Delete(clientId, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs successfully deleted"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByApprovalFlow ArticleApprovalStepLogs
|
||||||
|
// @Summary Get ArticleApprovalStepLogs by Approval Flow ID
|
||||||
|
// @Description API for getting ArticleApprovalStepLogs by Approval Flow ID
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param flow_id path int true "Approval Flow ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/flow/{flow_id} [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetByApprovalFlow(c *fiber.Ctx) error {
|
||||||
|
flowId, err := strconv.Atoi(c.Params("flow_id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid flow ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetByApprovalFlowID(clientId, uint(flowId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs by approval flow successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByWorkflowStep ArticleApprovalStepLogs
|
||||||
|
// @Summary Get ArticleApprovalStepLogs by Workflow Step ID
|
||||||
|
// @Description API for getting ArticleApprovalStepLogs by Workflow Step ID
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param step_id path int true "Workflow Step ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/step/{step_id} [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetByWorkflowStep(c *fiber.Ctx) error {
|
||||||
|
stepId, err := strconv.Atoi(c.Params("step_id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid step ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetByWorkflowStepID(clientId, uint(stepId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs by workflow step successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByApprover ArticleApprovalStepLogs
|
||||||
|
// @Summary Get ArticleApprovalStepLogs by Approver User ID
|
||||||
|
// @Description API for getting ArticleApprovalStepLogs by Approver User ID
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param user_id path int true "Approver User ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/approver/{user_id} [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetByApprover(c *fiber.Ctx) error {
|
||||||
|
userId, err := strconv.Atoi(c.Params("user_id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid user ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetByApproverUserID(clientId, uint(userId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"ArticleApprovalStepLogs by approver successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingApprovals ArticleApprovalStepLogs
|
||||||
|
// @Summary Get Pending Approvals
|
||||||
|
// @Description API for getting pending approvals
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param user_id query int false "Filter by user ID"
|
||||||
|
// @Param role_id query int false "Filter by role ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/pending [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetPendingApprovals(c *fiber.Ctx) error {
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
var userID *uint
|
||||||
|
var roleID *uint
|
||||||
|
|
||||||
|
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||||
|
if id, err := strconv.Atoi(userIDStr); err == nil {
|
||||||
|
userIDVal := uint(id)
|
||||||
|
userID = &userIDVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if roleIDStr := c.Query("role_id"); roleIDStr != "" {
|
||||||
|
if id, err := strconv.Atoi(roleIDStr); err == nil {
|
||||||
|
roleIDVal := uint(id)
|
||||||
|
roleID = &roleIDVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetPendingApprovals(clientId, userID, roleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Pending approvals successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOverdueApprovals ArticleApprovalStepLogs
|
||||||
|
// @Summary Get Overdue Approvals
|
||||||
|
// @Description API for getting overdue approvals
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param user_id query int false "Filter by user ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/overdue [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetOverdueApprovals(c *fiber.Ctx) error {
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
var userID *uint
|
||||||
|
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||||
|
if id, err := strconv.Atoi(userIDStr); err == nil {
|
||||||
|
userIDVal := uint(id)
|
||||||
|
userID = &userIDVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetOverdueApprovals(clientId, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Overdue approvals successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessApproval ArticleApprovalStepLogs
|
||||||
|
// @Summary Process Approval
|
||||||
|
// @Description API for processing approval step
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "Step Log ID"
|
||||||
|
// @Param payload body request.ProcessApprovalRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/{id}/process [post]
|
||||||
|
func (_i *articleApprovalStepLogsController) ProcessApproval(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(request.ProcessApprovalRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.articleApprovalStepLogsService.ProcessApproval(clientId, uint(id), req.UserID, req.StatusID, req.Comments)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Approval processed successfully"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkProcessApproval ArticleApprovalStepLogs
|
||||||
|
// @Summary Bulk Process Approvals
|
||||||
|
// @Description API for bulk processing approval steps
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param payload body request.BulkProcessApprovalRequest true "Required payload"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/bulk-process [post]
|
||||||
|
func (_i *articleApprovalStepLogsController) BulkProcessApproval(c *fiber.Ctx) error {
|
||||||
|
req := new(request.BulkProcessApprovalRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err := _i.articleApprovalStepLogsService.BulkProcessApproval(clientId, req.LogIDs, req.UserID, req.StatusID, req.Comments)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Approvals processed successfully"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoApprove ArticleApprovalStepLogs
|
||||||
|
// @Summary Auto Approve Step
|
||||||
|
// @Description API for automatically approving a step
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param id path int true "Step Log ID"
|
||||||
|
// @Param reason query string true "Auto approval reason"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/{id}/auto-approve [post]
|
||||||
|
func (_i *articleApprovalStepLogsController) AutoApprove(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := c.Query("reason")
|
||||||
|
if reason == "" {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Reason is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
err = _i.articleApprovalStepLogsService.AutoApprove(clientId, uint(id), reason)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Step auto approved successfully"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApprovalHistory ArticleApprovalStepLogs
|
||||||
|
// @Summary Get Approval History
|
||||||
|
// @Description API for getting approval history
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param article_id query int false "Filter by article ID"
|
||||||
|
// @Param user_id query int false "Filter by user ID"
|
||||||
|
// @Param from_date query string false "Filter from date (YYYY-MM-DD)"
|
||||||
|
// @Param to_date query string false "Filter to date (YYYY-MM-DD)"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/history [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetApprovalHistory(c *fiber.Ctx) error {
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
var articleID *uint
|
||||||
|
var userID *uint
|
||||||
|
filters := make(map[string]interface{})
|
||||||
|
|
||||||
|
if articleIDStr := c.Query("article_id"); articleIDStr != "" {
|
||||||
|
if id, err := strconv.Atoi(articleIDStr); err == nil {
|
||||||
|
articleIDVal := uint(id)
|
||||||
|
articleID = &articleIDVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||||
|
if id, err := strconv.Atoi(userIDStr); err == nil {
|
||||||
|
userIDVal := uint(id)
|
||||||
|
userID = &userIDVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromDate := c.Query("from_date"); fromDate != "" {
|
||||||
|
filters["from_date"] = fromDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if toDate := c.Query("to_date"); toDate != "" {
|
||||||
|
filters["to_date"] = toDate
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetApprovalHistory(clientId, articleID, userID, filters)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Approval history successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApprovalStats ArticleApprovalStepLogs
|
||||||
|
// @Summary Get Approval Statistics
|
||||||
|
// @Description API for getting approval statistics
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param from_date query string false "Filter from date (YYYY-MM-DD)"
|
||||||
|
// @Param to_date query string false "Filter to date (YYYY-MM-DD)"
|
||||||
|
// @Param workflow_id query int false "Filter by workflow ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/stats [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetApprovalStats(c *fiber.Ctx) error {
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
filters := make(map[string]interface{})
|
||||||
|
|
||||||
|
if fromDate := c.Query("from_date"); fromDate != "" {
|
||||||
|
filters["from_date"] = fromDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if toDate := c.Query("to_date"); toDate != "" {
|
||||||
|
filters["to_date"] = toDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if workflowID := c.Query("workflow_id"); workflowID != "" {
|
||||||
|
if id, err := strconv.Atoi(workflowID); err == nil {
|
||||||
|
filters["workflow_id"] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetApprovalStats(clientId, filters)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Approval statistics successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserWorkload ArticleApprovalStepLogs
|
||||||
|
// @Summary Get User Workload
|
||||||
|
// @Description API for getting user workload statistics
|
||||||
|
// @Tags ArticleApprovalStepLogs
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||||
|
// @Param user_id path int true "User ID"
|
||||||
|
// @Param include_stats query bool false "Include detailed statistics"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 400 {object} response.BadRequestError
|
||||||
|
// @Failure 401 {object} response.UnauthorizedError
|
||||||
|
// @Failure 500 {object} response.InternalServerError
|
||||||
|
// @Router /article-approval-step-logs/user/{user_id}/workload [get]
|
||||||
|
func (_i *articleApprovalStepLogsController) GetUserWorkload(c *fiber.Ctx) error {
|
||||||
|
userId, err := strconv.Atoi(c.Params("user_id"))
|
||||||
|
if err != nil {
|
||||||
|
return utilRes.ErrorBadRequest(c, "Invalid user ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
includeStats := c.Query("include_stats") == "true"
|
||||||
|
|
||||||
|
// Get ClientId from context
|
||||||
|
clientId := middleware.GetClientID(c)
|
||||||
|
|
||||||
|
articleApprovalStepLogsData, err := _i.articleApprovalStepLogsService.GetUserWorkload(clientId, uint(userId), includeStats)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"User workload successfully retrieved"},
|
||||||
|
Data: articleApprovalStepLogsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
res "web-medols-be/app/module/article_approval_step_logs/response"
|
||||||
|
usersRepository "web-medols-be/app/module/users/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ArticleApprovalStepLogsResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
articleApprovalStepLogsReq *entity.ArticleApprovalStepLogs,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (articleApprovalStepLogsRes *res.ArticleApprovalStepLogsResponse) {
|
||||||
|
|
||||||
|
if articleApprovalStepLogsReq != nil {
|
||||||
|
// Map entity fields to response fields
|
||||||
|
// approvalStatusID := uint(1) // Default status
|
||||||
|
// if articleApprovalStepLogsReq.Action == "approved" {
|
||||||
|
// approvalStatusID = 2
|
||||||
|
// } else if articleApprovalStepLogsReq.Action == "rejected" {
|
||||||
|
// approvalStatusID = 3
|
||||||
|
// }
|
||||||
|
|
||||||
|
articleApprovalStepLogsRes = &res.ArticleApprovalStepLogsResponse{
|
||||||
|
ID: articleApprovalStepLogsReq.ID,
|
||||||
|
ApprovalFlowId: articleApprovalStepLogsReq.ApprovalFlowId,
|
||||||
|
StepOrder: articleApprovalStepLogsReq.StepOrder,
|
||||||
|
StepName: articleApprovalStepLogsReq.StepName,
|
||||||
|
ApprovedById: articleApprovalStepLogsReq.ApprovedById,
|
||||||
|
Action: articleApprovalStepLogsReq.Action,
|
||||||
|
Message: articleApprovalStepLogsReq.Message,
|
||||||
|
ProcessedAt: articleApprovalStepLogsReq.ProcessedAt,
|
||||||
|
UserLevelId: articleApprovalStepLogsReq.UserLevelId,
|
||||||
|
ClientId: func() *string {
|
||||||
|
if articleApprovalStepLogsReq.ClientId != nil {
|
||||||
|
s := articleApprovalStepLogsReq.ClientId.String()
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(),
|
||||||
|
CreatedAt: articleApprovalStepLogsReq.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return articleApprovalStepLogsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticleApprovalStepLogsDetailResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
articleApprovalStepLogsReq *entity.ArticleApprovalStepLogs,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (articleApprovalStepLogsRes *res.ArticleApprovalStepLogsDetailResponse) {
|
||||||
|
|
||||||
|
if articleApprovalStepLogsReq != nil {
|
||||||
|
// Map entity fields to response fields
|
||||||
|
approvalStatusID := uint(1) // Default status
|
||||||
|
if articleApprovalStepLogsReq.Action == "approved" {
|
||||||
|
approvalStatusID = 2
|
||||||
|
} else if articleApprovalStepLogsReq.Action == "rejected" {
|
||||||
|
approvalStatusID = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalStepLogsRes = &res.ArticleApprovalStepLogsDetailResponse{
|
||||||
|
ID: articleApprovalStepLogsReq.ID,
|
||||||
|
ArticleApprovalFlowID: articleApprovalStepLogsReq.ApprovalFlowId,
|
||||||
|
WorkflowStepID: uint(articleApprovalStepLogsReq.StepOrder),
|
||||||
|
ApproverUserID: articleApprovalStepLogsReq.ApprovedById,
|
||||||
|
ApprovalStatusID: approvalStatusID,
|
||||||
|
Comments: articleApprovalStepLogsReq.Message,
|
||||||
|
ApprovedAt: &articleApprovalStepLogsReq.ProcessedAt,
|
||||||
|
IsAutoApproved: false, // Default value
|
||||||
|
CreatedAt: articleApprovalStepLogsReq.CreatedAt,
|
||||||
|
UpdatedAt: articleApprovalStepLogsReq.CreatedAt, // Use CreatedAt as UpdatedAt
|
||||||
|
// Relations would be populated separately if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return articleApprovalStepLogsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticleApprovalStepLogsSummaryResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
articleApprovalStepLogsReq *entity.ArticleApprovalStepLogs,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (articleApprovalStepLogsRes *res.ArticleApprovalStepLogsSummaryResponse) {
|
||||||
|
|
||||||
|
approverName := ""
|
||||||
|
if articleApprovalStepLogsReq.ApprovedById != nil {
|
||||||
|
findUser, _ := usersRepo.FindOne(clientId, *articleApprovalStepLogsReq.ApprovedById)
|
||||||
|
if findUser != nil {
|
||||||
|
approverName = findUser.Fullname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if articleApprovalStepLogsReq != nil {
|
||||||
|
// Map entity fields to response fields
|
||||||
|
approvalStatusName := "Pending"
|
||||||
|
if articleApprovalStepLogsReq.Action == "approved" {
|
||||||
|
approvalStatusName = "Approved"
|
||||||
|
} else if articleApprovalStepLogsReq.Action == "rejected" {
|
||||||
|
approvalStatusName = "Rejected"
|
||||||
|
}
|
||||||
|
|
||||||
|
articleApprovalStepLogsRes = &res.ArticleApprovalStepLogsSummaryResponse{
|
||||||
|
ID: articleApprovalStepLogsReq.ID,
|
||||||
|
WorkflowStepID: uint(articleApprovalStepLogsReq.StepOrder),
|
||||||
|
StepName: articleApprovalStepLogsReq.StepName,
|
||||||
|
ApproverUserID: articleApprovalStepLogsReq.ApprovedById,
|
||||||
|
ApproverUserName: &approverName,
|
||||||
|
ApprovalStatusID: uint(1), // Default status ID
|
||||||
|
ApprovalStatusName: approvalStatusName,
|
||||||
|
ApprovedAt: &articleApprovalStepLogsReq.ProcessedAt,
|
||||||
|
IsAutoApproved: false, // Default value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return articleApprovalStepLogsRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApprovalHistoryResponseMapper(
|
||||||
|
log zerolog.Logger,
|
||||||
|
clientId *uuid.UUID,
|
||||||
|
articleApprovalStepLogsReq *entity.ArticleApprovalStepLogs,
|
||||||
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
) (approvalHistoryRes *res.ApprovalHistoryResponse) {
|
||||||
|
|
||||||
|
if articleApprovalStepLogsReq != nil {
|
||||||
|
// Create a basic ApprovalHistoryResponse structure
|
||||||
|
// This would typically be built from multiple step logs, not a single one
|
||||||
|
approvalHistoryRes = &res.ApprovalHistoryResponse{
|
||||||
|
ArticleID: 0, // Would need article information
|
||||||
|
ArticleTitle: "", // Would need article information
|
||||||
|
WorkflowName: "", // Would need workflow information
|
||||||
|
StartedAt: articleApprovalStepLogsReq.CreatedAt,
|
||||||
|
CompletedAt: nil, // Would be set when workflow is complete
|
||||||
|
CurrentStep: &articleApprovalStepLogsReq.StepName,
|
||||||
|
Steps: []res.ArticleApprovalStepLogsSummaryResponse{}, // Would be populated with all steps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvalHistoryRes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,438 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/request"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type articleApprovalStepLogsRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArticleApprovalStepLogsRepository define interface of IArticleApprovalStepLogsRepository
|
||||||
|
type ArticleApprovalStepLogsRepository interface {
|
||||||
|
// Basic CRUD
|
||||||
|
GetAll(clientId *uuid.UUID, req request.ArticleApprovalStepLogsQueryRequest) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (log *entity.ArticleApprovalStepLogs, err error)
|
||||||
|
GetByApprovalFlowId(clientId *uuid.UUID, approvalFlowId uint) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
Create(clientId *uuid.UUID, log *entity.ArticleApprovalStepLogs) (logReturn *entity.ArticleApprovalStepLogs, err error)
|
||||||
|
Update(id uint, log *entity.ArticleApprovalStepLogs) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Approval History Methods
|
||||||
|
GetApprovalHistory(clientId *uuid.UUID, articleId uint, page, limit int) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error)
|
||||||
|
GetUserApprovalHistory(clientId *uuid.UUID, userId uint, page, limit int, filters map[string]interface{}) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error)
|
||||||
|
GetLevelApprovalHistory(clientId *uuid.UUID, userLevelId uint, page, limit int, filters map[string]interface{}) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error)
|
||||||
|
|
||||||
|
// Analytics Methods
|
||||||
|
GetApprovalTimeAnalytics(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (analytics map[string]interface{}, err error)
|
||||||
|
GetUserPerformanceMetrics(clientId *uuid.UUID, userId uint, startDate, endDate time.Time) (metrics map[string]interface{}, err error)
|
||||||
|
GetWorkflowStepAnalytics(clientId *uuid.UUID, workflowId uint, stepOrder int) (analytics map[string]interface{}, err error)
|
||||||
|
|
||||||
|
// Audit Methods
|
||||||
|
GetAuditTrail(clientId *uuid.UUID, articleId uint) (trail []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
GetRecentActions(clientId *uuid.UUID, userId uint, limit int) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArticleApprovalStepLogsRepository(db *database.Database, log zerolog.Logger) ArticleApprovalStepLogsRepository {
|
||||||
|
return &articleApprovalStepLogsRepository{
|
||||||
|
DB: db,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic CRUD implementations
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetAll(clientId *uuid.UUID, req request.ArticleApprovalStepLogsQueryRequest) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
// Apply client filter
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", *clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters based on request
|
||||||
|
if req.ArticleApprovalFlowID != nil {
|
||||||
|
query = query.Where("article_approval_flow_id = ?", *req.ArticleApprovalFlowID)
|
||||||
|
}
|
||||||
|
if req.WorkflowStepID != nil {
|
||||||
|
query = query.Where("workflow_step_id = ?", *req.WorkflowStepID)
|
||||||
|
}
|
||||||
|
if req.ApproverUserID != nil {
|
||||||
|
query = query.Where("approver_user_id = ?", *req.ApproverUserID)
|
||||||
|
}
|
||||||
|
if req.ApprovalStatusID != nil {
|
||||||
|
query = query.Where("approval_status_id = ?", *req.ApprovalStatusID)
|
||||||
|
}
|
||||||
|
if req.DateFrom != nil {
|
||||||
|
query = query.Where("approved_at >= ?", *req.DateFrom)
|
||||||
|
}
|
||||||
|
if req.DateTo != nil {
|
||||||
|
query = query.Where("approved_at <= ?", *req.DateTo)
|
||||||
|
}
|
||||||
|
if req.IsAutoApproved != nil {
|
||||||
|
query = query.Where("is_auto_approved = ?", *req.IsAutoApproved)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := "article_approval_step_logs.approved_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).Preload("ApprovalFlow").Preload("Step").Preload("ActionBy").Preload("Status").Find(&logs).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 logs, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) FindOne(clientId *uuid.UUID, id uint) (log *entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("ApprovalFlow").Preload("ApprovedBy").Preload("UserLevel")
|
||||||
|
|
||||||
|
err = query.First(&log, id).Error
|
||||||
|
return log, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetByApprovalFlowId(clientId *uuid.UUID, approvalFlowId uint) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("approval_flow_id = ?", approvalFlowId)
|
||||||
|
query = query.Preload("ApprovalFlow").Preload("ApprovedBy").Preload("UserLevel")
|
||||||
|
query = query.Order("step_order ASC, processed_at ASC")
|
||||||
|
|
||||||
|
err = query.Find(&logs).Error
|
||||||
|
return logs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) Create(clientId *uuid.UUID, log *entity.ArticleApprovalStepLogs) (logReturn *entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
log.ClientId = clientId
|
||||||
|
log.ProcessedAt = time.Now()
|
||||||
|
err = _i.DB.DB.Create(&log).Error
|
||||||
|
return log, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) Update(id uint, log *entity.ArticleApprovalStepLogs) (err error) {
|
||||||
|
err = _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{}).Where("id = ?", id).Updates(log).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.Delete(&entity.ArticleApprovalStepLogs{}, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approval History Methods
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetApprovalHistory(clientId *uuid.UUID, articleId uint, page, limit int) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_step_logs.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join with approval flows to filter by article
|
||||||
|
query = query.Joins("JOIN article_approval_flows ON article_approval_step_logs.approval_flow_id = article_approval_flows.id")
|
||||||
|
query = query.Where("article_approval_flows.article_id = ?", articleId)
|
||||||
|
query = query.Preload("ApprovalFlow").Preload("ApprovedBy").Preload("UserLevel")
|
||||||
|
query = query.Order("article_approval_step_logs.processed_at DESC")
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
err = query.Offset(offset).Limit(limit).Find(&logs).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Count: count,
|
||||||
|
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetUserApprovalHistory(clientId *uuid.UUID, userId uint, page, limit int, filters map[string]interface{}) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_step_logs.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("approved_by_id = ?", userId)
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if action, ok := filters["action"]; ok {
|
||||||
|
query = query.Where("action = ?", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if startDate, ok := filters["start_date"]; ok {
|
||||||
|
query = query.Where("processed_at >= ?", startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if endDate, ok := filters["end_date"]; ok {
|
||||||
|
query = query.Where("processed_at <= ?", endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("ApprovalFlow").Preload("ApprovedBy").Preload("UserLevel")
|
||||||
|
query = query.Order("processed_at DESC")
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
err = query.Offset(offset).Limit(limit).Find(&logs).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Count: count,
|
||||||
|
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetLevelApprovalHistory(clientId *uuid.UUID, userLevelId uint, page, limit int, filters map[string]interface{}) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_step_logs.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("user_level_id = ?", userLevelId)
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if action, ok := filters["action"]; ok {
|
||||||
|
query = query.Where("action = ?", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if startDate, ok := filters["start_date"]; ok {
|
||||||
|
query = query.Where("processed_at >= ?", startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if endDate, ok := filters["end_date"]; ok {
|
||||||
|
query = query.Where("processed_at <= ?", endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("ApprovalFlow").Preload("ApprovedBy").Preload("UserLevel")
|
||||||
|
query = query.Order("processed_at DESC")
|
||||||
|
|
||||||
|
err = query.Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
err = query.Offset(offset).Limit(limit).Find(&logs).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, paginator.Pagination{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paging = paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Count: count,
|
||||||
|
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analytics Methods
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetApprovalTimeAnalytics(clientId *uuid.UUID, userLevelId uint, startDate, endDate time.Time) (analytics map[string]interface{}, err error) {
|
||||||
|
analytics = make(map[string]interface{})
|
||||||
|
|
||||||
|
// Get average approval time for this level
|
||||||
|
var avgTime float64
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("user_level_id = ? AND action IN ('approve', 'reject') AND processed_at BETWEEN ? AND ?", userLevelId, startDate, endDate)
|
||||||
|
query = query.Select("AVG(EXTRACT(EPOCH FROM (processed_at - created_at))) as avg_time")
|
||||||
|
|
||||||
|
err = query.Scan(&avgTime).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
analytics["average_approval_time_seconds"] = avgTime
|
||||||
|
analytics["level_id"] = userLevelId
|
||||||
|
analytics["period_start"] = startDate
|
||||||
|
analytics["period_end"] = endDate
|
||||||
|
|
||||||
|
return analytics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetUserPerformanceMetrics(clientId *uuid.UUID, userId uint, startDate, endDate time.Time) (metrics map[string]interface{}, err error) {
|
||||||
|
metrics = make(map[string]interface{})
|
||||||
|
|
||||||
|
// Get counts by action
|
||||||
|
var approvedCount, rejectedCount, revisionCount int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
query = query.Where("approved_by_id = ? AND processed_at BETWEEN ? AND ?", userId, startDate, endDate)
|
||||||
|
|
||||||
|
// Approved count
|
||||||
|
query.Where("action = 'approve'").Count(&approvedCount)
|
||||||
|
|
||||||
|
// Rejected count
|
||||||
|
query.Where("action = 'reject'").Count(&rejectedCount)
|
||||||
|
|
||||||
|
// Revision requested count
|
||||||
|
query.Where("action = 'request_revision'").Count(&revisionCount)
|
||||||
|
|
||||||
|
metrics["approved_count"] = approvedCount
|
||||||
|
metrics["rejected_count"] = rejectedCount
|
||||||
|
metrics["revision_requested_count"] = revisionCount
|
||||||
|
metrics["total_processed"] = approvedCount + rejectedCount + revisionCount
|
||||||
|
metrics["user_id"] = userId
|
||||||
|
metrics["period_start"] = startDate
|
||||||
|
metrics["period_end"] = endDate
|
||||||
|
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetWorkflowStepAnalytics(clientId *uuid.UUID, workflowId uint, stepOrder int) (analytics map[string]interface{}, err error) {
|
||||||
|
analytics = make(map[string]interface{})
|
||||||
|
|
||||||
|
// Get step performance metrics
|
||||||
|
var totalProcessed, approvedCount, rejectedCount int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join with approval flows to filter by workflow
|
||||||
|
query = query.Joins("JOIN article_approval_flows ON article_approval_step_logs.approval_flow_id = article_approval_flows.id")
|
||||||
|
query = query.Where("article_approval_flows.workflow_id = ? AND article_approval_step_logs.step_order = ?", workflowId, stepOrder)
|
||||||
|
|
||||||
|
// Total processed
|
||||||
|
query.Count(&totalProcessed)
|
||||||
|
|
||||||
|
// Approved count
|
||||||
|
query.Where("article_approval_step_logs.action = 'approve'").Count(&approvedCount)
|
||||||
|
|
||||||
|
// Rejected count
|
||||||
|
query.Where("article_approval_step_logs.action = 'reject'").Count(&rejectedCount)
|
||||||
|
|
||||||
|
analytics["workflow_id"] = workflowId
|
||||||
|
analytics["step_order"] = stepOrder
|
||||||
|
analytics["total_processed"] = totalProcessed
|
||||||
|
analytics["approved_count"] = approvedCount
|
||||||
|
analytics["rejected_count"] = rejectedCount
|
||||||
|
analytics["approval_rate"] = float64(approvedCount) / float64(totalProcessed) * 100
|
||||||
|
|
||||||
|
return analytics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit Methods
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetAuditTrail(clientId *uuid.UUID, articleId uint) (trail []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("article_approval_step_logs.client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join with approval flows to filter by article
|
||||||
|
query = query.Joins("JOIN article_approval_flows ON article_approval_step_logs.approval_flow_id = article_approval_flows.id")
|
||||||
|
query = query.Where("article_approval_flows.article_id = ?", articleId)
|
||||||
|
query = query.Preload("ApprovalFlow").Preload("ApprovedBy").Preload("UserLevel")
|
||||||
|
query = query.Order("article_approval_step_logs.processed_at ASC")
|
||||||
|
|
||||||
|
err = query.Find(&trail).Error
|
||||||
|
return trail, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsRepository) GetRecentActions(clientId *uuid.UUID, userId uint, limit int) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
query := _i.DB.DB.Model(&entity.ArticleApprovalStepLogs{})
|
||||||
|
|
||||||
|
if clientId != nil {
|
||||||
|
query = query.Where("client_id = ?", clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("approved_by_id = ?", userId)
|
||||||
|
query = query.Preload("ApprovalFlow").Preload("ApprovedBy").Preload("UserLevel")
|
||||||
|
query = query.Order("processed_at DESC")
|
||||||
|
query = query.Limit(limit)
|
||||||
|
|
||||||
|
err = query.Find(&logs).Error
|
||||||
|
return logs, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateArticleApprovalStepLogsRequest struct {
|
||||||
|
ArticleApprovalFlowID uint `json:"articleApprovalFlowId" validate:"required"`
|
||||||
|
WorkflowStepID uint `json:"workflowStepId" validate:"required"`
|
||||||
|
ApproverUserID *uint `json:"approverUserId"`
|
||||||
|
ApprovalStatusID uint `json:"approvalStatusId" validate:"required"`
|
||||||
|
Comments *string `json:"comments" validate:"omitempty,max=1000"`
|
||||||
|
ApprovedAt *time.Time `json:"approvedAt"`
|
||||||
|
DueDate *time.Time `json:"dueDate"`
|
||||||
|
IsAutoApproved bool `json:"isAutoApproved"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateArticleApprovalStepLogsRequest struct {
|
||||||
|
ApproverUserID *uint `json:"approverUserId"`
|
||||||
|
ApprovalStatusID *uint `json:"approvalStatusId"`
|
||||||
|
Comments *string `json:"comments" validate:"omitempty,max=1000"`
|
||||||
|
ApprovedAt *time.Time `json:"approvedAt"`
|
||||||
|
DueDate *time.Time `json:"dueDate"`
|
||||||
|
IsAutoApproved *bool `json:"isAutoApproved"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogsQueryRequest struct {
|
||||||
|
ArticleApprovalFlowID *uint `json:"articleApprovalFlowId" form:"articleApprovalFlowId"`
|
||||||
|
WorkflowStepID *uint `json:"workflowStepId" form:"workflowStepId"`
|
||||||
|
ApproverUserID *uint `json:"approverUserId" form:"approverUserId"`
|
||||||
|
ApprovalStatusID *uint `json:"approvalStatusId" form:"approvalStatusId"`
|
||||||
|
IsAutoApproved *bool `json:"isAutoApproved" form:"isAutoApproved"`
|
||||||
|
DateFrom *time.Time `json:"dateFrom" form:"dateFrom"`
|
||||||
|
DateTo *time.Time `json:"dateTo" form:"dateTo"`
|
||||||
|
Pagination *paginator.Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessApprovalRequest struct {
|
||||||
|
UserID uint `json:"userId" validate:"required"`
|
||||||
|
StatusID uint `json:"statusId" validate:"required"`
|
||||||
|
ApprovalStatusID uint `json:"approvalStatusId" validate:"required"`
|
||||||
|
Comments *string `json:"comments" validate:"omitempty,max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkProcessApprovalRequest struct {
|
||||||
|
LogIDs []uint `json:"logIds" validate:"required,min=1,max=50,dive,required"`
|
||||||
|
UserID uint `json:"userId" validate:"required"`
|
||||||
|
StatusID uint `json:"statusId" validate:"required"`
|
||||||
|
StepLogIDs []uint `json:"stepLogIds" validate:"required,min=1,max=50,dive,required"`
|
||||||
|
ApprovalStatusID uint `json:"approvalStatusId" validate:"required"`
|
||||||
|
Comments *string `json:"comments" validate:"omitempty,max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetApprovalHistoryRequest struct {
|
||||||
|
ArticleID *uint `json:"articleId" form:"articleId"`
|
||||||
|
UserID *uint `json:"userId" form:"userId"`
|
||||||
|
WorkflowID *uint `json:"workflowId" form:"workflowId"`
|
||||||
|
StatusID *uint `json:"statusId" form:"statusId"`
|
||||||
|
DateFrom *time.Time `json:"dateFrom" form:"dateFrom"`
|
||||||
|
DateTo *time.Time `json:"dateTo" form:"dateTo"`
|
||||||
|
Page int `json:"page" form:"page" validate:"min=1"`
|
||||||
|
Limit int `json:"limit" form:"limit" validate:"min=1,max=100"`
|
||||||
|
SortBy *string `json:"sortBy" form:"sortBy"`
|
||||||
|
SortOrder *string `json:"sortOrder" form:"sortOrder" validate:"omitempty,oneof=asc desc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetApprovalStatsRequest struct {
|
||||||
|
WorkflowID *uint `json:"workflowId" form:"workflowId"`
|
||||||
|
StepID *uint `json:"stepId" form:"stepId"`
|
||||||
|
UserID *uint `json:"userId" form:"userId"`
|
||||||
|
DateFrom *time.Time `json:"dateFrom" form:"dateFrom"`
|
||||||
|
DateTo *time.Time `json:"dateTo" form:"dateTo"`
|
||||||
|
GroupBy *string `json:"groupBy" form:"groupBy" validate:"omitempty,oneof=step user workflow status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUserWorkloadRequest struct {
|
||||||
|
UserID *uint `json:"userId" form:"userId"`
|
||||||
|
RoleID *uint `json:"roleId" form:"roleId"`
|
||||||
|
StatusID *uint `json:"statusId" form:"statusId"`
|
||||||
|
DateFrom *time.Time `json:"dateFrom" form:"dateFrom"`
|
||||||
|
DateTo *time.Time `json:"dateTo" form:"dateTo"`
|
||||||
|
IncludeStats bool `json:"includeStats" form:"includeStats"`
|
||||||
|
Page int `json:"page" form:"page" validate:"min=1"`
|
||||||
|
Limit int `json:"limit" form:"limit" validate:"min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing request types that are referenced in controller
|
||||||
|
type ArticleApprovalStepLogsQueryRequestContext struct {
|
||||||
|
ApprovalFlowId string `json:"approvalFlowId" form:"approvalFlowId"`
|
||||||
|
StepId string `json:"stepId" form:"stepId"`
|
||||||
|
ActionById string `json:"actionById" form:"actionById"`
|
||||||
|
ActionType string `json:"actionType" form:"actionType"`
|
||||||
|
StatusId string `json:"statusId" form:"statusId"`
|
||||||
|
ActionDateFrom string `json:"actionDateFrom" form:"actionDateFrom"`
|
||||||
|
ActionDateTo string `json:"actionDateTo" form:"actionDateTo"`
|
||||||
|
UserLevelId string `json:"userLevelId" form:"userLevelId"`
|
||||||
|
IsUrgent string `json:"isUrgent" form:"isUrgent"`
|
||||||
|
Search string `json:"search" form:"search"`
|
||||||
|
OrderBy string `json:"orderBy" form:"orderBy"`
|
||||||
|
OrderDirection string `json:"orderDirection" form:"orderDirection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ArticleApprovalStepLogsQueryRequestContext) ToParamRequest() *ArticleApprovalStepLogsQueryRequest {
|
||||||
|
// Convert string parameters to appropriate types using helper functions
|
||||||
|
approvalFlowId := parseStringToUintPtr(r.ApprovalFlowId)
|
||||||
|
workflowStepId := parseStringToUintPtr(r.StepId)
|
||||||
|
approverUserId := parseStringToUintPtr(r.ActionById)
|
||||||
|
approvalStatusId := parseStringToUintPtr(r.StatusId)
|
||||||
|
isAutoApproved := parseStringToBoolPtr(r.IsUrgent)
|
||||||
|
dateFrom := parseStringToTimePtr(r.ActionDateFrom)
|
||||||
|
dateTo := parseStringToTimePtr(r.ActionDateTo)
|
||||||
|
|
||||||
|
// Handle string parameters
|
||||||
|
var sortBy *string
|
||||||
|
var sortOrder *string
|
||||||
|
|
||||||
|
if r.OrderBy != "" {
|
||||||
|
sortBy = &r.OrderBy
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.OrderDirection != "" {
|
||||||
|
sortOrder = &r.OrderDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default pagination
|
||||||
|
page := 1
|
||||||
|
limit := 10
|
||||||
|
|
||||||
|
// Create pagination object
|
||||||
|
pagination := &paginator.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sorting if provided
|
||||||
|
if sortBy != nil {
|
||||||
|
pagination.SortBy = *sortBy
|
||||||
|
}
|
||||||
|
if sortOrder != nil {
|
||||||
|
pagination.Sort = *sortOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ArticleApprovalStepLogsQueryRequest{
|
||||||
|
ArticleApprovalFlowID: approvalFlowId,
|
||||||
|
WorkflowStepID: workflowStepId,
|
||||||
|
ApproverUserID: approverUserId,
|
||||||
|
ApprovalStatusID: approvalStatusId,
|
||||||
|
IsAutoApproved: isAutoApproved,
|
||||||
|
DateFrom: dateFrom,
|
||||||
|
DateTo: dateTo,
|
||||||
|
Pagination: pagination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse string to uint pointer
|
||||||
|
func parseStringToUintPtr(s string) *uint {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val, err := strconv.ParseUint(s, 10, 32); err == nil {
|
||||||
|
uintVal := uint(val)
|
||||||
|
return &uintVal
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse string to bool pointer
|
||||||
|
func parseStringToBoolPtr(s string) *bool {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val, err := strconv.ParseBool(s); err == nil {
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse string to time pointer
|
||||||
|
func parseStringToTimePtr(s string) *time.Time {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Try different date formats
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02T15:04:05Z",
|
||||||
|
"2006-01-02T15:04:05Z07:00",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if val, err := time.Parse(format, s); err == nil {
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogsCreateRequest struct {
|
||||||
|
ArticleApprovalFlowID uint `json:"articleApprovalFlowId" validate:"required"`
|
||||||
|
WorkflowStepID uint `json:"workflowStepId" validate:"required"`
|
||||||
|
ApproverUserID *uint `json:"approverUserId"`
|
||||||
|
ApprovalStatusID uint `json:"approvalStatusId" validate:"required"`
|
||||||
|
Comments *string `json:"comments" validate:"omitempty,max=1000"`
|
||||||
|
ApprovedAt *time.Time `json:"approvedAt"`
|
||||||
|
DueDate *time.Time `json:"dueDate"`
|
||||||
|
IsAutoApproved bool `json:"isAutoApproved"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ArticleApprovalStepLogsCreateRequest) ToEntity() *entity.ArticleApprovalStepLogs {
|
||||||
|
// Return the entity representation
|
||||||
|
return &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovalFlowId: r.ArticleApprovalFlowID,
|
||||||
|
StepOrder: int(r.WorkflowStepID),
|
||||||
|
ApprovedById: r.ApproverUserID,
|
||||||
|
Action: "pending", // Default action
|
||||||
|
Message: r.Comments,
|
||||||
|
ProcessedAt: time.Now(),
|
||||||
|
UserLevelId: 1, // Default user level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogsUpdateRequest struct {
|
||||||
|
ApproverUserID *uint `json:"approverUserId"`
|
||||||
|
ApprovalStatusID *uint `json:"approvalStatusId"`
|
||||||
|
Comments *string `json:"comments" validate:"omitempty,max=1000"`
|
||||||
|
ApprovedAt *time.Time `json:"approvedAt"`
|
||||||
|
DueDate *time.Time `json:"dueDate"`
|
||||||
|
IsAutoApproved *bool `json:"isAutoApproved"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ArticleApprovalStepLogsUpdateRequest) ToEntity() *entity.ArticleApprovalStepLogs {
|
||||||
|
// Return the entity representation
|
||||||
|
return &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovedById: r.ApproverUserID,
|
||||||
|
Action: "updated", // Default action
|
||||||
|
Message: r.Comments,
|
||||||
|
ProcessedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
approvalWorkflowStepsResponse "web-medols-be/app/module/approval_workflow_steps/response"
|
||||||
|
articlesResponse "web-medols-be/app/module/articles/response"
|
||||||
|
usersResponse "web-medols-be/app/module/users/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogsResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ApprovalFlowId uint `json:"approvalFlowId"`
|
||||||
|
StepOrder int `json:"stepOrder"`
|
||||||
|
StepName string `json:"stepName"`
|
||||||
|
ApprovedById *uint `json:"approvedById"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
ProcessedAt time.Time `json:"processedAt"`
|
||||||
|
UserLevelId uint `json:"userLevelId"`
|
||||||
|
ClientId *string `json:"clientId"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
ApprovedBy *usersResponse.UsersResponse `json:"approvedBy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogsDetailResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ArticleApprovalFlowID uint `json:"articleApprovalFlowId"`
|
||||||
|
WorkflowStepID uint `json:"workflowStepId"`
|
||||||
|
ApproverUserID *uint `json:"approverUserId"`
|
||||||
|
ApprovalStatusID uint `json:"approvalStatusId"`
|
||||||
|
ApprovalStatusName *string `json:"approvalStatusName,omitempty"`
|
||||||
|
Comments *string `json:"comments"`
|
||||||
|
ApprovedAt *time.Time `json:"approvedAt"`
|
||||||
|
DueDate *time.Time `json:"dueDate"`
|
||||||
|
IsAutoApproved bool `json:"isAutoApproved"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
|
// Relations with full details
|
||||||
|
WorkflowStep *approvalWorkflowStepsResponse.ApprovalWorkflowStepsResponse `json:"workflowStep,omitempty"`
|
||||||
|
ApproverUser *usersResponse.UsersResponse `json:"approverUser,omitempty"`
|
||||||
|
Article *articlesResponse.ArticlesResponse `json:"article,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleApprovalStepLogsSummaryResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
WorkflowStepID uint `json:"workflowStepId"`
|
||||||
|
StepName string `json:"stepName"`
|
||||||
|
ApproverUserID *uint `json:"approverUserId"`
|
||||||
|
ApproverUserName *string `json:"approverUserName,omitempty"`
|
||||||
|
ApprovalStatusID uint `json:"approvalStatusId"`
|
||||||
|
ApprovalStatusName string `json:"approvalStatusName"`
|
||||||
|
ApprovedAt *time.Time `json:"approvedAt"`
|
||||||
|
DueDate *time.Time `json:"dueDate"`
|
||||||
|
IsAutoApproved bool `json:"isAutoApproved"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalHistoryResponse struct {
|
||||||
|
ArticleID uint `json:"articleId"`
|
||||||
|
ArticleTitle string `json:"articleTitle"`
|
||||||
|
WorkflowName string `json:"workflowName"`
|
||||||
|
StartedAt time.Time `json:"startedAt"`
|
||||||
|
CompletedAt *time.Time `json:"completedAt"`
|
||||||
|
CurrentStep *string `json:"currentStep"`
|
||||||
|
Steps []ArticleApprovalStepLogsSummaryResponse `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalStepStatsResponse struct {
|
||||||
|
StepName string `json:"stepName"`
|
||||||
|
TotalApprovals int `json:"totalApprovals"`
|
||||||
|
PendingApprovals int `json:"pendingApprovals"`
|
||||||
|
ApprovedCount int `json:"approvedCount"`
|
||||||
|
RejectedCount int `json:"rejectedCount"`
|
||||||
|
AutoApprovedCount int `json:"autoApprovedCount"`
|
||||||
|
AverageProcessingTime float64 `json:"averageProcessingTime"` // in hours
|
||||||
|
OverdueCount int `json:"overdueCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserApprovalStatsResponse struct {
|
||||||
|
UserID uint `json:"userId"`
|
||||||
|
UserName string `json:"userName"`
|
||||||
|
TotalAssigned int `json:"totalAssigned"`
|
||||||
|
PendingApprovals int `json:"pendingApprovals"`
|
||||||
|
CompletedApprovals int `json:"completedApprovals"`
|
||||||
|
ApprovedCount int `json:"approvedCount"`
|
||||||
|
RejectedCount int `json:"rejectedCount"`
|
||||||
|
AverageProcessingTime float64 `json:"averageProcessingTime"` // in hours
|
||||||
|
OverdueCount int `json:"overdueCount"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"web-medols-be/app/database/entity"
|
||||||
|
stepRepo "web-medols-be/app/module/approval_workflow_steps/repository"
|
||||||
|
flowRepo "web-medols-be/app/module/article_approval_flows/repository"
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/repository"
|
||||||
|
"web-medols-be/app/module/article_approval_step_logs/request"
|
||||||
|
"web-medols-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type articleApprovalStepLogsService struct {
|
||||||
|
ArticleApprovalStepLogsRepository repository.ArticleApprovalStepLogsRepository
|
||||||
|
ArticleApprovalFlowsRepository flowRepo.ArticleApprovalFlowsRepository
|
||||||
|
ApprovalWorkflowStepsRepository stepRepo.ApprovalWorkflowStepsRepository
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArticleApprovalStepLogsService define interface of IArticleApprovalStepLogsService
|
||||||
|
type ArticleApprovalStepLogsService interface {
|
||||||
|
// Basic CRUD
|
||||||
|
GetAll(clientId *uuid.UUID, req request.ArticleApprovalStepLogsQueryRequest) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error)
|
||||||
|
FindOne(clientId *uuid.UUID, id uint) (log *entity.ArticleApprovalStepLogs, err error)
|
||||||
|
Create(clientId *uuid.UUID, log *entity.ArticleApprovalStepLogs) (logReturn *entity.ArticleApprovalStepLogs, err error)
|
||||||
|
Update(clientId *uuid.UUID, id uint, log *entity.ArticleApprovalStepLogs) (err error)
|
||||||
|
Delete(clientId *uuid.UUID, id uint) (err error)
|
||||||
|
|
||||||
|
// Approval process management
|
||||||
|
GetByApprovalFlowID(clientId *uuid.UUID, flowID uint) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
GetByWorkflowStepID(clientId *uuid.UUID, stepID uint) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
GetByApproverUserID(clientId *uuid.UUID, userID uint) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
GetPendingApprovals(clientId *uuid.UUID, userID *uint, roleID *uint) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
GetOverdueApprovals(clientId *uuid.UUID, userID *uint) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
|
||||||
|
// Approval actions
|
||||||
|
ProcessApproval(clientId *uuid.UUID, logID uint, userID uint, statusID uint, comments *string) (err error)
|
||||||
|
BulkProcessApproval(clientId *uuid.UUID, logIDs []uint, userID uint, statusID uint, comments *string) (err error)
|
||||||
|
AutoApprove(clientId *uuid.UUID, logID uint, reason string) (err error)
|
||||||
|
|
||||||
|
// History and analytics
|
||||||
|
GetApprovalHistory(clientId *uuid.UUID, articleID *uint, userID *uint, filters map[string]interface{}) (logs []*entity.ArticleApprovalStepLogs, err error)
|
||||||
|
GetApprovalStats(clientId *uuid.UUID, filters map[string]interface{}) (stats map[string]interface{}, err error)
|
||||||
|
GetUserWorkload(clientId *uuid.UUID, userID uint, includeStats bool) (workload map[string]interface{}, err error)
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
ValidateStepLog(clientId *uuid.UUID, log *entity.ArticleApprovalStepLogs) (isValid bool, errors []string, err error)
|
||||||
|
CanProcessApproval(clientId *uuid.UUID, logID uint, userID uint) (canProcess bool, reason string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArticleApprovalStepLogsService(
|
||||||
|
articleApprovalStepLogsRepository repository.ArticleApprovalStepLogsRepository,
|
||||||
|
articleApprovalFlowsRepository flowRepo.ArticleApprovalFlowsRepository,
|
||||||
|
approvalWorkflowStepsRepository stepRepo.ApprovalWorkflowStepsRepository,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) ArticleApprovalStepLogsService {
|
||||||
|
return &articleApprovalStepLogsService{
|
||||||
|
ArticleApprovalStepLogsRepository: articleApprovalStepLogsRepository,
|
||||||
|
ArticleApprovalFlowsRepository: articleApprovalFlowsRepository,
|
||||||
|
ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetAll(clientId *uuid.UUID, req request.ArticleApprovalStepLogsQueryRequest) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error) {
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.GetAll(clientId, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) FindOne(clientId *uuid.UUID, id uint) (log *entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.FindOne(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) Create(clientId *uuid.UUID, log *entity.ArticleApprovalStepLogs) (logReturn *entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
// Validate business rules
|
||||||
|
if log.ApprovalFlowId == 0 {
|
||||||
|
return nil, errors.New("approval flow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate approval flow exists
|
||||||
|
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, log.ApprovalFlowId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("approval flow not found: %w", err)
|
||||||
|
}
|
||||||
|
if flow == nil {
|
||||||
|
return nil, errors.New("approval flow not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate step log data
|
||||||
|
isValid, validationErrors, err := _i.ValidateStepLog(clientId, log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isValid {
|
||||||
|
return nil, fmt.Errorf("validation failed: %v", validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.Create(clientId, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) Update(clientId *uuid.UUID, id uint, log *entity.ArticleApprovalStepLogs) (err error) {
|
||||||
|
// Check if log exists
|
||||||
|
existingLog, err := _i.ArticleApprovalStepLogsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existingLog == nil {
|
||||||
|
return errors.New("step log not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.Update(id, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) Delete(clientId *uuid.UUID, id uint) (err error) {
|
||||||
|
// Check if log exists
|
||||||
|
existingLog, err := _i.ArticleApprovalStepLogsRepository.FindOne(clientId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existingLog == nil {
|
||||||
|
return errors.New("step log not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.Delete(clientId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetByApprovalFlowID(clientId *uuid.UUID, flowID uint) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.GetByApprovalFlowId(clientId, flowID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetByWorkflowStepID(clientId *uuid.UUID, stepID uint) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
// This method is not implemented in repository, return empty slice for now
|
||||||
|
return []*entity.ArticleApprovalStepLogs{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetByApproverUserID(clientId *uuid.UUID, userID uint) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
// This method is not implemented in repository, return empty slice for now
|
||||||
|
return []*entity.ArticleApprovalStepLogs{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetPendingApprovals(clientId *uuid.UUID, userID *uint, roleID *uint) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
// This method is not implemented in repository, return empty slice for now
|
||||||
|
return []*entity.ArticleApprovalStepLogs{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetOverdueApprovals(clientId *uuid.UUID, userID *uint) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
// This method is not implemented in repository, return empty slice for now
|
||||||
|
return []*entity.ArticleApprovalStepLogs{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) ProcessApproval(clientId *uuid.UUID, logID uint, userID uint, statusID uint, comments *string) (err error) {
|
||||||
|
// Check if user can process this approval
|
||||||
|
canProcess, reason, err := _i.CanProcessApproval(clientId, logID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !canProcess {
|
||||||
|
return fmt.Errorf("cannot process approval: %s", reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the step log
|
||||||
|
now := time.Now()
|
||||||
|
updateLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovedById: &userID,
|
||||||
|
Action: "approve", // This should be determined based on statusID
|
||||||
|
Message: comments,
|
||||||
|
ProcessedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.Update(logID, updateLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) BulkProcessApproval(clientId *uuid.UUID, logIDs []uint, userID uint, statusID uint, comments *string) (err error) {
|
||||||
|
// Validate all logs can be processed by this user
|
||||||
|
for _, logID := range logIDs {
|
||||||
|
canProcess, reason, err := _i.CanProcessApproval(clientId, logID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !canProcess {
|
||||||
|
return fmt.Errorf("cannot process approval for log %d: %s", logID, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: BulkUpdate method is not available in repository
|
||||||
|
// This functionality would need to be implemented when repository is updated
|
||||||
|
// For now, we'll process each log individually
|
||||||
|
now := time.Now()
|
||||||
|
for _, logID := range logIDs {
|
||||||
|
updateLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
ApprovedById: &userID,
|
||||||
|
Action: "approve",
|
||||||
|
Message: comments,
|
||||||
|
ProcessedAt: now,
|
||||||
|
}
|
||||||
|
err := _i.ArticleApprovalStepLogsRepository.Update(logID, updateLog)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) AutoApprove(clientId *uuid.UUID, logID uint, reason string) (err error) {
|
||||||
|
// Get the step log
|
||||||
|
log, err := _i.ArticleApprovalStepLogsRepository.FindOne(clientId, logID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if log == nil {
|
||||||
|
return errors.New("step log not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: WorkflowStepID and AutoApprove fields are not available in current entity structure
|
||||||
|
// This functionality would need to be implemented when entity is updated
|
||||||
|
|
||||||
|
// Auto approve with current entity structure
|
||||||
|
now := time.Now()
|
||||||
|
updateLog := &entity.ArticleApprovalStepLogs{
|
||||||
|
Action: "approve",
|
||||||
|
Message: &reason,
|
||||||
|
ProcessedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
return _i.ArticleApprovalStepLogsRepository.Update(logID, updateLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetApprovalHistory(clientId *uuid.UUID, articleID *uint, userID *uint, filters map[string]interface{}) (logs []*entity.ArticleApprovalStepLogs, err error) {
|
||||||
|
// This method signature doesn't match repository, return empty slice for now
|
||||||
|
return []*entity.ArticleApprovalStepLogs{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetApprovalStats(clientId *uuid.UUID, filters map[string]interface{}) (stats map[string]interface{}, err error) {
|
||||||
|
// This method is not implemented in repository, return empty map for now
|
||||||
|
return make(map[string]interface{}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) GetUserWorkload(clientId *uuid.UUID, userID uint, includeStats bool) (workload map[string]interface{}, err error) {
|
||||||
|
// This method is not implemented in repository, return empty map for now
|
||||||
|
return make(map[string]interface{}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) ValidateStepLog(clientId *uuid.UUID, log *entity.ArticleApprovalStepLogs) (isValid bool, errors []string, err error) {
|
||||||
|
var validationErrors []string
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if log.ApprovalFlowId == 0 {
|
||||||
|
validationErrors = append(validationErrors, "approval flow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: WorkflowStepID field is not available in current entity structure
|
||||||
|
// This validation would need to be implemented when entity is updated
|
||||||
|
|
||||||
|
// Note: ApprovalStatusID field is not available in current entity structure
|
||||||
|
// This validation would need to be implemented when entity is updated
|
||||||
|
|
||||||
|
// Validate message length if provided
|
||||||
|
if log.Message != nil && len(*log.Message) > 1000 {
|
||||||
|
validationErrors = append(validationErrors, "message must not exceed 1000 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: DueDate field is not available in current entity structure
|
||||||
|
// This validation would need to be implemented when entity is updated
|
||||||
|
|
||||||
|
return len(validationErrors) == 0, validationErrors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *articleApprovalStepLogsService) CanProcessApproval(clientId *uuid.UUID, logID uint, userID uint) (canProcess bool, reason string, err error) {
|
||||||
|
// Get the step log
|
||||||
|
log, err := _i.ArticleApprovalStepLogsRepository.FindOne(clientId, logID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if log == nil {
|
||||||
|
return false, "step log not found", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already processed (using ProcessedAt field)
|
||||||
|
if !log.ProcessedAt.IsZero() {
|
||||||
|
return false, "approval already processed", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has permission to approve this step
|
||||||
|
// This would require checking user roles against step approver role
|
||||||
|
// For now, we'll allow any user to process
|
||||||
|
// TODO: Implement proper role-based authorization
|
||||||
|
|
||||||
|
// Note: DueDate field is not available in current entity structure
|
||||||
|
// This check would need to be implemented when entity is updated
|
||||||
|
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package article_approvals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"web-medols-be/app/module/article_approvals/controller"
|
||||||
|
"web-medols-be/app/module/article_approvals/repository"
|
||||||
|
"web-medols-be/app/module/article_approvals/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// struct of ArticleApprovalsRouter
|
||||||
|
type ArticleApprovalsRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller *controller.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// register bulky of ArticleApprovals module
|
||||||
|
var NewArticleApprovalsModule = fx.Options(
|
||||||
|
// register repository of ArticleApprovals module
|
||||||
|
fx.Provide(repository.NewArticleApprovalsRepository),
|
||||||
|
|
||||||
|
// register service of ArticleApprovals module
|
||||||
|
fx.Provide(service.NewArticleApprovalsService),
|
||||||
|
|
||||||
|
// register controller of ArticleApprovals module
|
||||||
|
fx.Provide(controller.NewController),
|
||||||
|
|
||||||
|
// register router of ArticleApprovals module
|
||||||
|
fx.Provide(NewArticleApprovalsRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// init ArticleApprovalsRouter
|
||||||
|
func NewArticleApprovalsRouter(fiber *fiber.App, controller *controller.Controller) *ArticleApprovalsRouter {
|
||||||
|
return &ArticleApprovalsRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register routes of ArticleApprovals module
|
||||||
|
func (_i *ArticleApprovalsRouter) RegisterArticleApprovalsRoutes() {
|
||||||
|
// define controllers
|
||||||
|
articleApprovalsController := _i.Controller.ArticleApprovals
|
||||||
|
|
||||||
|
// define routes
|
||||||
|
_i.App.Route("/article-approvals", func(router fiber.Router) {
|
||||||
|
router.Get("/", articleApprovalsController.All)
|
||||||
|
router.Get("/:id", articleApprovalsController.Show)
|
||||||
|
router.Post("/", articleApprovalsController.Save)
|
||||||
|
router.Put("/:id", articleApprovalsController.Update)
|
||||||
|
router.Delete("/:id", articleApprovalsController.Delete)
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue