Browse Source

object approval action

moell 8 months ago
parent
commit
e31b97ac3f

+ 9 - 1
app/Exceptions/Handler.php

@@ -2,11 +2,14 @@
 
 namespace App\Exceptions;
 
+use App\Http\Controllers\JsonResponseHelper;
 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
 use Throwable;
 
 class Handler extends ExceptionHandler
 {
+    use JsonResponseHelper;
+
     /**
      * The list of the inputs that are never flashed to the session on validation exceptions.
      *
@@ -24,7 +27,12 @@ class Handler extends ExceptionHandler
     public function register(): void
     {
         $this->reportable(function (Throwable $e) {
-            //
+            return match (get_class($e)) {
+                ValidationException::class => false,
+                default => true,
+            };
         });
+
+        $this->renderable(fn(ValidationException $e) => $this->unprocesableEtity(message: $e->getMessage()));
     }
 }

+ 18 - 0
app/Http/Controllers/API/ApprovalController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\API;
 
 use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Approval\ActionRequest;
 use App\Http\Requests\API\Approval\CreateOrUpdateRequest;
 use App\Models\Approval;
 use App\Models\ApprovalFlow;
@@ -10,6 +11,7 @@ use App\Models\Enums\ApprovalFlowObjectType;
 use App\Models\Enums\ApprovalFlowType;
 use App\Models\Enums\ApprovalObjectType;
 use App\Models\Enums\ObjectApprovalStatus;
+use App\Services\Approval\ActionService;
 use App\Services\Approval\StoreService;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
@@ -42,6 +44,22 @@ class ApprovalController extends Controller
         //
     }
 
+    public function action(ActionRequest $request, string $id, ActionService $actionService)
+    {
+        $approval = Approval::query()
+            ->where("users", 'like', '%,'.Auth::id().',%')
+            ->whereIn("status", [
+                ObjectApprovalStatus::DOING->value,
+            ])
+            ->find($id);
+
+        throw_validation_if(! $approval, "No authority for approval");
+
+        $actionService->action($approval, $request->status, $request->get("comment"));
+
+        return $this->noContent();
+    }
+
     /**
      * Update the specified resource in storage.
      */

+ 29 - 0
app/Http/Requests/API/Approval/ActionRequest.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Requests\API\Approval;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ActionRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
+     */
+    public function rules(): array
+    {
+        return [
+            "status" => "required|in:1,2",
+            "comment" => "max:255",
+        ];
+    }
+}

+ 10 - 0
app/Models/Enums/ApprovalObjectType.php

@@ -38,6 +38,16 @@ enum ApprovalObjectType: string
         };
     }
 
