Browse Source

Merge branch 'task-management' into dev

moell 1 year ago
parent
commit
15023d2817

+ 62 - 0
app/Http/Controllers/API/TaskController.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Task\CreateOrUpdateRequest;
+use App\Http\Resources\API\TaskDetailResource;
+use App\Models\Task;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class TaskController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     */
+    public function store(CreateOrUpdateRequest $request)
+    {
+        Task::create([
+            ...$request->all(),
+            'company_id' => Auth::user()->company_id,
+            'created_by' => Auth::id(),
+            'whitelist' => $request->whitelist ? sprintf(",%s", implode(',', $request->whitelist)) : null,
+        ]);
+
+        return $this->created();
+    }
+
+    /**
+     * Display the specified resource.
+     */
+    public function show(string $id)
+    {
+        $task = Task::query()->findOrFail($id);
+
+        return new TaskDetailResource($task);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     */
+    public function update(Request $request, string $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     */
+    public function destroy(string $id)
+    {
+        //
+    }
+}

+ 66 - 0
app/Http/Requests/API/Task/CreateOrUpdateRequest.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Http\Requests\API\Task;
+
+use App\Http\Requests\RuleHelper;
+use App\Models\Enums\TaskACL;
+use App\Models\User;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Rules\Enum;
+
+class CreateOrUpdateRequest extends FormRequest
+{
+    use RuleHelper;
+
+    /**
+     * 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 [
+            'project_id' => [
+                'required',
+                Rule::exists('projects', 'id')->where($this->userCompanyWhere()),
+            ],
+            'requirement_id' => [
+                'required',
+                Rule::exists('requirements', 'id')->where($this->userCompanyWhere()),
+            ],
+            'naming_rule_id' => [
+                Rule::when($this->get('naming_rule_id') > 0, [
+                    Rule::exists('naming_rules', 'id')->whereIn('company_id', [
+                        0, Auth::user()->company_id,
+                    ]),
+                ])
+            ],
+            'assign' => [
+                Rule::exists('users', 'id')->where($this->userCompanyWhere()),
+            ],
+            'name' => 'required|max:255',
+            'parent_id' => [
+                Rule::when($this->get('parent_id') > 0, [
+                    Rule::exists('tasks', 'id')->where($this->userCompanyWhere())->where("parent_id", 0),
+                ])
+            ],
+            'begin' => 'date',
+            'end' => 'date',
+            'acl' => [
+                new Enum(TaskACL::class),
+            ],
+            'whitelist' => $this->usersCompanyRules(),
+            'mailto' => $this->usersCompanyRules(),
+        ];
+    }
+}

+ 14 - 0
app/Http/Requests/RuleHelper.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Requests;
 
+use App\Models\User;
 use Illuminate\Database\Query\Builder;
 use Illuminate\Support\Facades\Auth;
 
@@ -11,4 +12,17 @@ trait RuleHelper
     {
         return fn (Builder $query) => $query->where('company_id', Auth::user()->company_id);
     }
+
+    protected function usersCompanyRules(): array
+    {
+        return [
+            'array',
+            function ($attribute, $value, $fail) {
+                $userCount = User::where("company_id", Auth::user()->company_id)->whereIn('id', $value)->count();
+                if ($userCount != count($value)) {
+                    $fail('The selected user is invalid.');
+                }
+            }
+        ];
+    }
 }

+ 22 - 0
app/Http/Resources/API/NamingRuleSimpleResource.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class NamingRuleSimpleResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+        ];
+    }
+}

+ 22 - 0
app/Http/Resources/API/ProjectSimpleResource.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ProjectSimpleResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+        ];
+    }
+}

+ 22 - 0
app/Http/Resources/API/RequirementSimpleResource.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class RequirementSimpleResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'title' => $this->title,
+        ];
+    }
+}

+ 55 - 0
app/Http/Resources/API/TaskDetailResource.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class TaskDetailResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            "id" => $this->id,
+            "name" => $this->name,
+            "project_id" => $this->project_id,
+            "project" => new ProjectSimpleResource($this->project),
+            "requirement_id" => $this->requirement_id,
+            "requirement" => new RequirementSimpleResource($this->requirement),
+            "naming_rule_id" => $this->naming_rule_id,
+            "naming_rule" => new NamingRuleSimpleResource($this->namingRule),
+            "parent_id" => $this->parent_id,
+            "task_type" => $this->task_type,
+            "doc_stage" => $this->doc_stage,
+            "doc_type" => $this->doc_type,
+            "status" => $this->status,
+            "assign" => $this->assign,
+            "description" => $this->description,
+            "begin" => $this->begin,
+            "end" => $this->end,
+            "mailto"  => $this->mailto,
+            "email_subject"  => $this->email_subject,
+            "acl"  => $this->acl,
+            "whitelist"  => $this->whitelist,
+            "closed_by" => new UserProfileResource($this->closedBy),
+            "closed_at" => $this->closed_at,
+            "canceled_by" => new UserProfileResource($this->canceledBy),
+            "canceled_at" => $this->canceled_at,
+            "approve_by" => new UserProfileResource($this->approveBy),
+            "approve_at" => $this->approve_at,
+            "finished_by" =>  new UserProfileResource($this->finishedBy),
+            "finished_at" => $this->finished_at,
+            "review_by" => new UserProfileResource($this->reviewBy),
+            "review_at" => $this->review_at,
+            "created_by" => new UserProfileResource($this->createdBy),
+            "custom_fields" => $this->custom_fields,
+            "created_at" => (string)$this->created_at,
+            "updated_at" => (string)$this->updated_at
+        ];
+    }
+}

+ 16 - 0
app/ModelFilters/TaskFilter.php

@@ -0,0 +1,16 @@
+<?php 
+
+namespace App\ModelFilters;
+
+use EloquentFilter\ModelFilter;
+
+class TaskFilter extends ModelFilter
+{
+    /**
+    * Related Models that have ModelFilters as well as the method on the ModelFilter
+    * As [relationMethod => [input_key1, input_key2]].
+    *
+    * @var array
+    */
+    public $relations = [];
+}

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

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum TaskACL: string
+{
+    case PRIVATE = 'private';
+
+    case CUSTOM = 'custom';
+}

+ 18 - 0
app/Models/Enums/TaskStatus.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum TaskStatus: string
+{
+    case WAIT = 'wait'; //未开始
+
+    case DOING = 'doing'; //进行中
+
+    case DONE = 'done'; //完成
+
+    case PAUSE = 'pause'; //暂停
+
+    case CANCEL = 'cancel'; //取消
+
+    case CLOSED = 'closed'; //关闭
+}

+ 73 - 0
app/Models/Task.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Models;
+
+use App\Models\Scopes\CompanyScope;
+use EloquentFilter\Filterable;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Task extends Model
+{
+    use HasFactory, Filterable;
+
+    protected $guarded = [
+        'id'
+    ];
+
+    protected $casts = [
+        'mailto' => 'array',
+        'custom_fields' => 'array',
+    ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope(new CompanyScope);
+    }
+
+    public function requirement(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(Requirement::class);
+    }
+
+    public function project(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(Project::class);
+    }
+
+    public function namingRule(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(NamingRule::class);
+    }
+
+    public function createdBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'created_by');
+    }
+
+    public function reviewBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'review_by');
+    }
+
+    public function finishedBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'finished_by');
+    }
+
+    public function approveBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'approve_by');
+    }
+
+    public function canceledBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'canceled_by');
+    }
+
+    public function closedBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'closed_by');
+    }
+
+}

