Browse Source

Merge branch 'link-requirement-and-management' of kyle/autocde2.0 into dev

Mo 1 year ago
parent
commit
eb952c00e3

+ 95 - 1
app/Http/Controllers/API/ProjectController.php

@@ -4,15 +4,23 @@ namespace App\Http\Controllers\API;
 
 use App\Http\Controllers\Controller;
 use App\Http\Requests\API\Project\CreateOrUpdateRequest;
+use App\Http\Requests\API\Project\LinkRequirementByPlanRequest;
+use App\Http\Requests\API\Project\LinkRequirementRequest;
 use App\Http\Requests\API\Project\PostponeRequest;
+use App\Http\Requests\API\Project\UnlinkRequirementRequest;
+use App\Http\Resources\API\PlanResource;
+use App\Http\Resources\API\ProjectRequirementResource;
 use App\Http\Resources\API\ProjectResource;
+use App\Http\Resources\API\RequirementResource;
 use App\Models\Enums\ProjectStatus;
+use App\Models\Plan;
 use App\Models\Project;
 use App\Models\ProjectAsset;
 use App\Models\ProjectPlan;
+use App\Models\ProjectRequirement;
+use App\Models\Requirement;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
-use function MongoDB\BSON\toJSON;
 
 class ProjectController extends Controller
 {
@@ -167,4 +175,90 @@ class ProjectController extends Controller
 
         return $this->noContent();
     }
+
+    public function linkRequirement(LinkRequirementRequest $request, string $id)
+    {
+        $project = Project::findOrFail($id);
+
+        $requirement = Requirement::findOrFail($request->requirement_id);
+
+        $exists = ProjectAsset::query()
+            ->where('project_id', $project->id)
+            ->where('asset_id', $requirement->asset_id)
+            ->count();
+        if (! $exists) {
+            return $this->forbidden("Please select the correct requirement");
+        }
+
+        ProjectRequirement::query()->firstOrCreate([
+            'project_id' => $project->id,
+            'requirement_id' => $requirement->id,
+        ]);
+
+        return $this->noContent();
+    }
+
+    public function unlinkRequirement(UnlinkRequirementRequest $request, string $id)
+    {
+        $project = Project::findOrFail($id);
+
+        $requirement = Requirement::findOrFail($request->requirement_id);
+
+        $exists = ProjectAsset::query()
+            ->where('project_id', $project->id)
+            ->where('asset_id', $requirement->asset_id)
+            ->count();
+        if (! $exists) {
+            return $this->forbidden("Please select the correct requirement");
+        }
+
+        ProjectRequirement::query()->where([
+            'project_id' => $project->id,
+            'requirement_id' => $requirement->id,
+        ])->delete();
+
+        return $this->noContent();
+    }
+
+    public function linkRequirementByPlan(LinkRequirementByPlanRequest $request, string $id)
+    {
+        $project = Project::findOrFail($id);
+
+        $plan = Plan::findOrFail($request->plan_id);
+
+        foreach ($plan->requirements as $requirement) {
+            ProjectRequirement::query()->firstOrCreate([
+                'project_id' => $project->id, 'requirement_id' => $requirement->id,
+            ]);
+        }
+
+        return $this->noContent();
+    }
+
+    public function plan(string $id)
+    {
+        $project = Project::findOrFail($id);
+
+        return PlanResource::collection($project->plans);
+    }
+
+    public function requirement(string $id)
+    {
+        $project = Project::findOrFail($id);
+
+        $requirements = $project->requirements()->with(['createdBy'])->simplePaginate();
+
+        return ProjectRequirementResource::collection($requirements);
+    }
+
+    public function notLinkAssetRequirement(string $id)
+    {
+        $project = Project::findOrFail($id);
+
+        $requirements = $project->assets
+            ? Requirement::query()->whereIn('asset_id', $project->assets?->pluck('id')->toArray())->simplePaginate()
+            : [];
+
+        return RequirementResource::collection($requirements);
+    }
 }

