diff --git a/app/module/article_approval_flows/article_approval_flows.module.go b/app/module/article_approval_flows/article_approval_flows.module.go index 75d3bf5..ce09238 100644 --- a/app/module/article_approval_flows/article_approval_flows.module.go +++ b/app/module/article_approval_flows/article_approval_flows.module.go @@ -58,8 +58,6 @@ func (_i *ArticleApprovalFlowsRouter) RegisterArticleApprovalFlowsRoutes() { router.Post("/:id/multi-branch-approve", articleApprovalFlowsController.ProcessMultiBranchApproval) router.Post("/articles/:articleId/approve", articleApprovalFlowsController.ApproveArticleByFlow) 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) }) } diff --git a/app/module/article_approval_flows/controller/article_approval_flows.controller.go b/app/module/article_approval_flows/controller/article_approval_flows.controller.go index 20898a3..b81d8f3 100644 --- a/app/module/article_approval_flows/controller/article_approval_flows.controller.go +++ b/app/module/article_approval_flows/controller/article_approval_flows.controller.go @@ -1,6 +1,7 @@ package controller import ( + "fmt" "netidhub-saas-be/app/module/article_approval_flows/request" "netidhub-saas-be/app/module/article_approval_flows/service" usersRepository "netidhub-saas-be/app/module/users/repository" @@ -26,8 +27,6 @@ type ArticleApprovalFlowsController interface { 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 @@ -237,110 +236,6 @@ func (_i *articleApprovalFlowsController) Approve(c *fiber.Ctx) error { }) } -// Reject ArticleApprovalFlows -// @Summary Reject article -// @Description API for rejecting article -// @Tags ArticleApprovalFlows -// @Security Bearer -// @Param Authorization header string true "Insert the Authorization" -// @Param Authorization header string false "Insert your access token" default(Bearer ) -// @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 Authorization token from header - 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(authToken, 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 Authorization header string true "Insert the Authorization" -// @Param Authorization header string false "Insert your access token" default(Bearer ) -// @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 Authorization token from header - 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(authToken, 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 @@ -801,17 +696,18 @@ func (_i *articleApprovalFlowsController) ApproveArticleByFlow(c *fiber.Ctx) err } // Process approval using multi-branch logic - err = _i.articleApprovalFlowsService.ProcessMultiBranchApproval(authToken, activeFlow.ID, user.ID, req.Message) + err = _i.articleApprovalFlowsService.ProcessApprovalAction(authToken, activeFlow.ID, req.Action, user.ID, req.Message) if err != nil { return err } return utilRes.Resp(c, utilRes.Response{ Success: true, - Messages: utilRes.Messages{"Article successfully approved through active approval flow"}, + Messages: utilRes.Messages{fmt.Sprintf("Article successfully %s through active approval flow", req.Action)}, Data: map[string]interface{}{ "article_id": articleId, "flow_id": activeFlow.ID, + "action": req.Action, "current_step": activeFlow.CurrentStep, "current_branch": activeFlow.CurrentBranch, "workflow_id": activeFlow.WorkflowId, diff --git a/app/module/article_approval_flows/request/article_approval_flows.request.go b/app/module/article_approval_flows/request/article_approval_flows.request.go index cde9848..10bb0dd 100644 --- a/app/module/article_approval_flows/request/article_approval_flows.request.go +++ b/app/module/article_approval_flows/request/article_approval_flows.request.go @@ -27,6 +27,7 @@ type SubmitForApprovalRequest struct { } type ApprovalActionRequest struct { + Action string `json:"action" validate:"required,oneof=approve revision reject"` Message string `json:"message"` } diff --git a/app/module/article_approval_flows/service/article_approval_flows.service.go b/app/module/article_approval_flows/service/article_approval_flows.service.go index 61899cc..57d11e0 100644 --- a/app/module/article_approval_flows/service/article_approval_flows.service.go +++ b/app/module/article_approval_flows/service/article_approval_flows.service.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "errors" + "fmt" "netidhub-saas-be/app/database/entity" approvalWorkflowStepsRepo "netidhub-saas-be/app/module/approval_workflow_steps/repository" approvalWorkflowsRepo "netidhub-saas-be/app/module/approval_workflows/repository" @@ -41,8 +42,6 @@ type ArticleApprovalFlowsService interface { // Article submission and approval workflow SubmitArticleForApproval(authToken string, articleId uint, submittedById uint, workflowId *uint) (flow *entity.ArticleApprovalFlows, err error) ApproveStep(authToken string, flowId uint, approvedById uint, message string) (err error) - RejectArticle(authToken string, flowId uint, rejectedById uint, reason string) (err error) - RequestRevision(authToken string, flowId uint, requestedById uint, revisionMessage string) (err error) ResubmitAfterRevision(authToken string, flowId uint, resubmittedById uint) (err error) // Dashboard and queue methods @@ -63,6 +62,7 @@ type ArticleApprovalFlowsService interface { // Multi-branch support methods ProcessMultiBranchApproval(authToken string, flowId uint, approvedById uint, message string) (err error) + ProcessApprovalAction(authToken string, flowId uint, action string, actionById uint, message string) (err error) FindNextStepsForBranch(authToken string, workflowId uint, currentStep int, submitterLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error) IsStepApplicableForLevel(step *entity.ApprovalWorkflowSteps, submitterLevelId uint) (isApplicable bool, err error) ProcessParallelBranches(authToken string, flow *entity.ArticleApprovalFlows, nextSteps []*entity.ApprovalWorkflowSteps, approvedById uint, message string) (err error) @@ -567,169 +567,6 @@ func (_i *articleApprovalFlowsService) ApproveStep(authToken string, flowId uint return nil } -func (_i *articleApprovalFlowsService) RejectArticle(authToken string, flowId uint, rejectedById uint, reason string) (err error) { - // Extract clientId from authToken - var clientId *uuid.UUID - if authToken != "" { - user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) - if user != nil && user.ClientId != nil { - clientId = user.ClientId - _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") - } - } - - if clientId == nil { - return errors.New("clientId not found in auth token") - } - - // 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(authToken string, flowId uint, requestedById uint, revisionMessage string) (err error) { - // Extract clientId from authToken - var clientId *uuid.UUID - if authToken != "" { - user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) - if user != nil && user.ClientId != nil { - clientId = user.ClientId - _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") - } - } - - if clientId == nil { - return errors.New("clientId not found in auth token") - } - - // 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(authToken string, flowId uint, resubmittedById uint) (err error) { // Extract clientId from authToken var clientId *uuid.UUID @@ -1382,3 +1219,166 @@ func (_i *articleApprovalFlowsService) GetUserLevelId(authToken string, userId u func (_i *articleApprovalFlowsService) FindActiveByArticleId(articleId uint) (flow *entity.ArticleApprovalFlows, err error) { return _i.ArticleApprovalFlowsRepository.FindActiveByArticleId(articleId) } + +// ProcessApprovalAction processes different approval actions (approve, request_update, reject) +func (_i *articleApprovalFlowsService) ProcessApprovalAction(authToken string, flowId uint, action string, actionById uint, message string) (err error) { + _i.Log.Info(). + Uint("flowId", flowId). + Str("action", action). + Uint("actionById", actionById). + Str("message", message). + Msg("Processing approval action") + + // Extract clientId from authToken + var clientId *uuid.UUID + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) + if user != nil && user.ClientId != nil { + clientId = user.ClientId + } + } + + // Get the approval flow + flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) + if err != nil { + return fmt.Errorf("failed to find approval flow: %w", err) + } + + // Get the article + article, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return fmt.Errorf("failed to find article: %w", err) + } + + switch action { + case "approve": + return _i.processApproveAction(authToken, flow, article, actionById, message) + case "revision": + return _i.processRequestUpdateAction(authToken, flow, article, actionById, message) + case "reject": + return _i.processRejectAction(authToken, flow, article, actionById, message) + default: + return fmt.Errorf("invalid action: %s", action) + } +} + +// processApproveAction handles approve action - moves to next step +func (_i *articleApprovalFlowsService) processApproveAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error { + _i.Log.Info().Uint("flowId", flow.ID).Msg("Processing approve action") + + // Use existing multi-branch approval logic + return _i.ProcessMultiBranchApproval(authToken, flow.ID, actionById, message) +} + +// processRequestUpdateAction handles request update action - moves back 1 step +func (_i *articleApprovalFlowsService) processRequestUpdateAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error { + _i.Log.Info().Uint("flowId", flow.ID).Msg("Processing request update action") + + var clientId *uuid.UUID + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) + if user != nil && user.ClientId != nil { + clientId = user.ClientId + } + } + + // Reset to initial step (step 1) and status pending + flow.CurrentStep = 1 + flow.StatusId = 1 // pending - seperti belum berjalan flow approval + flow.RevisionRequested = &[]bool{true}[0] + flow.RevisionMessage = &message + + err := _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) + if err != nil { + return fmt.Errorf("failed to update flow for request update: %w", err) + } + + // Update article status to draft for revision + article.StatusId = &[]int{1}[0] // 1 = draft + article.IsDraft = &[]bool{true}[0] + article.WorkflowId = &flow.WorkflowId // Keep workflow ID for restart + article.CurrentApprovalStep = &[]int{1}[0] // Reset to step 1 + + err = _i.ArticlesRepository.Update(clientId, article.ID, article) + if err != nil { + return fmt.Errorf("failed to update article for revision: %w", err) + } + + // Create step log + stepLog := &entity.ArticleApprovalStepLogs{ + ApprovalFlowId: flow.ID, + StepOrder: flow.CurrentStep, + Action: "revision", + ApprovedById: &actionById, + Message: &message, + CreatedAt: time.Now(), + } + + _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to create step log for request update") + } + + _i.Log.Info(). + Uint("flowId", flow.ID). + Int("resetStep", 1). + Msg("Revision request processed - flow reset to step 1") + + return nil +} + +// processRejectAction handles reject action - returns to draft status +func (_i *articleApprovalFlowsService) processRejectAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error { + _i.Log.Info().Uint("flowId", flow.ID).Msg("Processing reject action") + + var clientId *uuid.UUID + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) + if user != nil && user.ClientId != nil { + clientId = user.ClientId + } + } + + // Reset to initial step (step 1) and status pending + flow.CurrentStep = 1 + flow.StatusId = 1 // pending - seperti belum berjalan flow approval + flow.RevisionMessage = &message // Use same field for consistency + // Don't set completedAt - flow is not completed, just reset + + err := _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) + if err != nil { + return fmt.Errorf("failed to update flow for rejection: %w", err) + } + // Update article status to draft + article.StatusId = &[]int{1}[0] // 1 = draft + article.IsDraft = &[]bool{true}[0] + article.WorkflowId = &flow.WorkflowId // Keep workflow ID for restart + article.CurrentApprovalStep = &[]int{1}[0] // Reset to step 1 + + err = _i.ArticlesRepository.Update(clientId, article.ID, article) + if err != nil { + return fmt.Errorf("failed to update article for rejection: %w", err) + } + + // Create step log + stepLog := &entity.ArticleApprovalStepLogs{ + ApprovalFlowId: flow.ID, + StepOrder: flow.CurrentStep, + Action: "reject", + ApprovedById: &actionById, + Message: &message, + CreatedAt: time.Now(), + } + + _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to create step log for rejection") + } + + _i.Log.Info(). + Uint("flowId", flow.ID). + Uint("articleId", article.ID). + Msg("Rejection processed - flow reset to step 1, article returned to draft") + + return nil +} diff --git a/app/module/articles/controller/articles.controller.go b/app/module/articles/controller/articles.controller.go index 82470d4..d4a6498 100644 --- a/app/module/articles/controller/articles.controller.go +++ b/app/module/articles/controller/articles.controller.go @@ -233,6 +233,7 @@ func (_i *articlesController) SaveThumbnail(c *fiber.Ctx) error { // @Tags Articles // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param payload body request.ArticlesUpdateRequest true "Required payload" // @Param id path int true "Articles ID" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError diff --git a/docs/ENHANCED_APPROVAL_ACTIONS_API.md b/docs/ENHANCED_APPROVAL_ACTIONS_API.md new file mode 100644 index 0000000..2838668 --- /dev/null +++ b/docs/ENHANCED_APPROVAL_ACTIONS_API.md @@ -0,0 +1,286 @@ +# Enhanced Approval Actions API + +## ๐Ÿš€ **Overview** + +API ini telah ditingkatkan untuk mendukung 3 jenis action approval yang berbeda: **Approve**, **Request Update**, dan **Reject**. Setiap action memiliki behavior yang berbeda sesuai dengan kebutuhan workflow. + +## ๐Ÿ“‹ **Endpoint** + +``` +POST /api/article-approval-flows/articles/{articleId}/approve +``` + +## ๐Ÿ”ง **Request Body** + +```json +{ + "action": "approve", + "message": "Content looks good, approved for next level" +} +``` + +## ๐Ÿ“ **Action Types** + +### **1. Approve (`approve`)** +- **Behavior**: Naik ke step berikutnya dalam workflow +- **Use Case**: Content sudah sesuai dan siap untuk step selanjutnya +- **Result**: Article akan diproses menggunakan multi-branch logic + +### **2. Revision (`revision`)** +- **Behavior**: Mundur 1 step dalam workflow +- **Use Case**: Content perlu diperbaiki tapi masih bisa diperbaiki +- **Result**: Article akan kembali ke step sebelumnya untuk revisi + +### **3. Reject (`reject`)** +- **Behavior**: Balik ke status Draft +- **Use Case**: Content tidak sesuai dan perlu dibuat ulang +- **Result**: Article akan dikembalikan ke status draft dan workflow dihentikan + +## ๐Ÿ“ค **Response Examples** + +### **Approve Response (200):** +```json +{ + "success": true, + "messages": ["Article successfully approve through active approval flow"], + "data": { + "article_id": 123, + "flow_id": 456, + "action": "approve", + "current_step": 2, + "current_branch": "Branch_A", + "workflow_id": 1 + } +} +``` + +### **Revision Response (200):** +```json +{ + "success": true, + "messages": ["Article successfully revision through active approval flow"], + "data": { + "article_id": 123, + "flow_id": 456, + "action": "revision", + "current_step": 1, + "current_branch": "Branch_A", + "workflow_id": 1 + } +} +``` + +### **Reject Response (200):** +```json +{ + "success": true, + "messages": ["Article successfully reject through active approval flow"], + "data": { + "article_id": 123, + "flow_id": 456, + "action": "reject", + "current_step": 1, + "current_branch": "Branch_A", + "workflow_id": 1 + } +} +``` + +## ๐Ÿงช **Curl Examples** + +### **1. Approve Article:** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "action": "approve", + "message": "Content reviewed and approved. Ready for next level." + }' +``` + +### **2. Request Revision:** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "action": "revision", + "message": "Please fix the grammar and improve the conclusion section." + }' +``` + +### **3. Reject Article:** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "action": "reject", + "message": "Content does not meet our quality standards. Please rewrite the entire article." + }' +``` + +## ๐Ÿ”„ **Workflow Behavior** + +### **Approve Flow:** +1. **Input**: `{"action": "approve", "message": "..."}` +2. **Process**: Menggunakan multi-branch approval logic +3. **Result**: Article naik ke step berikutnya +4. **Status**: Workflow berlanjut + +### **Revision Flow:** +1. **Input**: `{"action": "revision", "message": "..."}` +2. **Process**: + - Current step dikurangi 1 + - `revision_requested` = true + - `revision_message` = message +3. **Result**: Article kembali ke step sebelumnya +4. **Status**: Workflow berlanjut tapi mundur 1 step + +### **Reject Flow:** +1. **Input**: `{"action": "reject", "message": "..."}` +2. **Process**: + - Flow status = rejected (3) + - Article status = draft (1) + - `is_draft` = true + - `workflow_id` = null + - `current_approval_step` = null +3. **Result**: Article dikembalikan ke draft +4. **Status**: Workflow dihentikan + +## ๐Ÿ“Š **Database Changes** + +### **Article Approval Flows Table:** +- `revision_requested`: Boolean flag untuk request update +- `revision_message`: Pesan untuk revisi +- `rejection_reason`: Alasan penolakan +- `completed_at`: Timestamp ketika workflow selesai + +### **Articles Table:** +- `status_id`: 1 (draft), 2 (published), 3 (rejected) +- `is_draft`: Boolean flag untuk draft status +- `workflow_id`: Null ketika di-reject +- `current_approval_step`: Null ketika di-reject + +### **Article Approval Step Logs Table:** +- `action`: "approve", "request_update", "reject" +- `message`: Pesan dari approver +- `action_by_id`: ID user yang melakukan action + +## ๐ŸŽฏ **Use Cases** + +### **1. Content Review Process:** +```bash +# Step 1: Editor review +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer EDITOR_TOKEN" \ + -d '{"action": "approve", "message": "Content is good"}' + +# Step 2: Senior Editor review +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \ + -d '{"action": "request_update", "message": "Please add more examples"}' + +# Step 3: After revision, approve again +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \ + -d '{"action": "approve", "message": "Much better now"}' +``` + +### **2. Quality Control Process:** +```bash +# Quality check fails +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer QUALITY_CHECKER_TOKEN" \ + -d '{"action": "reject", "message": "Content does not meet quality standards"}' +``` + +## ๐Ÿ”’ **Validation Rules** + +### **Action Validation:** +- `action` harus salah satu dari: `approve`, `request_update`, `reject` +- `action` adalah field required + +### **Message Validation:** +- `message` adalah optional +- Jika ada, akan disimpan dalam step log + +### **Authorization Validation:** +- User harus memiliki permission untuk approve +- User level harus sesuai dengan step yang sedang berjalan + +## ๐Ÿšจ **Error Handling** + +### **Invalid Action (400):** +```json +{ + "success": false, + "messages": ["Validation failed: [Action must be one of: approve, request_update, reject]"] +} +``` + +### **No Active Flow (400):** +```json +{ + "success": false, + "messages": ["No active approval flow found for this article"] +} +``` + +### **Unauthorized (401):** +```json +{ + "success": false, + "messages": ["Invalid authorization token"] +} +``` + +## ๐Ÿ”„ **Migration from Old API** + +### **Before (Only Approve):** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{"message": "Approved"}' +``` + +### **After (3 Actions):** +```bash +# Approve +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{"action": "approve", "message": "Approved"}' + +# Revision +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{"action": "revision", "message": "Please fix grammar"}' + +# Reject +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{"action": "reject", "message": "Content not suitable"}' +``` + +## ๐ŸŽ‰ **Benefits** + +1. **Flexible Workflow**: Support untuk berbagai jenis feedback +2. **Better User Experience**: User bisa memberikan feedback yang lebih spesifik +3. **Improved Quality Control**: Reject option untuk content yang tidak sesuai +4. **Revision Support**: Revision untuk perbaikan tanpa menghentikan workflow +5. **Audit Trail**: Semua action dicatat dalam step logs + +--- + +**๐ŸŽ‰ Enhanced Approval Actions API memberikan kontrol yang lebih baik atas workflow approval dengan 3 pilihan action yang berbeda!** + +Sekarang approver bisa memilih apakah akan approve, revision, atau reject artikel dengan behavior yang sesuai untuk setiap pilihan. ๐Ÿš€ diff --git a/docs/ENHANCED_APPROVAL_PRACTICAL_GUIDE.md b/docs/ENHANCED_APPROVAL_PRACTICAL_GUIDE.md new file mode 100644 index 0000000..ddb7c41 --- /dev/null +++ b/docs/ENHANCED_APPROVAL_PRACTICAL_GUIDE.md @@ -0,0 +1,423 @@ +# Enhanced Approval Actions - Practical Usage Guide + +## ๐ŸŽฏ **Overview** + +Panduan praktis untuk menggunakan Enhanced Approval Actions API yang mendukung 3 jenis action: **Approve**, **Revision**, dan **Reject**. + +## ๐Ÿ”„ **Action Types & Behavior** + +| Action | Behavior | Use Case | Result | +|--------|----------|----------|---------| +| **`approve`** | Naik ke step berikutnya | Content sudah sesuai | Workflow berlanjut | +| **`revision`** | Mundur 1 step | Content perlu diperbaiki | Workflow mundur 1 step | +| **`reject`** | Balik ke Draft | Content tidak sesuai | Workflow dihentikan, artikel jadi draft | + +## ๐Ÿงช **Practical Examples** + +### **Scenario 1: Normal Approval Flow** + +#### **Step 1: Editor Review** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/10/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer EDITOR_TOKEN" \ + -d '{ + "action": "approve", + "message": "Content is well-written and follows our guidelines" + }' +``` + +**Response:** +```json +{ + "success": true, + "messages": ["Article successfully approve through active approval flow"], + "data": { + "article_id": 10, + "flow_id": 1, + "action": "approve", + "current_step": 2, + "current_branch": "Final_Approval", + "workflow_id": 1 + } +} +``` + +#### **Step 2: Senior Editor Review** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/10/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \ + -d '{ + "action": "approve", + "message": "Final approval granted. Ready for publication." + }' +``` + +**Result:** Article published successfully! ๐ŸŽ‰ + +--- + +### **Scenario 2: Request Update Flow** + +#### **Step 1: Editor Review** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/11/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer EDITOR_TOKEN" \ + -d '{ + "action": "approve", + "message": "Content is good, moving to next level" + }' +``` + +#### **Step 2: Senior Editor Review (Request Revision)** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/11/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \ + -d '{ + "action": "revision", + "message": "Please add more examples in section 3 and improve the conclusion" + }' +``` + +**Response:** +```json +{ + "success": true, + "messages": ["Article successfully revision through active approval flow"], + "data": { + "article_id": 11, + "flow_id": 2, + "action": "revision", + "current_step": 1, + "current_branch": "Branch_A", + "workflow_id": 1 + } +} +``` + +**Result:** Article kembali ke step 1 untuk revisi! ๐Ÿ“ + +#### **Step 3: After Revision, Approve Again** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/11/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer EDITOR_TOKEN" \ + -d '{ + "action": "approve", + "message": "Revisions completed, content improved significantly" + }' +``` + +--- + +### **Scenario 3: Reject Flow** + +#### **Step 1: Editor Review (Reject)** +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/articles/12/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer EDITOR_TOKEN" \ + -d '{ + "action": "reject", + "message": "Content does not meet our quality standards. Please rewrite with better research and clearer structure." + }' +``` + +**Response:** +```json +{ + "success": true, + "messages": ["Article successfully reject through active approval flow"], + "data": { + "article_id": 12, + "flow_id": 3, + "action": "reject", + "current_step": 1, + "current_branch": "Branch_A", + "workflow_id": 1 + } +} +``` + +**Result:** Article dikembalikan ke status Draft! โŒ + +--- + +## ๐Ÿ” **Database State Changes** + +### **After Approve:** +```sql +-- Article Approval Flows +UPDATE article_approval_flows +SET current_step = 2, + updated_at = NOW() +WHERE id = 1; + +-- Article Approval Step Logs +INSERT INTO article_approval_step_logs +(approval_flow_id, step_order, action, action_by_id, message, created_at) +VALUES (1, 1, 'approve', 5, 'Content is well-written', NOW()); +``` + +### **After Revision:** +```sql +-- Article Approval Flows +UPDATE article_approval_flows +SET current_step = 1, + revision_requested = true, + revision_message = 'Please add more examples', + updated_at = NOW() +WHERE id = 2; + +-- Article Approval Step Logs +INSERT INTO article_approval_step_logs +(approval_flow_id, step_order, action, action_by_id, message, created_at) +VALUES (2, 2, 'revision', 6, 'Please add more examples', NOW()); +``` + +### **After Reject:** +```sql +-- Article Approval Flows +UPDATE article_approval_flows +SET status_id = 3, + rejection_reason = 'Content does not meet quality standards', + completed_at = NOW(), + updated_at = NOW() +WHERE id = 3; + +-- Articles +UPDATE articles +SET status_id = 1, + is_draft = true, + workflow_id = NULL, + current_approval_step = NULL, + updated_at = NOW() +WHERE id = 12; + +-- Article Approval Step Logs +INSERT INTO article_approval_step_logs +(approval_flow_id, step_order, action, action_by_id, message, created_at) +VALUES (3, 1, 'reject', 5, 'Content does not meet quality standards', NOW()); +``` + +## ๐ŸŽฏ **Frontend Integration Examples** + +### **React Component:** +```jsx +import React, { useState } from 'react'; + +const ApprovalActions = ({ articleId, onActionComplete }) => { + const [action, setAction] = useState('approve'); + const [message, setMessage] = useState(''); + + const handleSubmit = async () => { + try { + const response = await fetch(`/api/article-approval-flows/articles/${articleId}/approve`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ action, message }) + }); + + const result = await response.json(); + + if (result.success) { + onActionComplete(result.data); + alert(`Article ${action} successfully!`); + } else { + alert(`Error: ${result.messages[0]}`); + } + } catch (error) { + alert('Failed to process approval action'); + } + }; + + return ( +
+

Approval Actions

+ +
+ + + + + +
+ +