+ 46 - 0
database/factories/TaskFactory.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Enums\TaskACL;
+use App\Models\Enums\TaskStatus;
+use App\Models\Project;
+use App\Models\Requirement;
+use App\Models\User;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Auth;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Task>
+ */
+class TaskFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'project_id' => Project::factory()->create(),
+            'requirement_id' => Requirement::factory()->create(),
+            'naming_rule_id' => 0,
+            'name' => fake()->title(),
+            'status' => TaskStatus::WAIT->value,
+            'parent_id' => 0,
+            'description' => fake()->text(),
+            'begin' => Carbon::now()->toDateString(),
+            'end' => Carbon::now()->addMonth()->toDateString(),
+            'email_subject' => fake()->title(),
+            'acl' => TaskACL::PRIVATE->value,
+            'whitelist' => ',1,',
+            'created_by' => Auth::id(),
+            'company_id' => Auth::user()->company_id,
+            'mailto' => [
+                Auth::id(),
+            ],
+        ];
+    }
+}

+ 1 - 1
database/migrations/2024_02_18_061942_rome_avatar_add_gender_address_to_users.php

@@ -16,7 +16,6 @@ return new class extends Migration
             $table->integer('gender')->nullable()->comment('男0,女1');
             $table->string('address',250)->nullable();
             $table->integer('department_id')->nullable()->comment('部门id');
-
         });
     }
 
@@ -26,6 +25,7 @@ return new class extends Migration
     public function down(): void
     {
         Schema::table('users', function (Blueprint $table) {
+            $table->string('avatar', 250)->nullable();
             $table->dropColumn(['gender', 'address', 'department_id']);
         });
     }

+ 58 - 0
database/migrations/2024_02_23_122917_create_tasks_table.php