+    public function actionObjectType(): ActionObjectType
+    {
+        return match ($this) {
+            self::PROJECT => ActionObjectType::PROJECT,
+            self::TASK => ActionObjectType::TASK,
+            self::REQUIREMENT => ActionObjectType::REQUIREMENT,
+            self::CONTAINER => ActionObjectType::CONTAINER,
+        };
+    }
+
     public function modelBuilderAllowed(string $id = null): \Illuminate\Database\Eloquent\Builder
     {
         return match ($this) {

+ 8 - 0
app/Models/Enums/ObjectAction.php

@@ -78,6 +78,14 @@ enum ObjectAction: string
 
     case WAITED ="waited";
 
+    case APPROVAL_APPROVED = 'approvalApproved';
+
+    case APPROVAL_REJECTED = 'approvalRejected';
+
+    case APPROVED_TO_NEXT_NODE ='approvedToNextStep';
+
+    case APPROVED_CANCELED ='approvedCanceled';
+
     public static function messageNotificationItems()
     {
         return [

+ 4 - 2
app/Models/Enums/ObjectApprovalStatus.php

@@ -8,7 +8,9 @@ enum ObjectApprovalStatus: string
 
     case DOING = 'doing'; //进行中
 
-    case DONE = 'done'; //完成
+    case APPROVED = 'approved'; // 已批准
 
-    case CANCEL = 'cancel'; //取消
+    case REJECTED = 'rejected';
+
+    case CANCELED = 'canceled'; //取消
 }

+ 23 - 0
app/Repositories/ActionRepository.php

@@ -4,9 +4,11 @@ namespace App\Repositories;
 
 use App\Events\ObjectActionCreate;
 use App\Models\Action;
+use App\Models\Approval;
 use App\Models\Asset;
 use App\Models\Container;
 use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\ApprovalObjectType;
 use App\Models\Enums\ObjectAction;
 use App\Models\History;
 use App\Models\File;
@@ -53,6 +55,27 @@ class ActionRepository
         return $action;
     }
 
+    public static function createByApproval(
+        Approval $approval,
+        ObjectAction $action,
+        string $comment = null,
+        array $extraFields = [],
+        array $objectChanges = [],
+    ): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder
+    {
+        $actionObjectType = ApprovalObjectType::from($approval->object_type)->actionObjectType();
+
+        return self::create(
+            $approval->object_id,
+            $actionObjectType,
+            $action,
+            $actionObjectType == ActionObjectType::PROJECT ? $approval->object_id : null,
+            $comment,
+            $extraFields,
+            $objectChanges,
+        );
+    }
+
     public static function createByProject(
         Project $project,
         ObjectAction $action,

+ 80 - 0
app/Services/Approval/ActionService.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace App\Services\Approval;
+
+use App\Models\Approval;
+use App\Models\Enums\ApprovalObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\Enums\ObjectApprovalStatus;
+use App\Repositories\ActionRepository;
+
+class ActionService
+{
+    protected ?Approval $approval = null;
+
+    public function action(Approval $approval, int $status, string $comment = null): void
+    {
+        $this->approval = $approval;
+
+        match($status) {
+            1 => $this->approved(),
+            default => $this->rejected(),
+        };
+
+        $this->triggerAction($comment);
+
+        $this->changeApprovalObjectStatus();
+
+        $this->changeApprovalStatus();
+    }
+
+    protected function changeApprovalStatus(): void
+    {
+        $this->approval->save();
+    }
+
+    protected function changeApprovalObjectStatus(): void
+    {
+        $object = ApprovalObjectType::from($this->approval->object_type)
+            ->modelBuilderAllowed($this->approval->object_id)
+            ->findOrFail($this->approval->object_id);
+
+        $object->approval_status = $this->approval->status;
+        $object->save();
+    }
+
+    protected function triggerAction (string $comment = null): void
+    {
+        $objectAction = match ($this->approval->status) {
+            ObjectApprovalStatus::APPROVED->value => ObjectAction::APPROVAL_APPROVED,
+            ObjectApprovalStatus::DOING->value => ObjectAction::APPROVED_TO_NEXT_NODE,
+            ObjectApprovalStatus::REJECTED->value => ObjectAction::APPROVAL_REJECTED,
+        };
+
+        ActionRepository::createByApproval(
+            $this->approval,
+            $objectAction,
+            $comment
+        );
+    }
+
+    protected function approved(): void
+    {
+        $approvalFlowNodes = $this->approval->approvalFlow?->nodes;
+        $nextNodeIndex = $this->approval->node_level + 1;
+        $nextNodes = $approvalFlowNodes[$nextNodeIndex] ?? [];
+
+        if (! $nextNodes) {
+            $this->approval->status = ObjectApprovalStatus::APPROVED->value;
+            return;
+        }
+
+        $this->approval->node_level = $nextNodeIndex;
+        $this->approval->users = sprintf(",%s,", implode(',', $nextNodes['approval_users']));
+    }
+
+    protected function rejected(): void
+    {
+        $this->approval->status = ObjectApprovalStatus::REJECTED->value;
+    }
+}

+ 28 - 0
database/migrations/2024_06_25_160252_add_status_to_approvals_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('approvals', function (Blueprint $table) {
+            $table->string("status", 10)->default("wait")->after("approval_flow_id");
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('approvals', function (Blueprint $table) {
+            $table->dropColumn(['status']);
+        });
+    }
+};

+ 4 - 0
lang/en/action-labels.php

@@ -43,5 +43,9 @@ return[
         ObjectAction::UNLINK_CHILDREN_TASK->value => "unlinked a child task",
         ObjectAction::BATCH_CREATE_TASK->value => "batch created tasks",
         ObjectAction::DELETE_CHILDREN_TASK->value => "delete children task",
+        ObjectAction::APPROVAL_APPROVED->value => "approval approved",
+        ObjectAction::APPROVAL_REJECTED->value => "approval rejected",
+        ObjectAction::APPROVED_CANCELED->value => "approval canceled",
+        ObjectAction::APPROVED_TO_NEXT_NODE->value => "approved to next step",
     ]
 ];

+ 2 - 0
routes/api.php

@@ -76,6 +76,8 @@ Route::middleware(['auth:sanctum'])->group(function () {
             'index', 'show'
         ]);
 
+        Route::post("approval/{approval}/action", [API\ApprovalController::class, 'action'])->name("approval.action");
+
         Route::get("library-linkage/{type}", [API\LibraryController::class, "linkage"])->name("library.linkage");
 
         Route::get("asset/{asset_id}/report",[API\AssetController::class, 'report'])->name('asset.report');