+ 28 - 0
app/Http/Requests/API/Project/LinkRequirementByPlanRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Requests\API\Project;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class LinkRequirementByPlanRequest 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 [
+            'plan_id' => 'required',
+        ];
+    }
+}

+ 28 - 0
app/Http/Requests/API/Project/LinkRequirementRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Requests\API\Project;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class LinkRequirementRequest 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 [
+            'requirement_id' => 'required',
+        ];
+    }
+}

+ 28 - 0
app/Http/Requests/API/Project/UnlinkRequirementRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Requests\API\Project;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class UnlinkRequirementRequest 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 [
+            'requirement_id' => 'required',
+        ];
+    }
+}

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

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class PlanResource 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,
+        ];
+    }
+}

+ 25 - 0
app/Http/Resources/API/ProjectRequirementResource.php

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

+ 19 - 0
app/Http/Resources/API/RequirementResource.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class RequirementResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return parent::toArray($request);
+    }
+}

+ 23 - 0
app/Http/Resources/API/UserProfileResource.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class UserProfileResource 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,
+            'username' => $this->username,
+        ];
+    }
+}

+ 11 - 1
app/Models/Plan.php

@@ -10,8 +10,18 @@ class Plan extends Model
     use HasFactory;
 
     protected $fillable = [
-        'title'
+        'title',
+        'asset_id',
+        'parent_id',
+        'begin',
+        'end',
+        'description',
     ];
 
     public $timestamps = false;
+
+    public function requirements()
+    {
+        return $this->belongsToMany(Requirement::class, 'plan_requirement', 'plan_id', 'requirement_id');
+    }
 }

+ 20 - 0
app/Models/PlanRequirement.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class PlanRequirement extends Model
+{
+    use HasFactory;
+
+    protected $table = 'plan_requirement';
+
+    protected $fillable = [
+        'plan_id',
+        'requirement_id',
+    ];
+
+    public $timestamps = false;
+}

+ 15 - 0
app/Models/Project.php

@@ -19,4 +19,19 @@ class Project extends Model
     {
         static::addGlobalScope(new CompanyScope);
     }
+
+    public function plans(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
+    {
+        return $this->belongsToMany(Plan::class, 'project_plan');
+    }
+
+    public function assets(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
+    {
+        return $this->belongsToMany(Asset::class, 'project_asset');
+    }
+
+    public function requirements(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
+    {
+        return $this->belongsToMany(Requirement::class, 'project_requirement');
+    }
 }

+ 20 - 0
app/Models/ProjectRequirement.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class ProjectRequirement extends Model
+{
+    use HasFactory;
+
+    protected $table = 'project_requirement';
+
+    public $timestamps = false;
+
+    protected $fillable = [
+        'project_id',
+        'requirement_id',
+    ];
+}

+ 5 - 0
app/Models/Requirement.php

@@ -25,4 +25,9 @@ class Requirement extends Model
     protected $casts = [
         'mailto' => 'array',
     ];
+
+    public function createdBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'created_by');
+    }
 }

+ 6 - 0
database/factories/PlanFactory.php

@@ -2,6 +2,8 @@
 
 namespace Database\Factories;
 
+use App\Models\Asset;
+use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Factories\Factory;
 use Illuminate\Support\Facades\Auth;
 
@@ -20,6 +22,10 @@ class PlanFactory extends Factory
         return [
             'title' => fake()->title(),
             'company_id' => Auth::user()->company_id,
+            'asset_id' => Asset::factory(),
+            'begin' => Carbon::now()->toDateString(),
+            'end' => Carbon::now()->addDays(10)->toDateString(),
+            'description' => fake()->text(),
         ];
     }
 }

+ 4 - 1
database/factories/RequirementFactory.php