@@ -0,0 +1,58 @@
+<?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::create('tasks', function (Blueprint $table) {
+            $table->id();
+            $table->string("name", 150);
+            $table->integer("project_id");
+            $table->integer("company_id");
+            $table->integer("requirement_id")->nullable();
+            $table->integer("naming_rule_id")->nullable();
+            $table->integer("parent_id");
+            $table->string("task_type", 50)->nullable();
+            $table->string("doc_stage", 50)->nullable();
+            $table->string("doc_type", 50)->nullable();
+            $table->string("status")->default('wait')->comment('wait,doing,done,pause,cancel,closed');
+            $table->integer("assign")->nullable();
+            $table->text("description")->nullable();
+            $table->date("begin")->nullable();
+            $table->date("end")->nullable();
+            $table->json('mailto')->nullable();
+            $table->string('email_subject')->nullable();
+            $table->string('acl')->default('private')->comment('private,custom');
+            $table->string("whitelist")->nullable();
+            $table->integer('closed_by')->nullable();
+            $table->dateTime("closed_at")->nullable();
+            $table->integer('canceled_by')->nullable();
+            $table->dateTime("canceled_at")->nullable();
+            $table->integer('approve_by')->nullable();
+            $table->dateTime("approve_at")->nullable();
+            $table->integer('finished_by')->nullable();
+            $table->dateTime("finished_at")->nullable();
+            $table->integer('review_by')->nullable();
+            $table->dateTime("review_at")->nullable();
+            $table->integer('created_by')->nullable();
+            $table->json("custom_fields")->nullable();
+            $table->softDeletes();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('tasks');
+    }
+};

+ 1 - 0
routes/api.php

@@ -33,6 +33,7 @@ Route::middleware(['auth:sanctum'])->group(function () {
             'role' => API\RoleController::class,
             'custom-field' => API\CustomFieldController::class,
             'naming-rule' => API\NameRuleController::class,
+            'task' => API\TaskController::class,
         ]);
 
         Route::get("requirement/{asset_id}/asset", [API\RequirementController::class, "byAsset"])->name("requirement.byAsset");

+ 117 - 0
tests/Feature/API/TaskTest.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace Tests\Feature\API;
+
+use App\Models\Task;
+use App\Models\User;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Foundation\Testing\WithFaker;
+use Illuminate\Support\Facades\Auth;
+use Tests\Feature\TestCase;
+
+class TaskTest extends TestCase
+{
+    public function test_create_task(): void
+    {
+        $form = Task::factory()->make();
+        $form->whitelist = [
+            Auth::id(),
+        ];
+
+        $response = $this->postJson(route('task.store'), $form->toArray());
+
+        $response->assertStatus(201);
+    }
+
+    protected function test_task_list()
+    {
+        Task::factory(30)->create();
+
+        $response = $this->get(route('task.index'));
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    '*' => [
+                        'id',
+                        'name',
+                        'global',
+                        'status',
+                        'company'
+                    ]
+                ]
+            ]);
+    }
+
+    public function test_task_show(): void
+    {
+        $task = Task::factory()->create();
+
+        $response = $this->get(route('task.show', ['task' => $task->id]));
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    "id",
+                    "name",
+                    "project_id",
+                    "project",
+                    "requirement_id",
+                    "naming_rule_id",
+                    "parent_id",
+                    "task_type",
+                    "doc_stage",
+                    "doc_type",
+                    "status",
+                    "assign",
+                    "description",
+                    "begin",
+                    "end",
+                    "mailto",
+                    "email_subject",
+                    "acl",
+                    "whitelist",
+                    "closed_by",
+                    "closed_at",
+                    "canceled_by",
+                    "canceled_at",
+                    "approve_by",
+                    "approve_at",
+                    "finished_by",
+                    "finished_at",
+                    "review_by",
+                    "review_at",
+                    "created_by",
+                    "custom_fields",
+                    "created_at",
+                    "updated_at"
+                ]
+            ]);
+    }
+
+    protected function test_task_update(): void
+    {
+        $task = Task::factory()->create();
+
+        $form = Task::factory()->make();
+
+        $response = $this->put(route('task.update', ['task' => $task->id]), $form->toArray());
+
+        $response->assertStatus(204);
+
+        $newAsset = Task::find($task->id);
+
+        $this->assertEquals($form->name, $newAsset->name);
+    }
+
+    protected function test_task_delete(): void
+    {
+        $task = Task::factory()->create();
+
+        $response = $this->delete(route('task.destroy', ['task' => $task->id]));
+
+        $response->assertStatus(204);
+
+        $this->assertNull(Task::find($task->id));
+    }
+}

+ 0 - 20
tests/Feature/tests/Feature/API/ProjectTest.php.php

@@ -1,20 +0,0 @@
-<?php
-
-namespace Tests\Feature\tests\Feature\API;
-
-use Illuminate\Foundation\Testing\RefreshDatabase;
-use Illuminate\Foundation\Testing\WithFaker;
-use Tests\TestCase;
-
-class ProjectTest.php extends TestCase
-{
-    /**
-     * A basic feature test example.
-     */
-    public function test_example(): void
-    {
-        $response = $this->get('/');
-
-        $response->assertStatus(200);
-    }
-}