@@ -7,6 +7,7 @@ use App\Models\Enums\RequirementStatus;
 use App\Models\RequirementGroup;
 use App\Models\User;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Facades\Auth;
 
 /**
  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Requirement>
@@ -22,9 +23,11 @@ class RequirementFactory extends Factory
     {
         return [
             'title' => fake()->title(),
-            'asset_id' => Asset::factory()->create()->id,
+            'asset_id' => Asset::factory(),
             'status' => RequirementStatus::CHANGED->value,
             'requirement_group_id' => RequirementGroup::factory()->create()->id,
+            'company_id' => Auth::user()->company_id,
+            'created_by' => Auth::user()->id,
             'priority' => rand(1, 4),
             'note' => fake()->text(30),
             'description' => fake()->text(),

+ 28 - 0
database/migrations/2024_01_23_130940_craete_project_requirement_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::create('project_requirement', function (Blueprint $table) {
+            $table->id();
+            $table->integer('project_id')->index();
+            $table->integer('requirement_id')->index();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('project_requirement');
+    }
+};

+ 28 - 0
database/migrations/2024_01_24_122827_create_plan_requirement_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::create('plan_requirement', function (Blueprint $table) {
+            $table->id();
+            $table->integer('plan_id')->index();
+            $table->integer('requirement_id')->index();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('plan_requirement');
+    }
+};

+ 36 - 0
database/migrations/2024_01_24_124223_add_fields_to_plan_table.php

@@ -0,0 +1,36 @@
+<?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('plans', function (Blueprint $table) {
+            $table->integer('asset_id');
+            $table->integer('parent_id')->default(0);
+            $table->date("begin")->nullable();
+            $table->date("end")->nullable();
+            $table->text("description")->nullable();
+            $table->softDeletes();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('plans', function (Blueprint $table) {
+            $table->dropColumn([
+                'asset_id', 'parent_id', 'begin', 'end', 'description', 'deleted_at', 'created_at', 'udpated_at'
+            ]);
+        });
+    }
+};

+ 15 - 1
routes/api.php

@@ -36,5 +36,19 @@ Route::middleware(['auth:sanctum'])->group(function () {
     Route::patch("project/{project}/closed", [API\ProjectController::class, "closed"])->name("project.closed");
     Route::patch("project/{project}/start", [API\ProjectController::class, "start"])->name("project.start");
     Route::patch("project/{project}/pause", [API\ProjectController::class, "pause"])->name("project.pause");
-    Route::patch("project/{project}/postpone", [API\ProjectController::class, "postpone"])->name("project.postpone");
+    Route::patch("project/{project}/postpone", [API\ProjectController::class, "postpone"])
+        ->name("project.postpone");
+    Route::patch("project/{project}/link-requirement", [API\ProjectController::class, "linkRequirement"])
+        ->name("project.link-requirement");
+    Route::patch("project/{project}/unlink-requirement", [API\ProjectController::class, "unlinkRequirement"])
+        ->name("project.unlink-requirement");
+    Route::patch("project/{project}/link-requirement-by-plan", [API\ProjectController::class, "linkRequirementByPlan"])
+        ->name("project.link-requirement-by-plan");
+
+    Route::get("project/{project}/plan", [API\ProjectController::class, "plan"])
+        ->name("project.plan"); //项目关联计划
+    Route::get("project/{project}/requirement", [API\ProjectController::class, "requirement"])
+        ->name("project.requirement"); //项目需求
+    Route::get("project/{project}/not-link-asset-requirement", [API\ProjectController::class, "notLinkAssetRequirement"])
+        ->name("project.not-link-asset-requirement"); //项目未关联的资产需求
 });

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

@@ -5,7 +5,12 @@ namespace Tests\Feature\API;
 use App\Models\Asset;
 use App\Models\Enums\ProjectStatus;
 use App\Models\Plan;
+use App\Models\PlanRequirement;
 use App\Models\Project;
+use App\Models\ProjectAsset;
+use App\Models\ProjectPlan;
+use App\Models\ProjectRequirement;
+use App\Models\Requirement;
 use Carbon\Carbon;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Foundation\Testing\WithFaker;
@@ -141,4 +146,145 @@ class ProjectTest extends TestCase
 
         $response->assertStatus(204);
     }
+
+    public function test_project_plans(): void
+    {
+        $project = Project::factory()->create();
+        $plan = Plan::factory()->create();
+
+        $projectPlan = ProjectPlan::create([
+            'project_id' => $project->id,
+            'plan_id' => $plan->id,
+        ]);
+
+        $response = $this->get(route('project.plan', ['project' => $project->id]));
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    '*' => [
+                        'id',
+                        'title'
+                    ]
+                ]
+            ])->assertJsonPath("data.0.id", $projectPlan->id);
+    }
+
+    public function test_project_not_link_asset_requirement(): void
+    {
+        $project = Project::factory()->create();
+        $requirement = Requirement::factory()->create();
+
+        ProjectAsset::create([
+            'project_id' => $project->id,
+            'asset_id' => $requirement->asset_id,
+        ]);
+
+        $response = $this->get(route('project.not-link-asset-requirement', ['project' => $project->id]));
+
+        $response->assertStatus(200)
+            ->assertJsonStructure($this->simplePaginateResponseStructure([
+                'id', 'title',
+            ]))->assertJsonPath("data.0.id", $requirement->id);
+    }
+
+    public function test_link_requirement(): void
+    {
+        $project = Project::factory()->create();
+
+        $requirement = Requirement::factory()->create();
+
+        ProjectAsset::create([
+            'project_id' => $project->id,
+            'asset_id' => $requirement->asset_id,
+        ]);
+
+        $response = $this->patch(route('project.link-requirement', ['project' => $project->id]), [
+            'requirement_id' => $requirement->id,
+        ]);
+
+        $response->assertStatus(204);
+    }
+
+    public function test_unlink_requirement(): void
+    {
+        $project = Project::factory()->create();
+
+        $requirement = Requirement::factory()->create();
+
+        ProjectAsset::create([
+            'project_id' => $project->id,
+            'asset_id' => $requirement->asset_id,
+        ]);
+
+        ProjectRequirement::create([
+            'project_id' => $project->id,
+            'requirement_id' => $requirement->id,
+        ]);
+
+        $response = $this->patch(route('project.unlink-requirement', ['project' => $project->id]), [
+            'requirement_id' => $requirement->id,
+        ]);
+
+        $response->assertStatus(204);
+
+        $count = ProjectRequirement::where([
+            'project_id' => $project->id,
+            'requirement_id' => $requirement->id,
+        ])->count();
+
+        $this->assertEquals(0, $count);
+    }
+
+    public function test_project_link_requirement_by_plan(): void
+    {
+        $project = Project::factory()->create();
+
+        $plan = Plan::factory()->create();
+
+        $requirement = Requirement::factory()->make();
+        $requirement->asset_id = $plan->asset_id;
+        $requirement->save();
+
+        PlanRequirement::create([
+            'plan_id' => $plan->id,
+            'requirement_id' => $requirement->id,
+        ]);
+
+        $response = $this->patch(route('project.link-requirement-by-plan', ['project' => $project->id]), [
+            'plan_id' => $plan->id,
+        ]);
+
+        $response->assertStatus(204);
+
+        $count = ProjectRequirement::where([
+            'project_id' => $project->id,
+            'requirement_id' => $requirement->id,
+        ])->count();
+
+        $this->assertEquals(1, $count);
+    }
+
+    public function test_project_requirement_list(): void
+    {
+        $project = Project::factory()->create();
+
+        $requirements = Requirement::factory(30)->create();
+
+        foreach ($requirements as $requirement) {
+            ProjectRequirement::create([
+                'project_id' => $project->id,
+                'requirement_id' => $requirement->id,
+            ]);
+        }
+
+        $response = $this->get(route('project.requirement', ['project' => $project->id]));
+
+        dump($response->getContent());
+
+        $response->assertStatus(200)
+            ->assertJsonStructure($this->simplePaginateResponseStructure([
+                'id', 'title', 'created_by', 'status'
+            ]));
+    }
 }