Browse Source

Merge branch 'dev' into nameing-rules-is-invalid

kely 10 months ago
parent
commit
7637c3868b
100 changed files with 2949 additions and 172 deletions
  1. 46 0
      app/Events/ObjectActionCreate.php
  2. 74 0
      app/Http/Controllers/API/ActionController.php
  3. 5 1
      app/Http/Controllers/API/AssetController.php
  4. 2 2
      app/Http/Controllers/API/AssetGroupController.php
  5. 5 0
      app/Http/Controllers/API/AuthController.php
  6. 27 11
      app/Http/Controllers/API/CompanyController.php
  7. 79 0
      app/Http/Controllers/API/ConfigController.php
  8. 172 0
      app/Http/Controllers/API/ContainerController.php
  9. 2 1
      app/Http/Controllers/API/DepartmentController.php
  10. 180 19
      app/Http/Controllers/API/FileController.php
  11. 94 0
      app/Http/Controllers/API/NotificationController.php
  12. 17 3
      app/Http/Controllers/API/PlanController.php
  13. 25 8
      app/Http/Controllers/API/ProjectController.php
  14. 29 6
      app/Http/Controllers/API/RequirementController.php
  15. 2 1
      app/Http/Controllers/API/RequirementGroupController.php
  16. 3 0
      app/Http/Controllers/API/RoleController.php
  17. 153 5
      app/Http/Controllers/API/TaskController.php
  18. 117 8
      app/Http/Controllers/API/UserController.php
  19. 3 1
      app/Http/Kernel.php
  20. 26 0
      app/Http/Middleware/SuperAdmin.php
  21. 32 0
      app/Http/Requests/API/Action/CommentRequest.php
  22. 9 15
      app/Http/Requests/API/Config/AllowSettingConfig.php
  23. 66 0
      app/Http/Requests/API/Container/CreateOrUpdateRequest.php
  24. 2 10
      app/Http/Requests/API/File/DownloadZipRequest.php
  25. 59 0
      app/Http/Requests/API/File/FileUploadRequest.php
  26. 35 0
      app/Http/Requests/API/Task/AssignRequest.php
  27. 1 1
      app/Http/Requests/API/Task/BatchCreateItemRules.php
  28. 10 0
      app/Http/Requests/API/Task/BatchCreateRequest.php
  29. 24 2
      app/Http/Requests/API/Task/CreateOrUpdateRequest.php
  30. 53 0
      app/Http/Requests/API/User/AdminUpdateRequest.php
  31. 71 0
      app/Http/Requests/API/User/BatchCreateRequest.php
  32. 15 5
      app/Http/Requests/API/User/CreateRequest.php
  33. 52 0
      app/Http/Requests/API/User/UpdateRequest.php
  34. 1 1
      app/Http/Resources/API/AssetReportResource.php
  35. 2 1
      app/Http/Resources/API/AssetResource.php
  36. 36 0
      app/Http/Resources/API/ContainerDetailResource.php
  37. 1 0
      app/Http/Resources/API/CustomFieldResource.php
  38. 6 1
      app/Http/Resources/API/DepartmentResource.php
  39. 27 0
      app/Http/Resources/API/FileByObjectResource.php
  40. 27 0
      app/Http/Resources/API/FileDownloadResource.php
  41. 7 7
      app/Http/Resources/API/FileUploadSuccessResource.php
  42. 22 0
      app/Http/Resources/API/LibrarySimpleResource.php
  43. 25 0
      app/Http/Resources/API/NotificationResource.php
  44. 1 1
      app/Http/Resources/API/PlanResource.php
  45. 1 1
      app/Http/Resources/API/ProjectDetailResource.php
  46. 1 1
      app/Http/Resources/API/RequirementResource.php
  47. 1 0
      app/Http/Resources/API/RequirementSimpleResource.php
  48. 2 1
      app/Http/Resources/API/TaskDetailResource.php
  49. 6 1
      app/Http/Resources/API/TaskResource.php
  50. 1 0
      app/Http/Resources/API/UserInfoResource.php
  51. 35 0
      app/Http/Resources/API/UserSimpleResource.php
  52. 72 0
      app/Listeners/SendActionBrowserNotification.php
  53. 40 0
      app/Listeners/SendActionEmailNotification.php
  54. 56 0
      app/Mail/RequirementAction.php
  55. 56 0
      app/Mail/TaskAction.php
  56. 23 0
      app/ModelFilters/AssetGroupFilter.php
  57. 16 0
      app/ModelFilters/ContainerFilter.php
  58. 4 0
      app/ModelFilters/CustomFieldFilter.php
  59. 43 0
      app/ModelFilters/NotificationFilter.php
  60. 2 27
      app/ModelFilters/RequirementFilter.php
  61. 22 0
      app/ModelFilters/UserFilter.php
  62. 2 1
      app/Models/AssetGroup.php
  63. 12 1
      app/Models/Company.php
  64. 56 0
      app/Models/Container.php
  65. 15 0
      app/Models/ContainerContent.php
  66. 1 1
      app/Models/CustomField.php
  67. 23 2
      app/Models/Enums/ActionObjectType.php
  68. 12 0
      app/Models/Enums/ConfigGroup.php
  69. 10 0
      app/Models/Enums/ContainerACL.php
  70. 4 2
      app/Models/Enums/CustomFieldGroup.php
  71. 51 0
      app/Models/Enums/FileObjectType.php
  72. 10 0
      app/Models/Enums/FileSource.php
  73. 10 0
      app/Models/Enums/NotificationObjectType.php
  74. 12 0
      app/Models/Enums/NotificationStatus.php
  75. 23 0
      app/Models/Enums/ObjectAction.php
  76. 25 0
      app/Models/File.php
  77. 14 0
      app/Models/Notification.php
  78. 13 0
      app/Models/NotificationRecord.php
  79. 1 1
      app/Models/Project.php
  80. 1 1
      app/Models/Task.php
  81. 8 1
      app/Models/User.php
  82. 8 1
      app/Providers/EventServiceProvider.php
  83. 40 5
      app/Repositories/ActionRepository.php
  84. 41 0
      app/Repositories/ConfigRepository.php
  85. 24 0
      app/Repositories/Enums/BrowserConfigFiledEnum.php
  86. 41 0
      app/Repositories/Enums/EmailConfigFieldEnum.php
  87. 50 0
      app/Services/File/FileAssociationService.php
  88. 87 0
      app/Services/File/ImageUrlService.php
  89. 10 0
      app/Services/History/Converter/ContainerConverter.php
  90. 30 0
      app/Services/History/Detector/ContainerContentDetector.php
  91. 56 0
      app/Services/History/Detector/ContainerDetector.php
  92. 1 1
      app/Services/History/ModelChangeDetector.php
  93. 69 0
      app/Services/Notification/ActionEmail/ActionEmailService.php
  94. 69 5
      app/Services/Project/ProjectGanttService.php
  95. 17 9
      app/Services/Project/ProjectKanbanService.php
  96. 3 0
      app/helpers.php
  97. 5 0
      config/autocde.php
  98. 1 0
      config/custom-field.php
  99. 38 0
      database/migrations/2024_03_26_201122_create_files_table.php
  100. 30 0
      database/migrations/2024_03_27_151013_add_remark_to_custom_fields.php

+ 46 - 0
app/Events/ObjectActionCreate.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Events;
+
+use App\Models\Action;
+use App\Models\Config;
+use App\Models\Enums\ConfigGroup;
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class ObjectActionCreate
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    public array $notificationSetting = [];
+
+    /**
+     * Create a new event instance.
+     */
+    public function __construct(public Action $action)
+    {
+        $settingResult = Config::query()
+            ->where("group", ConfigGroup::MESSAGE_NOTIFICATION)
+            ->where("key", "setting")
+            ->first();
+
+        $this->notificationSetting = $settingResult ? json_decode($settingResult->value, true) : [];
+    }
+
+    /**
+     * Get the channels the event should broadcast on.
+     *
+     * @return array<int, \Illuminate\Broadcasting\Channel>
+     */
+    public function broadcastOn(): array
+    {
+        return [
+            new PrivateChannel('channel-name'),
+        ];
+    }
+}

+ 74 - 0
app/Http/Controllers/API/ActionController.php

@@ -3,8 +3,15 @@
 namespace App\Http\Controllers\API;
 
 use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Action\CommentRequest;
+use App\Models\Action;
 use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\FileObjectType;
+use App\Models\Enums\ObjectAction;
 use App\Repositories\ActionRepository;
+use App\Services\File\FileAssociationService;
+use App\Services\File\ImageUrlService;
+use DOMDocument;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 
@@ -20,4 +27,71 @@ class ActionController extends Controller
             'data' => ActionRepository::actionWithHistory($actionObjectType, $objectId),
         ]);
     }
+
+    /**
+     * action comment
+     *
+     * @param CommentRequest $request
+     * @param string $objectType
+     * @param string $objectId
+     * @return \Illuminate\Http\Responseassociation
+     */
+    public function comment(FileAssociationService $service, CommentRequest $request, string $objectType, string $objectId,ImageUrlService $imgService)
+    {
+        $actionObjectType =  ActionObjectType::from($objectType);
+
+        $comment=$request->get('comment');
+        $newComment=$imgService->interceptImageUrl($comment);
+
+        $object = $actionObjectType?->modelBuilderAllowed($objectId)
+            ->where("company_id", Auth::user()->company_id)
+            ->findOrFail($objectId);
+
+        $projectId = match ($actionObjectType) {
+            ActionObjectType::PROJECT => $objectId,
+            ActionObjectType::TASK => $object->project_id,
+            default => null
+        };
+
+        $action = ActionRepository::create(
+            $objectId,
+            $actionObjectType,
+            ObjectAction::COMMENTED,
+            projectId: $projectId,
+            comment: $newComment,
+        );
+
+        $service->association(
+            $request->get("file_ids", []),
+            $action->id,
+            FileObjectType::ACTION
+        );
+
+        return $this->created();
+    }
+
+    /**
+     * update comment
+     *
+     * @param CommentRequest $request
+     * @param string $id
+     * @return \Illuminate\Http\Response
+     */
+    public function updateComment(CommentRequest $request, string $id,ImageUrlService $imgService)
+    {
+        $action = Action::query()->findOrFail($id);
+
+        $actionObjectType =  ActionObjectType::from($action->object_type);
+
+        $actionObjectType?->modelBuilderAllowed($action->object_id)
+            ->where("company_id", Auth::user()->company_id)
+            ->findOrFail($action->object_id);
+
+        $comment=$request->get('comment');
+        $newComment=$imgService->interceptImageUrl($comment);
+        $action->comment = $newComment;
+        $action->save();
+
+        return $this->noContent();
+    }
 }

+ 5 - 1
app/Http/Controllers/API/AssetController.php

@@ -8,6 +8,7 @@ use App\Http\Resources\API\AssetReportResource;
 use App\Http\Resources\API\AssetResource;
 use App\Models\Asset;
 use App\Models\User;
+use App\Services\File\ImageUrlService;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 
@@ -18,11 +19,12 @@ class AssetController extends Controller
      */
     public function index(Request $request)
     {
+        $pageSize=$request->get('page_size') ?? 10;
         $assets = Asset::filter($request->all())
             ->where("parent_id", 0)
             ->where('company_id',Auth::user()->company_id)
             ->with(['children','children.children'])
-            ->paginate();
+            ->paginate($pageSize);
 
         return AssetResource::collection($assets);
     }
@@ -36,6 +38,7 @@ class AssetController extends Controller
             ...$request->all(),
             'company_id' => Auth::user()->company_id,
             'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
             'created_by' => Auth::id(),
         ]);
 
@@ -75,6 +78,7 @@ class AssetController extends Controller
         $formData = [
             ...$request->all(),
             'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
             'path' => $path
         ];
 

+ 2 - 2
app/Http/Controllers/API/AssetGroupController.php

@@ -16,8 +16,8 @@ class AssetGroupController extends Controller
      */
     public function index(Request $request)
     {
-        $name = $request->input('name');
-        $groups = AssetGroup::where('name', 'like', "%{$name}%")->orderByDesc("sequence")->get();
+        $pageSize=$request->get('page_size') ?? 10;
+        $groups = AssetGroup::filter($request->all())->orderByDesc("sequence")->paginate($pageSize);
         return AssetGroupResource::collection($groups);
     }
 

+ 5 - 0
app/Http/Controllers/API/AuthController.php

@@ -25,6 +25,11 @@ class AuthController extends Controller
                 'username' => [__("auth.failed")],
             ]);
         }
+        if ($user->status===0){
+            throw ValidationException::withMessages([
+                'username' => [__("auth.ban")],
+            ]);
+        }
 
         return $this->success([
             'data' => [

+ 27 - 11
app/Http/Controllers/API/CompanyController.php

@@ -13,20 +13,29 @@ use App\Http\Controllers\Controller;
 use App\Http\Requests\API\Company\CreateOrUpdateRequest;
 use App\Http\Resources\API\CompanyResource;
 use App\Models\Company;
+use App\Models\User;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
 
 class CompanyController extends Controller
 {
     public function index(Request $request)
     {
-        $company=Company::query()->filter($request->all())->get();
-        return CompanyResource::collection($company);
+
+        if(Auth::user()->super_admin){
+            $company=Company::query()->filter($request->all())->get();
+            return CompanyResource::collection($company);
+        }else{
+            $company=Auth::user()->company;
+            return new CompanyResource($company);
+        }
+
     }
 
 
     public function store(CreateOrUpdateRequest $request)
     {
-        $company=new Company();
+        $company = new Company();
 
         $company->fill([
             ...$request->all(),
@@ -39,26 +48,33 @@ class CompanyController extends Controller
 
     public function show(string $id)
     {
+        $companyId=Auth::user()->company->id;
+        if(empty(Auth::user()->super_admin)){
+            if($companyId!=$id){
+                return $this->forbidden("You are not a user under this company");
+            }
+        }
+
         $field = Company::query()->findOrFail($id);
 
         return new CompanyResource($field);
     }
 
-    public function update(CreateOrUpdateRequest $request,string $id){
-        $company=Company::findOrFail($id);
-        $company->fill($request->all());
+    public function update(Request $request,string $id){
+        $company = Company::findOrFail($id);
+
+        $company->email =$request->email;
         $company->save();
+
         return $this->noContent();
     }
 
 
     public function destroy(string $id)
     {
+        $company = Company::findOrFail($id);
+        $company->delete();
 
+        return $this->noContent();
     }
-
-
-
-
-
 }

+ 79 - 0
app/Http/Controllers/API/ConfigController.php

@@ -5,6 +5,8 @@ namespace App\Http\Controllers\API;
 use App\Http\Controllers\Controller;
 use App\Http\Requests\API\Config\AllowSettingConfig;
 use App\Models\Config;
+use App\Models\Enums\ConfigGroup;
+use App\Models\Enums\ObjectAction;
 use Illuminate\Http\Request;
 
 class ConfigController extends Controller
@@ -49,4 +51,81 @@ class ConfigController extends Controller
 
         return $this->noContent();
     }
+
+    public function messageNotificationSetting()
+    {
+        $relations = ObjectAction::messageNotificationItems();
+
+        $settingResult = Config::query()
+            ->where("group", ConfigGroup::MESSAGE_NOTIFICATION)
+            ->where("key", "setting")
+            ->first();
+
+        $setting = $settingResult ? json_decode($settingResult->value, true) : [];
+
+        $items = [];
+        foreach ($relations as $group => $relation) {
+            $item = [
+                'group_key' => $group,
+                'group_name' => __("action-labels.object_type." . $group),
+                'email' => [],
+                'browser' => [],
+            ];
+
+            foreach ($relation as $action) {
+                $actionItem = [
+                    'key' => $action->value,
+                    'name' => __("action-labels.label." . $action->value),
+                ];
+
+                $item['email'][] = [
+                    ...$actionItem,
+                    'checked' => in_array($action->value, $setting[$group]['email'] ?? []),
+                ];
+
+                $item['browser'][] = [
+                    ...$actionItem,
+                    'checked' => in_array($action->value, $setting[$group]['browser'] ?? []),
+                ];
+            }
+
+            $items[] = $item;
+        }
+
+        return $this->success([
+            'data' => $items
+        ]);
+    }
+
+    public function storeMessageNotificationSetting(Request $request)
+    {
+        $relations = ObjectAction::messageNotificationItems();
+
+        $settings = [];
+        foreach ($request->all() as $group => $items) {
+            if (! isset($relations[$group])) {
+                return $this->badRequest(sprintf("Group '%s' does not exist", $group));
+            }
+
+            foreach(['email', 'browser'] as $settingItem) {
+                foreach ($items[$settingItem] as $item) {
+                    $action = ObjectAction::tryFrom($item);
+                    if (! in_array($action, $relations[$group])) {
+                        return $this->badRequest(sprintf("In group '%s', '%s' exists", $group, $item));
+                    }
+                }
+            }
+
+            $settings[$group] = $items;
+        }
+
+        Config::query()->updateOrCreate([
+            'group' => ConfigGroup::MESSAGE_NOTIFICATION,
+            'key' => 'setting',
+        ], [
+            'value' => json_encode($settings),
+        ]);
+
+        return $this->noContent();
+    }
 }

+ 172 - 0
app/Http/Controllers/API/ContainerController.php

@@ -0,0 +1,172 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Container\CreateOrUpdateRequest;
+use App\Http\Resources\API\ContainerDetailResource;
+use App\Models\Container;
+use App\Models\ContainerContent;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\FileObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\File;
+use App\Repositories\ActionRepository;
+use App\Repositories\CustomFieldRepository;
+use App\Services\File\FileAssociationService;
+use App\Services\File\ImageUrlService;
+use App\Services\History\ModelChangeDetector;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class ContainerController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     */
+    public function store(
+        CreateOrUpdateRequest $request,
+        ImageUrlService $imageUrlService,
+        FileAssociationService $service,
+        CustomFieldRepository $customFieldRepo
+    )
+    {
+        $formData = [
+            ...$request->all(),
+            'company_id' => Auth::user()->company_id,
+            'created_by' => Auth::id(),
+            'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
+        ];
+
+        if ($request->has("naming_rule_id") && $request->get("naming_rule_id") > 0) {
+            $keys = $customFieldRepo->keysByGroup($request->get("naming_rule_id"));
+            $formData['naming_rules'] = $request->only($keys);
+        }
+
+        $container = new Container();
+        $container->mergeFillable(['company_id']);
+        $container->fill($formData);
+        $container->save();
+
+        ActionRepository::createByContainer($container, ObjectAction::CREATED);
+
+        $service->association(
+            $request->get("file_ids", []),
+            $container->id,
+            FileObjectType::CONTAINER
+        );
+
+        $files = File::query()->where('object_id', $container->id)
+            ->where('object_type', ActionObjectType::CONTAINER)
+            ->where('source', 1)
+            ->pluck("id")
+            ->sort();
+
+        $contentFormData = [
+            'description' => $imageUrlService->interceptImageUrl($request->description),
+            'container_id' => $container->id,
+            'created_by' => Auth::id(),
+            'name' => $request->name,
+            'files' => $files->implode(",") ?: null
+        ];
+
+        ContainerContent::query()->create($contentFormData);
+
+        return $this->created();
+    }
+
+    /**
+     * Display the specified resource.
+     */
+    public function show(string $id)
+    {
+        $container = Container::query()->allowed()
+            ->when(\request("version") > 0, fn($query) => $query->where("version", ">=", \request("version")))
+            ->findOrFail($id);
+
+        return new ContainerDetailResource($container);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     */
+    public function update(
+        CreateOrUpdateRequest $request,
+        ImageUrlService $imageUrlService,
+        CustomFieldRepository $customFieldRepo,
+        string $id
+    )
+    {
+        $container = Container::query()->allowed()->findOrFail($id);
+
+        $formData = [
+            ...$request->all(),
+            'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
+            'description' => $imageUrlService->interceptImageUrl($request->description) ,
+        ];
+
+        if ($request->has("naming_rule_id") && $request->get("naming_rule_id") > 0) {
+            $keys = $customFieldRepo->keysByGroup($request->get("naming_rule_id"));
+            $formData['naming_rules'] = $request->only($keys);
+        }
+
+        $container->fill($formData);
+        $changes = ModelChangeDetector::detector(ActionObjectType::CONTAINER, $container);
+
+        $files = File::query()->where('object_id', $container->id)
+            ->where('object_type', ActionObjectType::CONTAINER)
+            ->where('source', 1)
+            ->pluck("id")
+            ->sort();
+
+        $contentFormData = [
+            'description' => $imageUrlService->interceptImageUrl($request->description),
+            'name' => $request->name,
+            'files' => $files->implode(",") ?: null
+        ];
+
+        $containerContent = $container->content;
+        $containerContent->fill($contentFormData);
+        $contentChange = ModelChangeDetector::detector(ActionObjectType::CONTAINER_CONTENT, $containerContent);
+
+        if ($contentChange) {
+            $container->version++;
+            $changes = array_merge($changes, $contentChange);
+
+            ContainerContent::query()->create([
+                ...$contentFormData,
+                'container_id' => $container->id,
+                'created_by' => Auth::id(),
+                'version' => $container->version,
+            ]);
+        }
+
+        $container->save();
+
+        ActionRepository::createByContainer($container, ObjectAction::EDITED, objectChanges: $changes);
+
+        return $this->noContent();
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     */
+    public function destroy(string $id)
+    {
+        $container = Container::query()->allowed()->findOrFail($id);
+
+        $container->delete();
+
+        ActionRepository::createByContainer($container, ObjectAction::DELETED);
+
+        return $this->noContent();
+    }
+}

+ 2 - 1
app/Http/Controllers/API/DepartmentController.php

@@ -14,7 +14,8 @@ class DepartmentController extends Controller
 
     public function index(Request $request)
     {
-        $department=Department::filter($request->all())->where("parent_id",0)->with(['children'])->simplePaginate();
+        $pageSize=$request->get('page_size') ?? 10;
+        $department=Department::filter($request->all())->where("parent_id",0)->with(['children'])->paginate($pageSize);
 
         return DepartmentResource::collection($department);
     }

+ 180 - 19
app/Http/Controllers/API/FileController.php

@@ -3,37 +3,198 @@
 namespace App\Http\Controllers\API;
 
 use App\Http\Controllers\Controller;
-use App\Http\Requests\API\File\DownloadRequest;
-use App\Http\Requests\API\File\UploadRequest;
-use Carbon\Carbon;
+use App\Http\Requests\API\File\DownloadZipRequest;
+use App\Http\Requests\API\File\FileUploadRequest;
+use App\Http\Resources\API\FileByObjectResource;
+use App\Http\Resources\API\FileDownloadResource;
+use App\Http\Resources\API\FileUploadSuccessResource;
+use App\Models\Enums\FileObjectType;
+use App\Models\File;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
 
 class FileController extends Controller
 {
 
-    public function upload(UploadRequest $request)
+    public function download(string $id)
     {
+        $file = File::query()->findOrFail($id);
 
-        $file = $request->file('file');
-        $ext = $request->file('file')->getClientOriginalExtension() ? '.'.$request->file('file')->getClientOriginalExtension() : null;
-        $fileName = $request->fileName ? $request->fileName : Carbon::now()->timestamp. '_' . Str::random(10).$ext;
-        $path =  'uploads/' .date('Ymd/').$fileName;
-        Storage::put($path, file_get_contents($file->getRealPath()));
-        return [
-        'fileName' => $fileName,
-        'url' => $path,
-        'fullUrl' => Storage::url($path),
-        'ext' => $ext,
-    ];
+        $fileObjectType = FileObjectType::from($file->object_type);
+        $object = $fileObjectType->modelBuilderAllowed($file->object_id)->find($file->object_id);
+        if(! $object){
+            return $this->badRequest(sprintf("File ID: %s, no permission to access", $file->id));
+        }
+
+        return new FileDownloadResource($file);
+    }
+
+    public function changeName(Request $request,string $id)
+    {
+        $file = File::query()->findOrFail($id);
+
+        $fileObjectType = FileObjectType::from($file->object_type);
+        $object = $fileObjectType->modelBuilderAllowed($file->object_id)->find($file->object_id);
+        if(! $object){
+            return $this->badRequest(sprintf("File ID: %s, no permission to access", $file->id));
+        }
+
+        File::query()->where('title',$file->title)
+            ->where('object_type',$file->object_type)
+            ->where("object_id", $file->object_id)
+            ->update(['title' => $request->get('title')]);
+
+        return $this->noContent();
+    }
+
+    public function destroy(string $id){
+        $file = File::query()->findOrFail($id);
+
+        $fileObjectType = FileObjectType::from($file->object_type);
+        $object = $fileObjectType->modelBuilderAllowed($file->object_id)->find($file->object_id);
+        if(! $object){
+            return $this->badRequest(sprintf("File ID: %s, no permission to access", $file->id));
+        }
+
+        $files = File::query()
+            ->where('title',$file->title)
+            ->where('object_type',$file->object_type)
+            ->where("object_id", $file->object_id)
+            ->get();
+
+        File::query()->whereIn("id", $files->pluck("id")->toArray())->delete();
+
+        Storage::delete($files->pluck("pathname")->toArray());
+
+        $filesSize = $files->sum("size");
+
+        $company = Auth::user()->company;
+        $company->decrement("used_storage_capacity", $filesSize);
+        $company->save();
+
+        return $this->noContent();
     }
 
+    public function downloadZip(DownloadZipRequest $request)
+    {
+        $files = File::query()->whereIn("id", $request->get("ids"))->get();
+
+        foreach ($files as $file) {
+            $object = FileObjectType::from($file->object_type)
+                ->modelBuilderAllowed($file->object_id)
+                ->find($file->object_id);
+
+            if (! $object) {
+                return $this->badRequest(sprintf("File ID: %s, no permission to access", $file->id));
+            }
+        }
+
+        return FileDownloadResource::collection($files);
+    }
 
-    public function download(DownloadRequest $request)
+    /**
+     * 文件上传
+     *
+     * @param FileUploadRequest $request
+     * @return FileUploadSuccessResource|\Illuminate\Http\JsonResponse
+     */
+    public function upload(FileUploadRequest $request)
     {
-        $url=$request->url;
-        return Storage::download($url);
+        $names = $request->get("file_names", []);
+        $fileObjectType = FileObjectType::from($request->object_type);
+
+        $filesSize = 0;
+        foreach ($request->file("files") as $file) {
+            if (! $file->isValid()) {
+                return $this->badRequest("File upload failed.");
+            }
+
+            $filesSize += $file->getSize();
+        }
+
+        if ($filesSize + Auth::user()->company->used_storage_capacity > Auth::user()->company->storage_size) {
+            return $this->badRequest("Storage capacity is insufficient, please contact the administrator.");
+        }
+
+        $items = [];
+        foreach ($request->file("files") as $index => $file) {
+            $pathname = $file->storeAs(
+                sprintf("c%s/%s/%s", Auth::user()->company_id, $fileObjectType->value, date("Ymd")),
+                sprintf("%s.%s", md5(uniqid()), $file->extension())
+            );
+
+            if (! $pathname) {
+                return $this->badRequest("File upload failed.");
+            }
+
+            $items[] = [
+                'pathname' => $pathname,
+                'title' => $names[$index] ?? $file->getClientOriginalName(),
+                'size' => $file->getSize(),
+                'extension' => $file->extension(),
+                'object_type' => $request->object_type,
+                'object_id' => $request->object_id,
+                'created_by' => Auth::id(),
+                'company_id' => Auth::user()->company_id,
+                'source' => $request->get("source", 1),
+            ];
+        }
+
+        $uploadedFiles = [];
+        foreach ($items as $item) {
+            if ($item['object_id'] && $item['source'] == 1) {
+                $version = File::query()
+                    ->where('object_type', $item['object_type'])
+                    ->where('object_id', $item['object_id'])
+                    ->where("title", $item['title'])
+                    ->count();
+                $item['version'] = $version + 1;
+            }
+
+            $file = File::query()->create($item);
+
+            $uploadedFiles[] = new FileUploadSuccessResource($file);
+        }
+
+        $company = Auth::user()->company;
+        $company->increment("used_storage_capacity", $filesSize);
+        $company->save();
+
+        return $this->success([
+            'data' => $uploadedFiles
+        ]);
+
     }
 
+    public function byObject(string $objectType, string $objectId)
+    {
+        $fileObjectType = FileObjectType::from($objectType);
+        $fileObjectType->modelBuilderAllowed($objectId)->findOrFail($objectId);
+
+        $files = File::query()
+            ->with(['createdBy'])
+            ->where('object_type', $objectType)
+            ->where('object_id', $objectId)
+            ->orderByDesc("version")
+            ->where("source", 1)
+            ->get();
 
+
+        $items = [];
+        foreach ($files->groupBy("title") as $fileItems) {
+            $item = (new FileByObjectResource($fileItems->first()))->toArray(request());
+            $item['children'] = [];
+
+            foreach ($fileItems as $fileItem) {
+                $item['children'][] = new FileByObjectResource($fileItem);
+            }
+
+            $items[] = $item;
+        }
+
+        return $this->success([
+            'data' => $items
+        ]);
+    }
 }

+ 94 - 0
app/Http/Controllers/API/NotificationController.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\Http\Resources\API\NotificationResource;
+use App\Models\Enums\NotificationStatus;
+use App\Models\Notification;
+use App\Models\NotificationRecord;
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class NotificationController extends Controller
+{
+    /**
+     * 用户通知列表
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
+     */
+    public function index(Request $request)
+    {
+        $notifications = Notification::query()
+            ->filter($request->all())
+            ->join("notification_records", "notifications.id", "=", "notification_records.notification_id")
+            ->where("notification_records.user_id", Auth::id())
+            ->selectRaw("notifications.*,notification_records.read_at")
+            ->orderByDesc("created_at")
+            ->paginate();
+
+        return NotificationResource::collection($notifications);
+    }
+
+    /**
+     * 标记为已读
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
+     */
+    public function markAsRead(Request $request)
+    {
+        $ids = $request->get("ids");
+        if (! $ids) {
+            return $this->badRequest("Data is empty");
+        }
+
+        NotificationRecord::query()
+            ->where("user_id", Auth::id())
+            ->whereIn("notification_id", $ids)
+            ->whereNull("read_at")
+            ->update([
+                'read_at' => Carbon::now(),
+            ]);
+
+        return $this->noContent();
+    }
+
+    /**
+     * 未读消息
+     *
+     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
+     */
+    public function unread()
+    {
+        $announcements = Notification::query()
+            ->leftJoin("notification_records", "notifications.id", "=", "notification_records.notification_id")
+            ->where("notification_records.user_id", Auth::id())
+            ->where("notifications.status", NotificationStatus::RELEASE)
+            ->where("notifications.start_at", "<", Carbon::now())
+            ->where(fn($query) => $query->where("notifications.end_at", ">", Carbon::now())->orWhereNull("notifications.end_at"))
+            ->whereNull("notification_records.id")
+            ->selectRaw("notifications.id")
+            ->get();
+
+        foreach ($announcements as $announcement) {
+            NotificationRecord::query()->firstOrCreate([
+                'notification_id' => $announcement->id,
+                'user_id' => Auth::id()
+            ]);
+        }
+
+
+        $notifications = Notification::query()
+            ->join("notification_records", "notifications.id", "=", "notification_records.notification_id")
+            ->where("notification_records.user_id", Auth::id())
+            ->whereNull("notification_records.read_at")
+            ->selectRaw("notifications.*,notification_records.read_at")
+            ->orderByDesc("created_at")
+            ->paginate();
+
+        return NotificationResource::collection($notifications);
+    }
+}

+ 17 - 3
app/Http/Controllers/API/PlanController.php

@@ -7,7 +7,9 @@ use App\Http\Requests\API\Plan\CreateOrUpdateRequest;
 use App\Http\Resources\API\PlanByAssetResource;
 use App\Http\Resources\API\PlanResource;
 use App\Models\Asset;
+use App\Models\Enums\FileObjectType;
 use App\Models\Plan;
+use App\Services\File\FileAssociationService;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 
@@ -18,9 +20,10 @@ class PlanController extends Controller
      */
     public function index(Request $request)
     {
+        $pageSize=$request->get('page_size') ?? 10;
         $plans = Plan::filter($request->all())->where("parent_id", 0)->with(['children','asset'=>function($query){
             $query->with('parent');
-        }])->simplePaginate();
+        }])->paginate($pageSize);
 
         return PlanResource::collection($plans);
 
@@ -55,7 +58,7 @@ class PlanController extends Controller
     /**
      * Store a newly created resource in storage.
      */
-    public function store(CreateOrUpdateRequest $request)
+    public function store(FileAssociationService $service, CreateOrUpdateRequest $request)
     {
         $plan = new Plan();
 
@@ -66,10 +69,18 @@ class PlanController extends Controller
         $plan->fill([
             ...$request->all(),
             'company_id' => Auth::user()->company_id,
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
+
         ]);
 
         $plan->save();
 
+        $service->association(
+            $request->get("file_ids", []),
+            $plan->id,
+            FileObjectType::PLAN
+        );
+
         return $this->created();
     }
 
@@ -90,7 +101,10 @@ class PlanController extends Controller
     {
         $plan = Plan::findOrFail($id);
 
-        $plan->fill($request->all());
+        $plan->fill([
+            ...$request->all(),
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
+        ]);
 
         $plan->save();
 

+ 25 - 8
app/Http/Controllers/API/ProjectController.php

@@ -23,6 +23,7 @@ use App\Http\Resources\API\SimplePlanResource;
 use App\Http\Resources\API\ProjectRequirementResource;
 use App\Http\Resources\API\ProjectResource;
 use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\FileObjectType;
 use App\Models\Enums\ObjectAction;
 use App\Models\Enums\ProjectStatus;
 use App\Models\Enums\TaskStatus;
@@ -37,6 +38,7 @@ use App\Models\TeamMember;
 use App\Models\User;
 use App\Models\RequirementGroup;
 use App\Repositories\ActionRepository;
+use App\Services\File\FileAssociationService;
 use App\Services\History\ModelChangeDetector;
 use App\Services\Project\ProjectKanbanService;
 use App\Services\Project\ProjectGanttService;
@@ -54,7 +56,8 @@ class ProjectController extends Controller
      */
     public function index(Request $request)
     {
-        $projectAsset = Project::filter($request->all())->allowed()->with('assets')->simplePaginate();
+        $pageSize=$request->get('page_size') ?? 10;
+        $projectAsset = Project::filter($request->all())->allowed()->with('assets')->paginate($pageSize);
 
         return ProjectResource::collection($projectAsset);
     }
@@ -90,7 +93,7 @@ class ProjectController extends Controller
     /**
      * Store a newly created resource in storage.
      */
-    public function store(CreateOrUpdateRequest $request)
+    public function store(FileAssociationService $service, CreateOrUpdateRequest $request)
     {
         $project = new Project();
 
@@ -98,11 +101,12 @@ class ProjectController extends Controller
             'company_id', 'created_by'
         ]);
 
-        DB::transaction(function () use ($request,$project) {
+        DB::transaction(function () use ($request, $project, $service) {
             $project->fill([
                 ...$request->all(),
                 'company_id' => Auth::user()->company_id,
                 'created_by' => Auth::id(),
+                'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
                 'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
             ]);
 
@@ -110,6 +114,12 @@ class ProjectController extends Controller
 
             ActionRepository::createByProject($project, ObjectAction::CREATED);
 
+            $service->association(
+                $request->get("file_ids", []),
+                $project->id,
+                FileObjectType::PROJECT
+            );
+
             if ($request->has("assets")) {
                 foreach ($request->get("assets", []) as $assetId) {
                     ProjectAsset::create([
@@ -131,7 +141,7 @@ class ProjectController extends Controller
             TeamMember::create([
                 'project_id' => $project->id,
                 'user_id' => Auth::id(),
-                'role' => Auth::user()->role,
+                'role' => '',
                 'limited' => 1,
                 'join_at' => Carbon::now()->toDateString(),
                 'created_by' => Auth::id(),
@@ -160,6 +170,7 @@ class ProjectController extends Controller
 
         $project->fill([
             ...$request->all(),
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
             'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
         ]);
 
@@ -217,7 +228,9 @@ class ProjectController extends Controller
         $project->save();
 
         ActionRepository::createByProject(
-            $project, ObjectAction::CLOSED, $request->get("comment"), objectChanges: $changes
+            $project, ObjectAction::CLOSED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
         );
 
         return $this->noContent();
@@ -234,7 +247,7 @@ class ProjectController extends Controller
         ActionRepository::createByProject(
             $project,
             ObjectAction::STARTED,
-            $request->get("comment"),
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
             objectChanges: $changes
         );
 
@@ -250,7 +263,9 @@ class ProjectController extends Controller
         $project->save();
 
         ActionRepository::createByProject(
-            $project, ObjectAction::PAUSED, $request->get("comment"), objectChanges: $changes
+            $project, ObjectAction::PAUSED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
         );
 
         return $this->noContent();
@@ -274,7 +289,9 @@ class ProjectController extends Controller
         $project->save();
 
         ActionRepository::createByProject(
-            $project, ObjectAction::DELAY, $request->get("comment"), objectChanges: $changes
+            $project, ObjectAction::DELAY,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
         );
 
         return $this->noContent();

+ 29 - 6
app/Http/Controllers/API/RequirementController.php

@@ -11,11 +11,13 @@ use App\Http\Resources\API\AssetRequirementResource;
 use App\Http\Resources\API\RequirementResource;
 use App\Models\Asset;
 use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\FileObjectType;
 use App\Models\Enums\RequirementStatus;
 use App\Models\Enums\ObjectAction;
 use App\Models\Plan;
 use App\Models\Requirement;
 use App\Repositories\ActionRepository;
+use App\Services\File\FileAssociationService;
 use App\Services\History\ModelChangeDetector;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
@@ -29,9 +31,10 @@ class RequirementController extends Controller
      */
     public function index(Request $request)
     {
+        $pageSize=$request->get('page_size') ?? 10;
         $requirements=Requirement::filter($request->all())->where('company_id',Auth::user()->company_id)->with(['createdBy', 'plan','group','asset'=>function($query){
             $query->with('parent');
-        }]) ->simplePaginate();
+        }]) ->paginate($pageSize);
 
         return AssetRequirementResource::collection($requirements);
     }
@@ -46,7 +49,7 @@ class RequirementController extends Controller
     /**
      * Store a newly created resource in storage.
      */
-    public function store(CreateOrUpdateRequest $request)
+    public function store(FileAssociationService $service, CreateOrUpdateRequest $request)
     {
         $requirement = new Requirement();
 
@@ -57,6 +60,7 @@ class RequirementController extends Controller
         $requirement->fill([
             ...$request->all(),
             'company_id' => Auth::user()->company_id,
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
             'created_by' => Auth::id(),
         ]);
         $requirement->save();
@@ -65,6 +69,12 @@ class RequirementController extends Controller
             $requirement, ObjectAction::CREATED
         );
 
+        $service->association(
+            $request->get("file_ids", []),
+            $requirement->id,
+            FileObjectType::REQUIREMENT
+        );
+
         return $this->created();
     }
 
@@ -85,12 +95,15 @@ class RequirementController extends Controller
     {
         $requirement = Requirement::findOrFail($id);
 
-        $requirement->fill($request->all());
+        $requirement->fill([
+            ...$request->all(),
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
+        ]);
         $changes = ModelChangeDetector::detector(ActionObjectType::REQUIREMENT, $requirement);
         $requirement->save();
 
         ActionRepository::createRequirement(
-            $requirement, ObjectAction::STARTED,objectChanges: $changes
+            $requirement, ObjectAction::EDITED,objectChanges: $changes
         );
         return $this->noContent();
     }
@@ -119,7 +132,10 @@ class RequirementController extends Controller
         $requirement->save();
 
         ActionRepository::createRequirement(
-            $requirement, ObjectAction::CLOSED, $request->get("comment"), objectChanges: $changes
+            $requirement,
+            ObjectAction::CLOSED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
         );
 
         return $this->noContent();
@@ -134,7 +150,10 @@ class RequirementController extends Controller
         $requirement->save();
 
         ActionRepository::createRequirement(
-            $requirement, ObjectAction::STARTED, $request->get("comment"), objectChanges: $changes
+            $requirement,
+            ObjectAction::STARTED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
         );
 
         return $this->noContent();
@@ -186,6 +205,10 @@ class RequirementController extends Controller
                     'mailto' => [],
                 ]);
                 $requirement->save();
+
+                 ActionRepository::createRequirement(
+                     $requirement, ObjectAction::CREATED
+                 );
             }
         });
         return $this->created();

+ 2 - 1
app/Http/Controllers/API/RequirementGroupController.php

@@ -17,7 +17,8 @@ class RequirementGroupController extends Controller
      */
     public function index(Request $request)
     {
-        $groups = RequirementGroup::filter($request->all())->where("parent_id",0)->with(['children'])->simplePaginate();
+        $pageSize=$request->get('page_size') ?? 10;
+        $groups = RequirementGroup::filter($request->all())->where("parent_id",0)->with(['children'])->paginate($pageSize);
 
         return RequirementGroupResource::collection($groups);
     }

+ 3 - 0
app/Http/Controllers/API/RoleController.php

@@ -39,6 +39,9 @@ class RoleController extends Controller
     public function show(string $id)
     {
         //
+        $role=Role::query()->findOrFail($id);
+
+        return new RoleResource($role);
     }
 
     /**

+ 153 - 5
app/Http/Controllers/API/TaskController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\API;
 
 use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Task\AssignRequest;
 use App\Http\Requests\API\Task\BatchCreateItemRules;
 use App\Http\Requests\API\Task\BatchCreateRequest;
 use App\Http\Requests\API\Task\CreateOrUpdateRequest;
@@ -10,15 +11,18 @@ use App\Http\Resources\API\TaskDetailResource;
 use App\Http\Resources\API\TaskResource;
 use App\Models\CustomField;
 use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\FileObjectType;
 use App\Models\Enums\ObjectAction;
 use App\Models\NamingRule;
 use App\Models\Project;
 use App\Models\Requirement;
-use App\Models\RequirementGroup;
+use App\Models\Enums\TaskStatus;
 use App\Models\Task;
 use App\Repositories\ActionRepository;
 use App\Repositories\CustomFieldRepository;
+use App\Services\File\FileAssociationService;
 use App\Services\History\ModelChangeDetector;
+use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Validator;
@@ -30,12 +34,13 @@ class TaskController extends Controller
      */
     public function index(Request $request)
     {
+        $pageSize=$request->get('page_size') ?? 10;
         $tasks = Task::query()
             ->where("parent_id", 0)
             ->with(['children', 'assignTo', 'createdBy'])
             ->filter($request->all())
             ->allowed()
-            ->paginate();
+            ->paginate($pageSize);
 
         return TaskResource::collection($tasks);
     }
@@ -43,7 +48,7 @@ class TaskController extends Controller
     /**
      * Store a newly created resource in storage.
      */
-    public function store(CreateOrUpdateRequest $request, CustomFieldRepository $customFieldRepo)
+    public function store(FileAssociationService $service, CreateOrUpdateRequest $request, CustomFieldRepository $customFieldRepo)
     {
         $requirement = $request->has('requirement_id')
             ? Requirement::query()->findOrFail($request->get("requirement_id"))
@@ -55,6 +60,7 @@ class TaskController extends Controller
             'created_by' => Auth::id(),
             'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
             'asset_id' => $requirement?->asset_id,
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
             'requirement_group_id'=> $requirement?->requirement_group_id,
         ];
 
@@ -67,6 +73,12 @@ class TaskController extends Controller
 
         ActionRepository::createByTask($task, ObjectAction::CREATED);
 
+        $service->association(
+            $request->get("file_ids", []),
+            $task->id,
+            FileObjectType::TASK
+        );
+
         return $this->created();
     }
 
@@ -80,6 +92,137 @@ class TaskController extends Controller
         return new TaskDetailResource($task);
     }
 
+    public function start(Request $request, string $id)
+    {
+        $task = Task::query()->allowed($id)->findOrFail($id);
+        $task->status = TaskStatus::DOING->value;
+        $changes = ModelChangeDetector::detector(ActionObjectType::TASK, $task);
+        $task->save();
+
+        ActionRepository::createByTask(
+            $task,
+            ObjectAction::STARTED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
+        );
+
+        return $this->noContent();
+    }
+
+    public function pause(Request $request, string $id)
+    {
+        $task = Task::query()->allowed($id)->findOrFail($id);
+
+        $task->status = TaskStatus::PAUSE->value;
+        $changes = ModelChangeDetector::detector(ActionObjectType::TASK, $task);
+        $task->save();
+
+        ActionRepository::createByTask(
+            $task, ObjectAction::PAUSED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
+        );
+
+        return $this->noContent();
+    }
+
+    public function closed(Request $request, string $id)
+    {
+        $task = Task::query()->allowed($id)->findOrFail($id);
+
+        $task->status = TaskStatus::CLOSED->value;
+        $changes = ModelChangeDetector::detector(ActionObjectType::TASK, $task);
+        $task->save();
+
+        ActionRepository::createByTask(
+            $task, ObjectAction::CLOSED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
+        );
+
+        return $this->noContent();
+    }
+
+    public function done(Request $request, string $id)
+    {
+        $task = Task::query()->allowed($id)->findOrFail($id);
+
+        $task->fill([
+            'status' => TaskStatus::DONE->value,
+            'finished_by' => Auth::user()->id,
+            'finished_at' => Carbon::now(),
+        ]);
+
+        $changes = ModelChangeDetector::detector(ActionObjectType::TASK, $task);
+        $task->save();
+
+        ActionRepository::createByTask(
+            $task, ObjectAction::DONE,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
+        );
+
+        return $this->noContent();
+    }
+
+    public function cancel(Request $request, string $id)
+    {
+        $task = Task::query()->allowed($id)->findOrFail($id);
+
+        $task->fill([
+            'status' => TaskStatus::CANCEL->value,
+            'canceled_by' => Auth::user()->id,
+            'canceled_at' => Carbon::now(),
+        ]);
+
+        $changes = ModelChangeDetector::detector(ActionObjectType::TASK, $task);
+        $task->save();
+
+        ActionRepository::createByTask(
+            $task, ObjectAction::CANCELED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
+        );
+
+        return $this->noContent();
+    }
+
+    public function  wait(Request $request,string $id){
+        $task = Task::query()->allowed($id)->findOrFail($id);
+        $task->status=TaskStatus::WAIT->value;
+
+        $changes = ModelChangeDetector::detector(ActionObjectType::TASK, $task);
+        $task->save();
+
+        ActionRepository::createByTask(
+            $task, ObjectAction::WAITED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
+        );
+
+        return $this->noContent();
+
+    }
+
+    public function assign(AssignRequest $request,string $id){
+        $task = Task::query()->allowed($id)->findOrFail($id);
+        $task->fill([
+            'assign'=>$request->get('assign'),
+              ...$request->all(),
+        ]);
+
+        $changes = ModelChangeDetector::detector(ActionObjectType::TASK, $task);
+        $task->save();
+
+        ActionRepository::createByTask(
+            $task, ObjectAction::ASSIGNED,
+            $request->comment?(new \App\Services\File\ImageUrlService)->interceptImageUrl($request->comment) : null,
+            objectChanges: $changes
+        );
+
+        return $this->noContent();
+    }
+
     /**
      * Update the specified resource in storage.
      */
@@ -93,6 +236,7 @@ class TaskController extends Controller
 
         $formData = [...$request->all(),
             'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
+            'description' => $request->description? (new \App\Services\File\ImageUrlService)->interceptImageUrl($request->description) : null,
             'asset_id' => $requirement?->asset_id,
         ];
 
@@ -127,7 +271,6 @@ class TaskController extends Controller
     public function batchStore(BatchCreateRequest $request, CustomFieldRepository $customFieldRepo)
     {
         $project = Project::query()->allowed($request->project_id)->find($request->project_id);
-
         $parsedItems = [];
         $previousItem = [];
         foreach ($request->items as $index => $item) {
@@ -159,6 +302,9 @@ class TaskController extends Controller
         }
 
         foreach ($parsedItems as $item) {
+            $requirement=$item['requirement_id']>0 ? Requirement::query()->findOrFail($item['requirement_id']) : null;
+            $item["whitelist"]=!empty($item['whitelist']) ? sprintf(",%s,", implode(',',$item['whitelist'])) : null;
+
             $namingRuleId = data_get($item, "naming_rule_id", 0);
             if ($namingRuleId > 0) {
                 $keys = $customFieldRepo->keysByGroup($namingRuleId);
@@ -168,9 +314,11 @@ class TaskController extends Controller
             $task = Task::query()->create([
                 ...$item,
                 'project_id' => $project->id,
-                'parent_id' => 0,
+                'parent_id' => $request->parent_id,
                 'company_id' => Auth::user()->company_id,
                 'created_by' => Auth::id(),
+                'asset_id' => $requirement?->asset_id,
+                'requirement_group_id'=> $requirement?->requirement_group_id,
             ]);
 
             ActionRepository::createByTask($task, ObjectAction::CREATED);

+ 117 - 8
app/Http/Controllers/API/UserController.php

@@ -3,17 +3,24 @@
 namespace App\Http\Controllers\API;
 
 use App\Http\Controllers\Controller;
-use App\Http\Requests\API\User\CreateOrUpdateRequest;
+use App\Http\Requests\API\User\AdminUpdateRequest;
+use App\Http\Requests\API\User\BatchCreateRequest;
+use App\Http\Requests\API\User\CreateRequest;
+use App\Http\Requests\API\User\UpdateRequest;
 use App\Http\Resources\API\UserInfoResource;
+use App\Http\Resources\API\UserSimpleResource;
+use App\Models\Enums\RequirementStatus;
 use App\Models\Role;
 use App\Models\User;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Hash;
+use function Laravel\Prompts\password;
 
 class UserController extends Controller
 {
-    public function info()
+    public function details()
     {
         $user = Auth::user();
 
@@ -38,8 +45,16 @@ class UserController extends Controller
         return $this->noContent();
     }
 
-    public function index(){
-        $user = User::all();
+    public function index(Request $request){
+        //超管能看到所有用户
+        if(Auth::user()->super_admin){
+            $user = User::query()->filter($request->all())->paginate();
+            return UserSimpleResource::collection($user);
+        }
+        //普通管理员能看到自己公司的用户
+        $user=User::query()
+            ->where('company_id',Auth::user()->company_id)
+            ->filter($request->all()) ->paginate();
         return UserInfoResource::collection($user);
     }
 
@@ -47,7 +62,7 @@ class UserController extends Controller
      * add a new User
      * @return \Illuminate\Http\Response
      */
-    public function store(CreateOrUpdateRequest $request){
+    public function store(CreateRequest $request){
         $password = Hash::make($request->password);
         $user=$request->all();
         $user['password']=$password;
@@ -60,18 +75,112 @@ class UserController extends Controller
     }
 
 
+    /**
+     * batchCreate  User,为ditto时参考上一条
+     * @return \Illuminate\Http\Response
+     */
+    public function batchStore(BatchCreateRequest $request){
+        $userData = $request->users;
+        DB::transaction(function () use ($userData) {
+        foreach ($userData as $k => $data) {
+            $user = new User();
+            if ($k != 0) {
+                $userData[$k]["department_id"] = $userData[$k]["department_id"] == 'ditto' ? $userData[$k - 1]["department_id"] : $userData[$k]["department_id"];
+                $userData[$k]["role_id"] = $userData[$k]["role_id"] == 'ditto' ? $userData[$k - 1]["role_id"] : $userData[$k]["role_id"];
+            }
+            $userData[$k]['password'] = Hash::make($userData[$k]['password']);
+            $user->fill([
+                ...$userData[$k],
+                'created_by' => Auth::id(),
+            ]);
+            //        TODO:发送邮箱给目标用户
+            $user->save();
+        }
+        });
+//        TODO:发送邮箱给目标用户
+        return $this->created();
+    }
+
+    /**
+     * enable or ban users 启用或禁用用户
+     * @param Request $request
+     * @return \Illuminate\Http\Response
+     */
+    public function status(Request $request,string $status){
+        //只能删除自己公司的;超管除外
+        if (Auth::user()->super_admin){
+            User::whereIn('id', $request->user_id)->update(['status' => $status]);
+        }
+        else{
+            User::whereIn('id', $request->user_id)->where('company_id',Auth::user()->company_id)->update(['status' => $status]);
+        }
+        return $this->created();
+    }
+
     public function destroy(string $id)
     {
-
+        $user = User::query()->findOrFail($id);
+        $user->delete();
+        return $this->noContent();
     }
 
     public function show(string $id)
     {
-
+        $user = User::query()->findOrFail($id);
+        return new UserInfoResource($user);
     }
 
-    public function update(CreateOrUpdateRequest $request, string $id)
+    public function update(UpdateRequest $request,string $id)
     {
+        $user = User::findOrFail($id);
+        $newPassword=null;
+
+        if(Auth::user()->super_admin){
+            $user->fill([
+            ...$request->except(['username']),
+            'password'=> $request->password ? Hash::make($request->password):Auth::user()->password,
+            ]);
+            $user->save();
+            return $this->noContent();
+        }
+        $user->fill([
+            ...$request->except(['role_id','department_id','company_id']),
+            'password'=> $request->password ? Hash::make($request->password):Auth::user()->password,
+        ]);
+        $user->save();
+        return $this->noContent();
 
     }
+//    /**
+//     * @param CreateRequest $request
+//     * @return \Illuminate\Http\Response
+//     * 修改个人信息
+//     */
+//    public function updateInfo(UpdateRequest $request)
+//    {
+//        $user = User::findOrFail(Auth::user()->id);
+//        $user->fill([
+//            ...$request->except(['username','role_id','department_id','company_id'])
+//        ]);
+//        $user->save();
+//        return $this->noContent();
+//    }
+//
+//    /**
+//     * @param AdminUpdateRequest $request 修改主体
+//     * @param string $id 用户id
+//     * @return \Illuminate\Http\Response
+//     * 超管修改用户的信息
+//     */
+//    public function updateUserInfo(AdminUpdateRequest $request, string $id)
+//    {
+//        $user = User::findOrFail($id);
+//        $user->fill([
+//            ...$request->all()
+//        ]);
+//        $user->save();
+//        return $this->noContent();
+//
+//    }
+
 }

+ 3 - 1
app/Http/Kernel.php

@@ -3,6 +3,7 @@
 namespace App\Http;
 
 use App\Http\Middleware\CheckPermission;
+use App\Http\Middleware\SuperAdmin;
 use Illuminate\Foundation\Http\Kernel as HttpKernel;
 
 class Kernel extends HttpKernel
@@ -41,7 +42,7 @@ class Kernel extends HttpKernel
 
         'api' => [
             \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
-            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
+            //\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
         ],
     ];
@@ -66,5 +67,6 @@ class Kernel extends HttpKernel
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
         'permission' => CheckPermission::class,
+        'role.super-admin' => SuperAdmin::class,
     ];
 }

+ 26 - 0
app/Http/Middleware/SuperAdmin.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+class SuperAdmin
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
+     */
+    public function handle(Request $request, Closure $next): Response
+    {
+        if (Auth::user()->super_admin) {
+            return $next($request);
+        }
+
+        throw new HttpException(403, 'Operation without permission');
+    }
+}

+ 32 - 0
app/Http/Requests/API/Action/CommentRequest.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Requests\API\Action;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class CommentRequest 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 [
+            "comment" => "required",
+            'file_ids' => [
+                'nullable',
+                'array',
+            ]
+        ];
+    }
+}

+ 9 - 15
app/Http/Requests/API/Config/AllowSettingConfig.php

@@ -2,6 +2,9 @@
 
 namespace App\Http\Requests\API\Config;
 
+use App\Repositories\Enums\EmailConfigFieldEnum;
+use App\Repositories\Enums\BrowserConfigFiledEnum;
+
 class AllowSettingConfig
 {
     public function check(string $group, string $key, mixed $value)
@@ -29,20 +32,11 @@ class AllowSettingConfig
      */
     private function email(): array
     {
-        return [
-            "email_notification" => "in:on,off",
-            "async_sender" => "in:yes,no",
-            "sender_email" => "nullable|email",
-            "sender" => "nullable|min:1",
-            "domain" => "nullable|url",
-            "smtp_server" => "nullable|min:1",
-            "smtp_account" => "nullable|min:1",
-            "smtp_validation" => "in:yes,no",
-            "smtp_port" => "nullable|numeric",
-            "encryption" => "in:ssl,plain,tls",
-            "smtp_password" => "nullable|min:6",
-            "debug" => "in:off,normal,high",
-            "charset" => "in:utf8,gbk"
-        ];
+        return EmailConfigFieldEnum::checkRules();
+    }
+
+    private function browser(): array
+    {
+        return BrowserConfigFiledEnum::checkRules();
     }
 }

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

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Http\Requests\API\Container;
+
+use App\Http\Requests\CustomFieldRuleHelper;
+use App\Http\Requests\NamingRuleHelper;
+use App\Http\Requests\RuleHelper;
+use App\Models\Enums\ContainerACL;
+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, CustomFieldRuleHelper, NamingRuleHelper;
+
+    /**
+     * 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
+    {
+        $rules =  [
+            'library_id' => [
+                'required',
+                Rule::exists('libraries', '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,
+                    ]),
+                ])
+            ],
+            'name' => 'required|max:150',
+            'email_subject' => 'max:255',
+            'acl' => [
+                new Enum(ContainerACL::class),
+            ],
+            'whitelist' => $this->usersCompanyRules(),
+            'mailto' => $this->usersCompanyRules(),
+        ];
+
+        $containerRules = $this->customFieldRuleByGroup("container", ['doc_type', 'doc_stage']);
+
+        if ($this->has("naming_rule_id") && $this->get("naming_rule_id") > 0) {
+            $this->namingRuleCheck($this->get("naming_rule_id"));
+
+            $namingRules = $this->customFieldRuleByGroup($this->get("naming_rule_id"));
+
+            $rules = [...$rules, ... $namingRules];
+        }
+
+        return [...$containerRules, ...$rules];
+    }
+}

+ 2 - 10
app/Http/Requests/API/File/DownloadRequest.php → app/Http/Requests/API/File/DownloadZipRequest.php

@@ -6,7 +6,7 @@ use App\Http\Requests\RuleHelper;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Support\Facades\Storage;
 
-class DownloadRequest extends FormRequest
+class DownloadZipRequest extends FormRequest
 {
     use RuleHelper;
     /**
@@ -25,15 +25,7 @@ class DownloadRequest extends FormRequest
     public function rules(): array
     {
         return [
-            'url' => [
-                'required',
-                function ($attribute, $value, $fail) {
-                    // 检查文件是否存在默认存储对象系统中
-                    if (!Storage::exists($value)) {
-                        $fail('url file does not exist');
-                    }
-                },
-            ],
+            "ids" => "required|array",
         ];
     }
 }

+ 59 - 0
app/Http/Requests/API/File/FileUploadRequest.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\Http\Requests\API\File;
+
+use App\Models\Enums\FileObjectType;
+use App\Models\User;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Validation\Rules\Enum;
+use Illuminate\Validation\Rules\File;
+
+class FileUploadRequest 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
+    {
+        $rules =  [
+            "files.*" => [
+                'required',
+            ],
+
+            "object_type" => [
+                'required',
+                new Enum(FileObjectType::class),
+            ],
+            "object_id" => [
+                function ($attribute, $value, $fail) {
+                    $exist = FileObjectType::from($this->get("object_type"))
+                        ->modelBuilderAllowed($value)
+                        ->where("company_id", Auth::user()->company_id)
+                        ->where('id', $value)
+                        ->count();
+                    if (! $exist) {
+                        $fail('Resources without permission to access.');
+                    }
+                }
+            ],
+            "source" => "in:1,2",
+        ];
+
+        $rules['file.*'][] = $this->get("source", 1) == 1
+            ? File::types(['jpeg', 'png', 'gif'])->max("10mb")
+            : File::types(['txt', 'jpeg', 'png', 'gif', 'pdf', 'xls', 'xlsx', 'zip', 'wps', 'docx', 'doc'])->max("2gb");
+
+        return $rules;
+    }
+}

+ 35 - 0
app/Http/Requests/API/Task/AssignRequest.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: kelyliang
+ * Date: 2024/4/8
+ * Time: 上午 11:09
+ */
+
+namespace App\Http\Requests\API\Task;
+
+
+use App\Http\Requests\RuleHelper;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class AssignRequest extends FormRequest
+{
+    use RuleHelper;
+
+
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        return $rules =  [
+            'assign' => [
+                Rule::exists('users', 'id')->where($this->userCompanyWhere()),
+            ],
+        ];
+
+    }
+}

+ 1 - 1
app/Http/Requests/API/Task/BatchCreateItemRules.php

@@ -18,7 +18,7 @@ class BatchCreateItemRules
     {
         $rules =  [
             'requirement_id' => [
-                'required',
+                'nullable',
                 Rule::exists('requirements', 'id')->where($this->userCompanyWhere()),
             ],
             'asset_group_id' => [

+ 10 - 0
app/Http/Requests/API/Task/BatchCreateRequest.php

@@ -3,6 +3,7 @@
 namespace App\Http\Requests\API\Task;
 
 use App\Http\Requests\RuleHelper;
+use function DragonCode\Support\Http\exists;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Validation\Rule;
 
@@ -30,6 +31,15 @@ class BatchCreateRequest extends FormRequest
                 'required',
                 Rule::exists('projects', 'id')->where($this->userCompanyWhere()),
             ],
+            'parent_id' => [
+                'required',
+                Rule::when(
+                    $this->get('parent_id') != 0,
+                    Rule::exists('tasks', 'id')->where(function ($query) {
+                        $this->userCompanyWhere($query);
+                    })
+                ),
+            ],
             'items' => [
                 'required', 'array'
             ]

+ 24 - 2
app/Http/Requests/API/Task/CreateOrUpdateRequest.php

@@ -6,6 +6,7 @@ use App\Http\Requests\CustomFieldRuleHelper;
 use App\Http\Requests\NamingRuleHelper;
 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;
@@ -36,6 +37,7 @@ class CreateOrUpdateRequest extends FormRequest
                 Rule::exists('projects', 'id')->where($this->userCompanyWhere()),
             ],
             'requirement_id' => [
+                'nullable',
                 Rule::exists('requirements', 'id')->where($this->userCompanyWhere()),
             ],
             'naming_rule_id' => [
@@ -46,6 +48,7 @@ class CreateOrUpdateRequest extends FormRequest
                 ])
             ],
             'assign' => [
+                'nullable',
                 Rule::exists('users', 'id')->where($this->userCompanyWhere()),
             ],
             'name' => 'required|max:255',
@@ -59,8 +62,26 @@ class CreateOrUpdateRequest extends FormRequest
             'acl' => [
                 new Enum(TaskACL::class),
             ],
-            'whitelist' => $this->usersCompanyRules(),
-            'mailto' => $this->usersCompanyRules(),
+            'whitelist' => [
+                'array',
+                'nullable',
+                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.');
+                    }
+                }
+            ],
+            'mailto' => [
+                'array',
+                'nullable',
+                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.');
+                    }
+                }
+            ],
         ];
 
         $taskRules = $this->customFieldRuleByGroup("task", ['doc_type', 'task_type', 'doc_stage', "state", "suitability"]);
@@ -74,5 +95,6 @@ class CreateOrUpdateRequest extends FormRequest
         }
 
         return [...$rules, ...$taskRules];
+
     }
 }

+ 53 - 0
app/Http/Requests/API/User/AdminUpdateRequest.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Http\Requests\API\User;
+
+use App\Http\Requests\RuleHelper;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+
+class AdminUpdateRequest 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 [
+            'name' => 'max:100',
+            'email'=> 'email|unique:users',
+            'username'=>'max:30|unique:users',
+            'password' => 'min:6|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/', // 至少6位,包含大小写字母和数字,At least 6 digits, including upper and lower case letters and numbers
+            'auth_password' => [
+                'required',
+                function ($attribute, $value, $fail) {
+
+                    if (!Hash::check($value, Auth::user()->password)) {
+                        $fail("Wrong security authentication password!");
+                    }
+                }
+            ],
+            'phone'=>'nullable',
+            'gender'=>'in:1,0',
+            'address'=>'max:255',
+            'company_id'=>'exists:company,id',
+            'department_id'=>'exists:department,id',
+            'role_id'=>'exists:roles,id',
+        ];
+    }
+
+}

+ 71 - 0
app/Http/Requests/API/User/BatchCreateRequest.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Http\Requests\API\User;
+
+use App\Http\Requests\RuleHelper;
+use App\Models\Company;
+use App\Models\Department;
+use App\Models\Role;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Validation\Rule;
+use Illuminate\Support\Facades\Hash;
+
+class BatchCreateRequest 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
+    {
+//        $departmentIds=[];
+//        if(Auth::user()->super_admin){ //若是超级管理员可以查看所有部门,且添加
+//            $departmentIds = Department::withoutGlobalScopes()->pluck('id')->toArray();
+//
+//        }else{
+//            $departmentIds = Department::pluck('id')->toArray();
+//        }
+//        $roleIds = Role::pluck('id')->toArray();
+//
+//        $IdsAndDitto=$departmentIds;
+//        $IdsAndDitto[] = 'ditto';
+//
+//        $IdsAndDittoWithRole=$roleIds;
+//        $IdsAndDittoWithRole[]='ditto';
+
+        return [
+                'users.*.name' => 'required|max:100',
+                'users.*.email'=>  'required|email|unique:users',
+                'users.*.username'=>'required|max:30|unique:users',
+//            '*.pwd_is_ditto'=>'required|in:1,0',取消密码同上
+                'users.*.password' => 'required|min:6|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/',//至少6位,包含大小写字母和数字,At least 6 digits, including upper and lower case letters and numbers
+                'users.*.gender'=>'nullable|in:1,0',
+                'users.*.company_id'=>'required|exists:company,id',
+                'users.*.department_id'=>'required|exists:department,id',
+                'users.*.role_id'=> 'required|exists:roles,id',
+
+            'auth_password' => [
+                'required',
+                function ($attribute, $value, $fail) {
+
+                    if (!Hash::check($value, Auth::user()->password)) {
+                        $fail("Wrong security authentication password!");
+                    }
+                }
+            ],
+        ];
+    }
+
+}

+ 15 - 5
app/Http/Requests/API/User/CreateOrUpdateRequest.php → app/Http/Requests/API/User/CreateRequest.php

@@ -4,8 +4,10 @@ namespace App\Http\Requests\API\User;
 
 use App\Http\Requests\RuleHelper;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
 
-class CreateOrUpdateRequest extends FormRequest
+class CreateRequest extends FormRequest
 {
     use RuleHelper;
 
@@ -27,16 +29,24 @@ class CreateOrUpdateRequest extends FormRequest
 
         return [
             'name' => 'required|max:100',
-            'email'=> 'required|email|unique:users',
+            'email' => 'required|email|unique:users',
             'username'=>'required|max:30|unique:users',
-            'password' => 'required|min:6|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/', // 至少6位,包含大小写字母和数字
-            'confirm_password' => 'required|same:password', // 与password字段相同
-            'phone'=>'nullable|regex:/^\d{10}$/',
+            'password' => 'required|min:6|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/', // 至少6位,包含大小写字母和数字,At least 6 digits, including upper and lower case letters and numbers
+            'phone'=>'nullable|regex:/^\d{8,11}$/',
             'gender'=>'nullable|in:1,0',
             'address'=>'nullable|max:255',
             'company_id'=>'required|exists:company,id',
             'department_id'=>'required|exists:department,id',
             'role_id'=>'required|exists:roles,id',
+            'auth_password' => [
+                'required',
+                function ($attribute, $value, $fail) {
+
+                    if (!Hash::check($value, Auth::user()->password)) {
+                        $fail("Wrong security authentication password!");
+                    }
+                }
+            ],
         ];
     }
 

+ 52 - 0
app/Http/Requests/API/User/UpdateRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Http\Requests\API\User;
+
+use App\Http\Requests\RuleHelper;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+
+class UpdateRequest 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 [
+            'name' => 'required|max:100',
+            'email' => 'required|email',//为了编辑时没改邮箱的情况不用required|email|unique:users
+            'password' => 'nullable|min:6|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/', // 编辑时可为空,包含大小写字母和数字,At least 6 digits, including upper and lower case letters and numbers
+            'auth_password' => [
+                'required',
+                function ($attribute, $value, $fail) {
+
+                    if (!Hash::check($value, Auth::user()->password)) {
+                        $fail("Wrong security authentication password!");
+                    }
+                }
+            ],
+            'phone'=>'nullable|regex:/^\d{8,11}$/',
+            'gender'=>'nullable|in:1,0',
+            'address'=>'nullable|max:255',
+            'company_id'=>'exists:company,id',
+            'department_id'=>'exists:department,id',
+            'role_id'=>'required|exists:roles,id',
+
+        ];
+    }
+
+}

+ 1 - 1
app/Http/Resources/API/AssetReportResource.php

@@ -38,7 +38,7 @@ class AssetReportResource  extends JsonResource
             'id' => $this->id,
             'name' => $this->name,
             'code' => $this->code,
-            'description' => $this->description,
+            'description' => $this->description?(new \App\Services\File\ImageUrlService)->getImageUrl($this->description):null,
             'status' => $this->status,
             'created_by' => $this->created_by,
             'owner' => $this->owner,

+ 2 - 1
app/Http/Resources/API/AssetResource.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Resources\API;
 
+use App\Services\File\ImageUrlService;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 
@@ -18,7 +19,7 @@ class AssetResource extends JsonResource
             'id' => $this->id,
             'name' => $this->name,
             'code' => $this->code,
-            'description' => $this->description,
+            'description' => $this->description?(new \App\Services\File\ImageUrlService)->getImageUrl($this->description):null,
             'status' => $this->status,
             'created_by' => $this->created_by,
             'owner' => $this->owner,

+ 36 - 0
app/Http/Resources/API/ContainerDetailResource.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ContainerDetailResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        $content = $this->content($request->get("version", 0))->first();
+
+        return [
+            'id' => $this->id,
+            'name' => $content?->name,
+            'library' => new LibrarySimpleResource($this->library),
+            'naming_rule' => new NamingRuleSimpleResource($this->namingRule),
+            "mailto"  => $this->mailto,
+            "email_subject"  => $this->email_subject,
+            "doc_stage"  => $this->doc_stage,
+            "doc_type"  => $this->doc_type,
+            "description"  => $content?->description ? (new \App\Services\File\ImageUrlService)->getImageUrl($content?->description) : "",
+            "acl"  => $this->acl,
+            "whitelist"  => $this->whitelist,
+            "version" => $this->version,
+            "created_at"  => (string)$this->created_at,
+            "created_by" => new UserProfileResource($this->createdBy),
+        ];
+    }
+}

+ 1 - 0
app/Http/Resources/API/CustomFieldResource.php

@@ -22,6 +22,7 @@ class CustomFieldResource extends JsonResource
             'options' => $this->options,
             'type' => $this->type,
             'required' => $this->required,
+            'remark' => $this->remark,
         ];
     }
 }

+ 6 - 1
app/Http/Resources/API/DepartmentResource.php

@@ -19,7 +19,12 @@ class DepartmentResource extends JsonResource
           'id' => $this->id,
           'name' => $this->name,
           'parent_id'  => $this->parent_id,
-          'children' => $this->parent_id == 0 ? DepartmentResource::collection($this->children) : [],
+          'manager_id' => $this->manager_id,
+          'children' => $this->when($this->children->isNotEmpty(),function (){
+                return $this->children->map(function ($child){
+                    return new DepartmentResource($child);
+                })->all();
+            }),
         ];
     }
 }

+ 27 - 0
app/Http/Resources/API/FileByObjectResource.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class FileByObjectResource 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,
+            'extension' => $this->extension,
+            'size' => $this->size,
+            'created_by' => new UserProfileResource($this->createdBy),
+            'created_at' => (string)$this->created_at,
+            'version' => $this->version,
+        ];
+    }
+}

+ 27 - 0
app/Http/Resources/API/FileDownloadResource.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Support\Facades\Storage;
+
+class FileDownloadResource 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,
+            'download_url' => Storage::url($this->pathname),
+            'extension' => $this->extension,
+            'size' => $this->size,
+            'version' => $this->version,
+        ];
+    }
+}

+ 7 - 7
app/Http/Resources/API/ProjectGanttResource.php → app/Http/Resources/API/FileUploadSuccessResource.php

@@ -4,8 +4,9 @@ namespace App\Http\Resources\API;
 
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Support\Facades\Storage;
 
-class ProjectGanttResource extends JsonResource
+class FileUploadSuccessResource extends JsonResource
 {
     /**
      * Transform the resource into an array.
@@ -15,12 +16,11 @@ class ProjectGanttResource extends JsonResource
     public function toArray(Request $request): array
     {
         return [
-            "id" => $this->id,
-            "name" => $this->name,
-            "status" => $this->status,
-            "assign" => new UserProfileResource($this->assignTo),
-            "begin" => (string)$this->begin,
-            "end" => (string)$this->end,
+            'id' => $this->id,
+            'title' => $this->title,
+            'url' => Storage::url($this->pathname),
+            'extension' => $this->extension,
+            'size' => $this->extension,
         ];
     }
 }

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

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

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

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

+ 1 - 1
app/Http/Resources/API/PlanResource.php

@@ -24,7 +24,7 @@ class PlanResource extends JsonResource
             'project_total' => $this->projects()->count(),
             'begin' => $this->begin,
             'end' => $this->end,
-            'description' => $this->description,
+            'description' => $this->description?(new \App\Services\File\ImageUrlService)->getImageUrl($this->description):null,
             //'children' => $this->parent_id == 0 ? PlanResource::collection($this->children) : [],
             'children' => $this->when($this->children->isNotEmpty(),function (){
                 return $this->children->map(function ($child){

+ 1 - 1
app/Http/Resources/API/ProjectDetailResource.php

@@ -41,7 +41,7 @@ class ProjectDetailResource extends JsonResource
             'assets' =>$assetIds,
             'plans' =>$plansIds,
             //'whitelist' => $this->whitelist,
-            'description' => $this->description,
+            'description' => $this->description?(new \App\Services\File\ImageUrlService)->getImageUrl($this->description):null,
         ];
     }
 }

+ 1 - 1
app/Http/Resources/API/RequirementResource.php

@@ -25,7 +25,7 @@ class RequirementResource extends JsonResource
             'reviewed_by' => $this->reviewed_by,
             'priority' => $this->priority,
             'note' => $this->note,
-            'description' => $this->description,
+            'description' => $this->description?(new \App\Services\File\ImageUrlService)->getImageUrl($this->description):null,
             'acceptance' => $this->acceptance,
             'mailto' => $this->mailto,
             'created_by' => new UserProfileResource($this->createdBy),

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

@@ -17,6 +17,7 @@ class RequirementSimpleResource extends JsonResource
         return [
             'id' => $this->id,
             'title' => $this->title,
+            'description' => $this->description,
         ];
     }
 }

+ 2 - 1
app/Http/Resources/API/TaskDetailResource.php

@@ -24,13 +24,14 @@ class TaskDetailResource extends JsonResource
             "requirement_group_id"=> $this->requirement_group_id,
             "naming_rule_id" => $this->naming_rule_id,
             "naming_rule" => new NamingRuleSimpleResource($this->namingRule),
+            "naming_custom_fields" => $this->naming_rules,
             "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,
+            'description' => $this->description?(new \App\Services\File\ImageUrlService)->getImageUrl($this->description):null,
             "begin" => $this->begin,
             "end" => $this->end,
             "mailto"  => $this->mailto,

+ 6 - 1
app/Http/Resources/API/TaskResource.php

@@ -23,7 +23,12 @@ class TaskResource extends JsonResource
             "status" => $this->status,
             "assign_to" => new UserProfileResource($this->assignTo),
             "created_by" => new UserProfileResource($this->createdBy),
-            "children" => TaskResource::collection($this->children),
+            //"children" => TaskResource::collection($this->children),
+            'children' => $this->when($this->children->isNotEmpty(),function (){
+                return $this->children->map(function ($child){
+                    return new TaskResource($child);
+                })->all();
+            }),
         ];
     }
 }

+ 1 - 0
app/Http/Resources/API/UserInfoResource.php

@@ -27,6 +27,7 @@ class UserInfoResource extends JsonResource
             'company' => new SimpleCompanyResource($this->company),
             'department' =>new SimpleDepartmentResource($this->department),
             'role' => new RoleResource($this->role),
+            'status' =>$this->status,
         ];
     }
 }

+ 35 - 0
app/Http/Resources/API/UserSimpleResource.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: kelyliang
+ * Date: 2024/4/17
+ * Time: 下午 04:35
+ */
+
+namespace App\Http\Resources\API;
+
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class UserSimpleResource  extends JsonResource
+{
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+            'email' => $this->email,
+            'username' => $this->username,
+            'phone' => $this->phone,
+            'gender' => $this->gender,
+            'created_at' => (string)$this->created_at,
+            'created_by' => new UserProfileResource($this->createdBy),
+            'company' => $this->company_id,
+            'department' =>$this->department_id,
+            'role' => $this->role_id,
+            'role_name' => $this->role->name,
+            'status' =>$this->status,
+        ];
+    }
+}

+ 72 - 0
app/Listeners/SendActionBrowserNotification.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Listeners;
+
+use App\Events\ObjectActionCreate;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\NotificationObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\Notification;
+use App\Models\NotificationRecord;
+use App\Repositories\ConfigRepository;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Queue\InteractsWithQueue;
+
+class SendActionBrowserNotification implements ShouldQueue
+{
+    /**
+     * Create the event listener.
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Handle the event.
+     */
+    public function handle(ObjectActionCreate $event): void
+    {
+        if(! ConfigRepository::openBrowserNotification()){
+            return;
+        }
+        $actionObjectType = ActionObjectType::tryFrom($event->action->object_type);
+
+        $object = $actionObjectType->modelBuilder()->find($event->action->object_id);
+        if (! $object) {
+            return;
+        }
+
+        $userIds = match ($actionObjectType) {
+            ActionObjectType::TASK => $object->assign > 0 ? [$object->assign] : [],
+            default => [],
+        };
+
+        if (! $userIds) {
+            return;
+        }
+
+        $notification = Notification::query()->create([
+            'object_type' => NotificationObjectType::ACTION->value,
+            'object_id' => $event->action->id,
+        ]);
+
+        foreach ($userIds as $userId) {
+            NotificationRecord::query()->create([
+                'notification_id' => $notification->id,
+                'user_id' => $userId,
+            ]);
+        }
+    }
+
+    public function shouldQueue(ObjectActionCreate $event): bool
+    {
+        $actionObjectType = ActionObjectType::tryFrom($event->action->object_type);
+        $objectAction = ObjectAction::tryFrom($event->action->action);
+        if (! $actionObjectType || !$objectAction) {
+            return false;
+        }
+
+        return in_array($objectAction->value, $event->notificationSetting[$actionObjectType->value]['email'] ?? []);
+    }
+}

+ 40 - 0
app/Listeners/SendActionEmailNotification.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Listeners;
+
+use App\Events\ObjectActionCreate;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Services\Notification\ActionEmail\ActionEmailService;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Queue\InteractsWithQueue;
+
+class SendActionEmailNotification implements ShouldQueue
+{
+    /**
+     * Create the event listener.
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Handle the event.
+     */
+    public function handle(ObjectActionCreate $event): void
+    {
+        (new ActionEmailService($event->action))->send();
+    }
+
+    public function shouldQueue(ObjectActionCreate $event): bool
+    {
+        $actionObjectType = ActionObjectType::tryFrom($event->action->object_type);
+        $objectAction = ObjectAction::tryFrom($event->action->action);
+        if (! $actionObjectType || !$objectAction) {
+            return false;
+        }
+
+        return in_array($objectAction->value, $event->notificationSetting[$actionObjectType->value]['email'] ?? []);
+    }
+}

+ 56 - 0
app/Mail/RequirementAction.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Mail;
+
+use App\Models\Enums\ObjectAction;
+use App\Models\Requirement;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class RequirementAction extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(protected  Requirement $requirement, protected ObjectAction $objectAction)
+    {
+        //
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Requirement Action',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            //view: 'view.name',
+            markdown: 'emails.actions.requirement',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 56 - 0
app/Mail/TaskAction.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Mail;
+
+use App\Models\Enums\ObjectAction;
+use App\Models\Task;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class TaskAction extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(protected Task $task, protected ObjectAction $objectAction)
+    {
+        //
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Task Action',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            //view: 'view.name',
+            markdown: 'emails.actions.task',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 23 - 0
app/ModelFilters/AssetGroupFilter.php

@@ -0,0 +1,23 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: kelyliang
+ * Date: 2024/4/2
+ * Time: 上午 10:25
+ */
+
+namespace App\ModelFilters;
+
+
+use EloquentFilter\ModelFilter;
+
+class AssetGroupFilter extends ModelFilter
+{
+    public $relations = [];
+
+    public function name($name): ModelFilter
+    {
+        return $this->where('name', 'like', "%{$name}%");
+    }
+
+}

+ 16 - 0
app/ModelFilters/ContainerFilter.php

@@ -0,0 +1,16 @@
+<?php 
+
+namespace App\ModelFilters;
+
+use EloquentFilter\ModelFilter;
+
+class ContainerFilter 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 = [];
+}

+ 4 - 0
app/ModelFilters/CustomFieldFilter.php

@@ -19,6 +19,10 @@ class CustomFieldFilter extends ModelFilter
         return $this->where("group", $group);
     }
 
+    public function key($key){
+        return $this->where("key", "like", "%$key%");
+    }
+
     public function batch($batch)
     {
         $in = [];

+ 43 - 0
app/ModelFilters/NotificationFilter.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\ModelFilters;
+
+use EloquentFilter\ModelFilter;
+
+class NotificationFilter 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 = [];
+
+    public function startReadAt($date)
+    {
+        return $this->where("notification_records.read_at", ">=", $date);
+    }
+
+    public function endReadAt($date)
+    {
+        return $this->where("notification_records.read_at", "<", $date);
+    }
+
+    public function startNotificationAt($date)
+    {
+        return $this->where("notifications.created_at", ">=", $date);
+    }
+
+    public function endNotificationAt($date)
+    {
+        return $this->where("notifications.created_at", "<", $date);
+    }
+
+    public function readStatus($status)
+    {
+        return !$status
+            ? $this->whereNull("notification_records.read_at")
+            : $this->whereNotNull("notification_records.read_at");
+    }
+}

+ 2 - 27
app/ModelFilters/RequirementFilter.php

@@ -27,35 +27,10 @@ class RequirementFilter extends ModelFilter
     }
 
     public function assetId($assetId){
-        $asset = Asset::find($assetId);
-        if (!$asset || !$asset->children()->first()) {
-            // 如果没有子级,只返回该分组的需求
-            return $this->where('asset_id', $assetId);
-        }
-        $allRelatedIds = $this->getAllRelatedIds($assetId);
-        array_push($allRelatedIds, $asset->id);
-        //把父级id去重
-        $unIds=array_unique($allRelatedIds);
-        return $this->whereIn('asset_id',$unIds);
+        $assetId = Asset::query()->where('path','like',"%," .$assetId.",%")->pluck('id');
+        return $this->whereIn('asset_id',$assetId);
     }
-    //递归获取资产
-    private function getAllRelatedIds($assetId)
-    {
-        $asset = Asset::find($assetId);
-
-        if (!$asset) {
-            return [];
-        }
 
-        // 获取所有子级ID
-        $childIds = $asset->children()->pluck('id')->toArray();
-        // 递归获取所有子级的子级ID
-        foreach ($childIds as $childId) {
-            $childIds = array_merge($childIds, $this->getAllRelatedIds($childId));
-        }
-
-        return $childIds;
-    }
 
     public function group($groupId){
         if($groupId==0){

+ 22 - 0
app/ModelFilters/UserFilter.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: kelyliang
+ * Date: 2024/4/17
+ * Time: 下午 04:15
+ */
+
+namespace App\ModelFilters;
+
+
+use EloquentFilter\ModelFilter;
+
+class UserFilter extends ModelFilter
+{
+    public $relations = [];
+
+    public function name($name): ModelFilter
+    {
+        return $this->where('name', 'like', "%$name%");
+    }
+}

+ 2 - 1
app/Models/AssetGroup.php

@@ -3,13 +3,14 @@
 namespace App\Models;
 
 use App\Models\Scopes\CompanyScope;
+use EloquentFilter\Filterable;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
 
 class AssetGroup extends Model
 {
-    use HasFactory, SoftDeletes;
+    use HasFactory, SoftDeletes, Filterable;
 
     protected $fillable = [
         'company_id',

+ 12 - 1
app/Models/Company.php

@@ -3,12 +3,14 @@
 namespace App\Models;
 
 use EloquentFilter\Filterable;
+use Illuminate\Database\Eloquent\Casts\Attribute;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
 
 class Company extends Model
 {
-    use HasFactory,Filterable;
+    use HasFactory,Filterable,SoftDeletes;
 
     protected $table = 'company';
 
@@ -20,4 +22,13 @@ class Company extends Model
     protected $guarded = [
         'id'
     ];
+
+    protected function storageSize(): Attribute
+    {
+        return Attribute::get(
+            fn() => $this->storage_limit_size > 0
+                ? $this->storage_limit_size
+                : config("autocde.company_default_storage_limit_size")  * 1024 * 1024
+        );
+    }
 }

+ 56 - 0
app/Models/Container.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Models;
+
+use App\Models\Scopes\CompanyScope;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class Container extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $fillable = [
+        'name', 'library_id', 'naming_rule_id', 'naming_rules', 'mailto', 'email_subject', 'acl', 'whitelist',
+        'doc_stage', 'doc_type'
+    ];
+
+    protected $casts = [
+        'mailto' => 'array',
+        'naming_rules' => 'array',
+    ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope(new CompanyScope);
+    }
+
+    public function scopeAllowed(Builder $query, string $id = null)
+    {
+
+    }
+
+    public function library(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(Library::class, "library_id");
+    }
+
+    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 content(int $version = 0): \Illuminate\Database\Eloquent\Relations\HasOne
+    {
+        $version = $version > 0 ? $version : $this->version;
+
+        return $this->hasOne(ContainerContent::class, "container_id")->where("version", $version);
+    }
+}

+ 15 - 0
app/Models/ContainerContent.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class ContainerContent extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'container_id', 'description', 'files', 'version', 'name', 'created_by',
+    ];
+}

+ 1 - 1
app/Models/CustomField.php

@@ -13,7 +13,7 @@ class CustomField extends Model
     public $timestamps = false;
 
     protected $fillable = [
-        'group', 'key', 'options', 'type', 'required', 'label'
+        'group', 'key', 'options', 'type', 'required', 'label','remark'
     ];
 
     protected $casts = [

+ 23 - 2
app/Models/Enums/ActionObjectType.php

@@ -3,11 +3,13 @@
 namespace App\Models\Enums;
 
 use App\Models\Asset;
+use App\Models\Container;
 use App\Models\Plan;
 use App\Models\Project;
 use App\Models\Requirement;
 use App\Models\Task;
-use App\Services\History\Detector\DetectorContact;
+use App\Services\History\Detector\ContainerContentDetector;
+use App\Services\History\Detector\ContainerDetector;
 use App\Services\History\Detector\ProjectDetector;
 use App\Services\History\Detector\RequirementDetector;
 use App\Services\History\Detector\TaskDetector;
@@ -18,12 +20,16 @@ enum ActionObjectType: string
 
     case PROJECT = "project";
 
-    case REQUIREMENT="requirement";
+    case REQUIREMENT = "requirement";
 
     case TASK = "task";
 
     case PLAN = "plan";
 
+    case CONTAINER = "container";
+
+    case CONTAINER_CONTENT = "container_content";
+
     public function modelBuilder(): \Illuminate\Database\Eloquent\Builder
     {
         return match ($this) {
@@ -32,6 +38,19 @@ enum ActionObjectType: string
             self::TASK => Task::query(),
             self::PLAN => Plan::query(),
             self::REQUIREMENT => Requirement::query(),
+            self::CONTAINER => Container::query(),
+        };
+    }
+
+    public function modelBuilderAllowed(string $id = null): \Illuminate\Database\Eloquent\Builder
+    {
+        return match ($this) {
+            self::ASSET => Asset::query()->allowed(),
+            self::PROJECT => Project::query()->allowed($id),
+            self::TASK => Task::query()->allowed($id),
+            self::PLAN => Plan::query(),
+            self::REQUIREMENT => Requirement::query(),
+            self::CONTAINER => Container::query()->allowed($id),
         };
     }
 
@@ -50,6 +69,8 @@ enum ActionObjectType: string
             ActionObjectType::PROJECT => ProjectDetector::class,
             ActionObjectType::REQUIREMENT => RequirementDetector::class,
             ActionObjectType::TASK => TaskDetector::class,
+            ActionObjectType::CONTAINER => ContainerDetector::class,
+            ActionObjectType::CONTAINER_CONTENT => ContainerContentDetector::class,
             default => null
         };
     }

+ 12 - 0
app/Models/Enums/ConfigGroup.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum ConfigGroup: string
+{
+    case EMAIL = "email";
+
+    case BROWSER = "browser";
+
+    case MESSAGE_NOTIFICATION = "message_notification";
+}

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

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

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

@@ -4,7 +4,9 @@ namespace App\Models\Enums;
 
 enum CustomFieldGroup: string
 {
-    case PRIVATE = 'private';
+    case TASK = 'task';
 
-    case CUSTOM = 'custom';
+    case PROJECT = 'project';
+
+    case CONTAINER = 'container';
 }

+ 51 - 0
app/Models/Enums/FileObjectType.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Models\Enums;
+
+use App\Models\Action;
+use App\Models\Asset;
+use App\Models\Plan;
+use App\Models\Project;
+use App\Models\Requirement;
+use App\Models\Task;
+
+enum FileObjectType: string
+{
+    case ASSET ="asset";
+
+    case PROJECT = "project";
+
+    case REQUIREMENT="requirement";
+
+    case TASK = "task";
+
+    case ACTION = "action";
+
+    case PLAN = "plan";
+
+    case CONTAINER = "container";
+
+    public function modelBuilder(): \Illuminate\Database\Eloquent\Builder
+    {
+        return match ($this) {
+            self::ASSET => Asset::query(),
+            self::PROJECT => Project::query(),
+            self::TASK => Task::query(),
+            self::REQUIREMENT => Requirement::query(),
+            self::ACTION => Action::query(),
+            self::PLAN => Plan::query(),
+        };
+    }
+
+    public function modelBuilderAllowed(string $id = null): \Illuminate\Database\Eloquent\Builder
+    {
+        return match ($this) {
+            self::ASSET => Asset::query(),
+            self::PROJECT => Project::query()->allowed($id),
+            self::TASK => Task::query()->allowed($id),
+            self::REQUIREMENT => Requirement::query(),
+            self::ACTION => Action::query(),
+            self::PLAN => Plan::query(),
+        };
+    }
+}

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

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum FileSource: int
+{
+    case ATTACHMENT = 1;
+
+    case EDITOR = 2;
+}

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

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum NotificationObjectType: string
+{
+    case ANNOUNCEMENT = "announcement"; //公告
+
+    case ACTION = "action"; //Action 通知
+}

+ 12 - 0
app/Models/Enums/NotificationStatus.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum NotificationStatus: string
+{
+    case DRAFT = "draft";
+
+    case RELEASE = "release";
+
+    case CANCEL = "cancel";
+}

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

@@ -75,4 +75,27 @@ enum ObjectAction: string
     case BATCH_CREATE_TASK = "batchCreateTask";
 
     case DELETE_CHILDREN_TASK = "deleteChildrenTask";
+
+    case WAITED ="waited";
+
+    public static function messageNotificationItems()
+    {
+        return [
+            ActionObjectType::REQUIREMENT->value => [
+                self::CREATED,
+                self::EDITED,
+                self::STARTED,
+                self::CHANGED,
+                self::CLOSED,
+            ],
+            ActionObjectType::TASK->value => [
+                ObjectAction::STARTED,
+                ObjectAction::PAUSED,
+                ObjectAction::CLOSED,
+                ObjectAction::DONE,
+                ObjectAction::CANCELED,
+                ObjectAction::EDITED,
+            ]
+        ];
+    }
 }

+ 25 - 0
app/Models/File.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Models;
+
+use App\Models\Scopes\CompanyScope;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class File extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $guarded = ['id'];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope(new CompanyScope);
+    }
+
+    public function createdBy()
+    {
+        return $this->belongsTo(User::class, "created_by");
+    }
+}

+ 14 - 0
app/Models/Notification.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Models;
+
+use EloquentFilter\Filterable;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Notification extends Model
+{
+    use HasFactory, Filterable;
+
+    protected $guarded = ['id'];
+}

+ 13 - 0
app/Models/NotificationRecord.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class NotificationRecord extends Model
+{
+    use HasFactory;
+
+    protected $guarded = ['id'];
+}

+ 1 - 1
app/Models/Project.php

@@ -34,7 +34,7 @@ class Project extends Model
     public function scopeAllowed(Builder $query, string $id = null): void
     {
         $projectIds = Project::query()->leftJoin("team_members", "projects.id", "=", "team_members.project_id")
-            ->filter(request()->query())
+            ->when($id == null, fn($query) => $query->filter(request()->query()))
             ->when($id, fn($query) => $query->where("projects.id", $id))
             ->where(function ($query) {
                 $query->where("team_members.user_id", Auth::id())

+ 1 - 1
app/Models/Task.php

@@ -39,7 +39,7 @@ class Task extends Model
     public function scopeAllowed(Builder $query, string $id = null): void
     {
         $taskIds = Task::query()->leftJoin("team_members", "tasks.project_id", "=", "team_members.project_id")
-            ->filter(request()->query())
+            ->when($id == null, fn($query) => $query->filter(request()->query()))
             ->when($id, fn($query) => $query->where("tasks.id", $id))
             ->where(function($query) {
                 $query->where("team_members.user_id", Auth::id())

+ 8 - 1
app/Models/User.php

@@ -4,8 +4,10 @@ namespace App\Models;
 
 // use Illuminate\Contracts\Auth\MustVerifyEmail;
 use App\Models\Scopes\CompanyScope;
+use EloquentFilter\Filterable;
 use Illuminate\Database\Eloquent\Casts\Attribute;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\SoftDeletes;
 use Illuminate\Foundation\Auth\User as Authenticatable;
 use Illuminate\Notifications\Notifiable;
 use Laravel\Sanctum\HasApiTokens;
@@ -13,7 +15,7 @@ use Spatie\Permission\Traits\HasRoles;
 
 class User extends Authenticatable
 {
-    use HasApiTokens, HasFactory, Notifiable, HasRoles;
+    use HasApiTokens, HasFactory, Notifiable, HasRoles, SoftDeletes,Filterable;
 
     protected string $guard_name = 'api';
 
@@ -30,6 +32,11 @@ class User extends Authenticatable
         'company_id',
         'department_id',
         'role_id',
+        'created_by',
+        'gender',
+        'address',
+        'phone',
+        'status',
     ];
 
     /**

+ 8 - 1
app/Providers/EventServiceProvider.php

@@ -2,6 +2,9 @@
 
 namespace App\Providers;
 
+use App\Events\ObjectActionCreate;
+use App\Listeners\SendActionBrowserNotification;
+use App\Listeners\SendActionEmailNotification;
 use Illuminate\Auth\Events\Registered;
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@@ -18,6 +21,10 @@ class EventServiceProvider extends ServiceProvider
         Registered::class => [
             SendEmailVerificationNotification::class,
         ],
+        ObjectActionCreate::class => [
+            SendActionBrowserNotification::class,
+            SendActionEmailNotification::class,
+        ]
     ];
 
     /**
@@ -25,7 +32,7 @@ class EventServiceProvider extends ServiceProvider
      */
     public function boot(): void
     {
-        //
+
     }
 
     /**

+ 40 - 5
app/Repositories/ActionRepository.php

@@ -2,17 +2,24 @@
 
 namespace App\Repositories;
 
+use App\Events\ObjectActionCreate;
 use App\Models\Action;
+use App\Models\Container;
 use App\Models\Enums\ActionObjectType;
 use App\Models\Enums\ObjectAction;
 use App\Models\History;
+use App\Models\File;
 use App\Models\Project;
 use App\Models\Requirement;
 use App\Models\Task;
-use App\Services\History\ModelChangeDetector;
+use App\Services\File\ImageUrlService;
 use Carbon\Carbon;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use Overtrue\CosClient\Client;
+use Overtrue\Flysystem\Cos\CosAdapter;
 
 class ActionRepository
 {
@@ -29,7 +36,7 @@ class ActionRepository
         $action = Action::query()->create([
             "object_id" => $objectId,
             "object_type" => $objectType->value,
-            "action" => $action,
+            "action" => $action->value,
             "project_id" => $projectId,
             "comment" => $comment,
             "extra_fields" => $extraFields ?: null,
@@ -40,6 +47,8 @@ class ActionRepository
             History::query()->insert(array_map(fn($change) => [...$change, 'action_id' => $action->id], $objectChanges));
         }
 
+        ObjectActionCreate::dispatch($action);
+
         return $action;
     }
 
@@ -100,6 +109,25 @@ class ActionRepository
         );
     }
 
+    public static function createByContainer(
+        Container    $container,
+        ObjectAction $action,
+        string       $comment = null,
+        array        $extraFields = [],
+        array        $objectChanges = [],
+    ): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder
+    {
+        return self::create(
+            $container->id,
+            ActionObjectType::CONTAINER,
+            $action,
+            $container->library?->project_id,
+            $comment,
+            $extraFields,
+            $objectChanges,
+        );
+    }
+
     public static function latestDynamic(Project $project)
     {
         $actions = Action::query()
@@ -124,8 +152,15 @@ class ActionRepository
         $labelKey = sprintf("action-labels.label.%s", $action['action']);
         $objectLabelKey = sprintf("action-labels.object_type.%s", $action['object_type']);
 
-        $cratedAt = Carbon::parse($action['created_at']);
+        $cratedAt = Carbon::parse($action['created_at'])->timezone('Asia/Shanghai');
+        $imgService=new ImageUrlService();
+        //存在评论时才会执行获取图片路径
+        if(isset($action['comment'])){
+            $action['comment']=$imgService->getImageUrl($action['comment']);
+        }
+
         return [
+            'id' => $action['id'],
             'time' => $cratedAt->toTimeString(),
             'created_at' => $cratedAt->toDateTimeString(),
             'created_by' => [
@@ -292,8 +327,8 @@ class ActionRepository
             $item = [
                 'field' => $history->field,
                 'field_label' => app('translator')->has($labelKey) ? __($labelKey) : $history->field,
-                'new' => self::coverFieldValue($detector, $history->field, $history->new),
-                'old' => self::coverFieldValue($detector, $history->field, $history->old),
+                'new' => self::coverFieldValue($detector, $history->field ?? '', $history->new ?? ''),
+                'old' => self::coverFieldValue($detector, $history->field ?? '', $history->old ?? ''),
                 'diff' => (string)$history->diff,
             ];
 

+ 41 - 0
app/Repositories/ConfigRepository.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Repositories;
+
+use App\Models\Config;
+use App\Models\Enums\ConfigGroup;
+use App\Repositories\Enums\EmailConfigFieldEnum;
+use App\Repositories\Enums\BrowserConfigFiledEnum;
+
+class ConfigRepository
+{
+    public static function openEmailNotification(): bool
+    {
+        return self::getConfigItem(ConfigGroup::EMAIL->value, EmailConfigFieldEnum::OPEN_EMAIL_NOTIFICATION->value) == "on";
+    }
+
+    public static function openBrowserNotification(): bool{
+        return self::getConfigItem(ConfigGroup::BROWSER->value, BrowserConfigFiledEnum::OPEN_BROWSER_NOTIFICATION->value) == "on";
+    }
+
+    protected static function getConfigItem(string $group, string $key)
+    {
+        $config = Config::query()->where('group', $group)->where("key", $key)->first();
+
+        return $config?->value;
+    }
+
+    public static function emailDynamicSetting()
+    {
+        $configs = Config::query()->where('group', ConfigGroup::EMAIL->value)->get();
+
+        $fieldRelations = EmailConfigFieldEnum::fieldRelations();
+        foreach ($configs as $config) {
+            if(! isset($fieldRelations[$config->key]) || !$config->value) {
+                continue;
+            }
+
+            \Illuminate\Support\Facades\Config::set($fieldRelations[$config->key], $config->value);
+        }
+    }
+}

+ 24 - 0
app/Repositories/Enums/BrowserConfigFiledEnum.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: kelyliang
+ * Date: 2024/4/15
+ * Time: 下午 05:18
+ */
+
+namespace App\Repositories\Enums;
+
+
+enum BrowserConfigFiledEnum:string
+{
+    case OPEN_BROWSER_NOTIFICATION = "browser_notification"; //站内信通知
+
+    public static function checkRules(): array
+    {
+        return [
+            "browser_notification" => "in:on,off",
+            //轮询时间
+            "polling_time" => "nullable|numeric",
+        ];
+    }
+}

+ 41 - 0
app/Repositories/Enums/EmailConfigFieldEnum.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Repositories\Enums;
+
+enum EmailConfigFieldEnum: string
+{
+    case OPEN_EMAIL_NOTIFICATION = "email_notification"; //邮件通知
+
+    public static function checkRules(): array
+    {
+        return [
+            "email_notification" => "in:on,off",
+            "async_sender" => "in:yes,no",
+            "sender_email" => "nullable|email",
+            "sender" => "nullable|min:1",
+            "domain" => "nullable|url",
+            "smtp_server" => "nullable|min:1",
+            "smtp_account" => "nullable|min:1",
+            "smtp_validation" => "in:yes,no",
+            "smtp_port" => "nullable|numeric",
+            "encryption" => "in:ssl,plain,tls",
+            "smtp_password" => "nullable|min:6",
+            "debug" => "in:off,normal,high",
+            "charset" => "in:utf8,gbk"
+        ];
+    }
+
+    public static function fieldRelations(): array
+    {
+        return [
+            "sender_email" => "mail.from.address",
+            "sender" => "mail.from.name",
+            "domain" => "mail.mailers.smtp.url",
+            "smtp_server" => "mail.mailers.smtp.host",
+            "smtp_account" => "mail.mailers.smtp.username",
+            "smtp_port" => "mail.mailers.smtp.port",
+            "encryption" => "mail.mailers.smtp.encryption",
+            "smtp_password" => "mail.mailers.smtp.password",
+        ];
+    }
+}

+ 50 - 0
app/Services/File/FileAssociationService.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Services\File;
+
+use App\Models\Enums\FileObjectType;
+use App\Models\Enums\FileSource;
+use App\Models\File;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Auth;
+
+class FileAssociationService
+{
+    /**
+     * @param array $fileIds
+     * @param string $objectId
+     * @param FileObjectType $fileObjectType
+     * @return void
+     */
+    public function association(array $fileIds, string $objectId, FileObjectType $fileObjectType)
+    {
+        if (! $fileIds) {
+            return;
+        }
+
+        $files = File::query()
+            ->whereIn("id", $fileIds)
+            ->where("created_by", Auth::id())
+            ->whereNull("object_id")
+            ->where("object_type", $fileObjectType->value)
+            ->where("created_at", ">=", Carbon::now()->subMinutes(10))
+            ->orderBy("created_at")
+            ->get();
+
+        foreach ($files as $file) {
+            if ($file->source == FileSource::ATTACHMENT->value) {
+                $version = File::query()
+                    ->where("created_at", ">=", Carbon::now()->subMinutes(10))
+                    ->where("created_by", Auth::id())
+                    ->where("object_id", $objectId)
+                    ->where("object_type", $fileObjectType->value)
+                    ->where("title", $file->title)
+                    ->count();
+                $file->version = $version + 1;
+            }
+
+            $file->object_id = $objectId;
+            $file->save();
+        }
+    }
+}

+ 87 - 0
app/Services/File/ImageUrlService.php

@@ -0,0 +1,87 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: kelyliang
+ * Date: 2024/4/10
+ * Time: 上午 10:45
+ */
+
+namespace App\Services\File;
+
+
+use App\Models\File;
+use DOMDocument;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+
+class ImageUrlService
+{
+    public function interceptImageUrl(string $comment): bool|string|null
+    {
+        if (! $comment) {
+            return null;
+        }
+
+        $newComment = null;
+        $dom = new DOMDocument();
+        libxml_use_internal_errors(true); // 禁用错误报告
+        $dom->loadHTML($comment, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
+        libxml_clear_errors();
+        // 获取所有的 img 标签
+        $imgTags = $dom->getElementsByTagName('img');
+        if($imgTags->length>0){
+            // 遍历所有的 img 标签
+            foreach ($imgTags as $imgTag) {
+                $src = $imgTag->getAttribute('src');
+                // 查找 &fild_ 的位置
+                $fildPos = strpos($src, 'fild_');
+                // 如果找到了 &fild_,则截取该位置之前的数据
+                if ($fildPos !== false) {
+                    $src = substr($src, $fildPos); // 保留 &fild_ 及之后的部分
+                } else {
+                    // 如果没有找到 &fild_,则清空整个 src 属性(或者根据需求处理)
+                    $src = '';
+                }
+                // 设置修改后的 src 属性
+                $imgTag->setAttribute('src', $src);
+            }
+            $newComment = $dom->saveHTML();
+        }
+
+
+        return $newComment??$comment;
+    }
+
+    public function getImageUrl(string $comment){
+        if (! $comment) {
+            return $comment;
+        }
+
+        $newComment = null;
+        preg_match_all('/<img\s+[^>]*src=[\'"]([^\'"]*)[\'"][^>]*>/i', $comment, $matches);
+        $ids = [];
+
+        // 遍历匹配到的 src 属性值
+        foreach ($matches[1] as $src) {
+            // 使用正则表达式从 src 属性值中提取 fild_ 后面的 ID
+            if (preg_match('/fild_(\d+)/', $src, $idMatch)) {
+                $ids[] = $idMatch[1]; // 将提取到的 ID 添加到数组中
+            }
+        }
+
+        if(!empty($ids)){
+            $files=File::query()->whereIn('id',$ids)->get();
+            $search = [];
+            $replace = [];
+            foreach ($files as $file){
+                $url=Storage::url($file->pathname);
+                $placeholder = "fild_" . $file->id; // 生成占位符
+                $search[] = $placeholder; // 将占位符添加到 $search 数组
+                $replace[] = $url."&fild_".$file->id; // 将替换 URL 添加到 $replace 数组
+            }
+            $newComment=Str::replace($search, $replace, $comment);
+        }
+
+        return $newComment??$comment;
+    }
+}

+ 10 - 0
app/Services/History/Converter/ContainerConverter.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Services\History\Converter;
+
+use App\Models\Container;
+
+class ContainerConverter extends ModelConverter
+{
+    protected static string $modelClassName = Container::class;
+}

+ 30 - 0
app/Services/History/Detector/ContainerContentDetector.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Services\History\Detector;
+
+
+class ContainerContentDetector extends DetectorAbstract
+{
+    public static function fields(): array
+    {
+        return [
+            'name',
+            'description',
+            'files',
+        ];
+    }
+
+    public static function diffFields(): array
+    {
+        return [
+            'description'
+        ];
+    }
+
+    public static function converters(): array
+    {
+        return [
+
+        ];
+    }
+}

+ 56 - 0
app/Services/History/Detector/ContainerDetector.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Services\History\Detector;
+
+use App\Models\Enums\CustomFieldGroup;
+use App\Services\History\Converter\ContainerConverter;
+use App\Services\History\Converter\CustomFieldSelectConverter;
+use App\Services\History\Converter\EmailConverter;
+use App\Services\History\Converter\ModelEnumConverter;
+use App\Services\History\Converter\NamingRuleConverter;
+use App\Services\History\Converter\WhitelistConverter;
+
+class ContainerDetector extends DetectorAbstract
+{
+    public static function fields(): array
+    {
+        return [
+            'library_id',
+            'naming_rule_id',
+            'task_type',
+            'doc_stage',
+            'acl',
+            'whitelist',
+            'description',
+            'mailto',
+            'email_subject',
+        ];
+    }
+
+    public static function diffFields(): array
+    {
+        return [
+            'description',
+        ];
+    }
+
+    public static function arrayFields(): array
+    {
+        return [
+            'mailto',
+        ];
+    }
+
+    public static function converters(): array
+    {
+        return [
+            "whitelist" => new WhitelistConverter(),
+            "acl" => new ModelEnumConverter("container.acl"),
+            "doc_stage" => new CustomFieldSelectConverter(CustomFieldGroup::CONTAINER->value, "doc_stage"),
+            "doc_type" => new CustomFieldSelectConverter(CustomFieldGroup::CONTAINER->value, "doc_type"),
+            "naming_rule_id" => new NamingRuleConverter(),
+            "library_id" => new ContainerConverter(),
+            "mailto" => new EmailConverter(),
+        ];
+    }
+}

+ 1 - 1
app/Services/History/ModelChangeDetector.php

@@ -32,7 +32,7 @@ class ModelChangeDetector
                 'old' => is_array($model->getOriginal($field)) ? json_encode($model->getOriginal($field)) : $model->getOriginal($field),
                 'field' => $field,
                 'new' => is_array($model->$field) ? json_encode($model->$field) : $model->$field,
-                'diff' => in_array($field, $diffFields) ? text_diff($model->getOriginal($field), $model->$field) : null,
+                'diff' => in_array($field, $diffFields) ? text_diff($model->getOriginal($field) ?? '', $model->$field ?? '') : null,
             ];
         }
 

+ 69 - 0
app/Services/Notification/ActionEmail/ActionEmailService.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Services\Notification\ActionEmail;
+
+use App\Mail\RequirementAction;
+use App\Mail\TaskAction;
+use App\Models\Action;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\Requirement;
+use App\Models\Task;
+use App\Models\User;
+use App\Repositories\ConfigRepository;
+use Illuminate\Contracts\Mail\Mailable;
+use Illuminate\Support\Facades\Mail;
+
+class ActionEmailService
+{
+    protected ObjectAction $objectAction;
+
+    public function __construct(
+        protected Action $action
+    )
+    {
+        $this->objectAction = ObjectAction::tryFrom($this->action->action);
+    }
+
+    public function send()
+    {
+        if (! ConfigRepository::openEmailNotification()) {
+            return;
+        }
+
+        ConfigRepository::emailDynamicSetting();
+
+        $actionObjectType = ActionObjectType::tryFrom($this->action->object_type);
+
+        $actionObjectModel = $actionObjectType->modelBuilder()->find($this->action->object_id);
+
+        match ($actionObjectType) {
+            ActionObjectType::REQUIREMENT => $this->requirement($actionObjectModel),
+            ActionObjectType::TASK => $this->task($actionObjectModel),
+        };
+    }
+
+    protected function requirement(Requirement $requirement)
+    {
+        $this->dispatch($requirement->mailto, new RequirementAction($requirement, $this->objectAction));
+    }
+
+    protected function task(Task $task)
+    {
+        $userIds = array_filter([$task->assign, ...$task->mailto]);
+
+        $this->dispatch($userIds, new TaskAction($task, $this->objectAction));
+    }
+
+    protected function dispatch(array $userIds, Mailable $mailable)
+    {
+        $users = User::query()->whereIn("id", $userIds)->get();
+        if ($users->isEmpty()) {
+            return;
+        }
+
+        foreach ($users as $user) {
+            Mail::to($user)->send($mailable);
+        }
+    }
+}

+ 69 - 5
app/Services/Project/ProjectGanttService.php

@@ -9,6 +9,7 @@ use App\Models\Project;
 use App\Models\Requirement;
 use App\Models\Task;
 use App\Models\User;
+use Carbon\Carbon;
 use Illuminate\Support\Collection;
 
 class ProjectGanttService
@@ -27,11 +28,74 @@ class ProjectGanttService
                 continue;
             }
 
-            $items[] = [
-                'group' => $group,
-                'group_label' => $groupKey == "" ? ['id' => "", "name" => ""] : $groupNamesKeyBy[$groupKey],
-                'tasks' => ProjectGanttResource::collection($groupTasks[$groupKey]),
-            ];
+            $treeTasks = make_tree($groupTasks[$groupKey]->toArray());
+            $groupUniqueKey = uniqid();
+            $items[] = $this->topGroupFormat(
+                $groupUniqueKey,
+                isset($groupNamesKeyBy[$groupKey]) ? $groupNamesKeyBy[$groupKey]['name'] : "Empty"
+            );
+            $tasks = $this->flattenTasks($treeTasks);
+
+            foreach ($tasks as $task) {
+                $items[] = $this->taskFormat($task, $groupUniqueKey);
+            }
+        }
+
+        return $items;
+    }
+
+    protected function topGroupFormat(string $id, string $name)
+    {
+        return [
+            'id' => $id,
+            'text' => $name,
+            'parent' => 0,
+            'start_date' => null,
+            'duration' => null,
+            'progress' => 0,
+            'assign_to' => null,
+            'open' => true,
+        ];
+    }
+
+    protected function taskFormat(array $task, string $topId)
+    {
+        $progress = 0;
+        $begin = $task['begin'] ? Carbon::parse($task['begin']) : Carbon::parse($task['created_at']);
+        $end = $task['end'] ? Carbon::parse($task['end']) : $begin->copy()->addYears(2);
+        $now = Carbon::now();
+
+        if ($now->gt($end)) {
+            $progress = 1;
+        }
+
+        if ($now->gt($begin) && $end->gt($now)) {
+            $totalDay = $end->diffInDays($begin);
+            $day = $now->diffInDays($begin);
+            $progress = (float)number_format($day / $totalDay, 4);
+        }
+
+        return [
+            'id' => $task['id'],
+            'text' => $task['name'],
+            'parent' => $task['parent_id'] > 0 ? $task['parent_id'] : $topId ,
+            'start_date' => $task['begin'],
+            'duration' => $end->diffInDays($begin),
+            'progress' => $progress,
+            'assign_to' =>$task['assign']?User::query()->where('id',$task['assign'])->first()->name:null,
+            'open' => true,
+        ];
+    }
+
+    protected function flattenTasks($tasks) {
+        $items = [];
+
+        foreach ($tasks as $task) {
+            $items[] = $task;
+            if (!empty($task['children'])) {
+                $items = array_merge($items, $this->flattenTasks($task['children']));
+            }
+            unset($task['children']);
         }
 
         return $items;

+ 17 - 9
app/Services/Project/ProjectKanbanService.php

@@ -4,10 +4,11 @@ namespace App\Services\Project;
 
 use App\Http\Resources\API\KanbanTaskResource;
 use App\Http\Resources\API\UserProfileResource;
-use App\Models\Enums\ProjectStatus;
+use App\Models\Enums\TaskStatus;
 use App\Models\Project;
 use App\Models\Requirement;
 use App\Models\User;
+use function PHPUnit\Framework\MockObject\object;
 
 class ProjectKanbanService
 {
@@ -21,7 +22,7 @@ class ProjectKanbanService
 
         $groupTasks = $project->tasks()->with(['assignTo'])->get()->groupBy($groupKey);
 
-        $statusItems = array_column(ProjectStatus::cases(), 'value');
+        $statusItems = array_column(TaskStatus::cases(), 'value');
 
         $items = [];
         $groupIds = [];
@@ -39,11 +40,18 @@ class ProjectKanbanService
             $items[$groupId ?: "empty"] = $groupItems;
             $groupIds[] = $groupId;
         }
+        $collection=$this->getKanbanGroupData($groupIds, $group);
 
+        $collection->each(function ($item, $key)use ($collection,$items) {
+            $collection[$key]=(object)array_merge($collection[$key]->toArray(),$items[$collection[$key]->id]);
+        });
+        if (isset($items['empty'])){//当存在empty时,单独在末尾添加改task
+            $collection->push($items['empty']);
+        }
         return [
-            'group_data' => $this->getKanbanGroupData($groupIds, $group),
-            'group' => $group,
-            'tasks' => $items,
+            'group_data' => $collection,
+//            'group' => $group,
+//            'tasks' => $items,
             'status_items' => $statusItems,
         ];
     }
@@ -51,7 +59,7 @@ class ProjectKanbanService
     protected function getKanbanGroupData(array $ids, string $group)
     {
         $orderBy = match ($group) {
-            "requirement_asc" => ['id', 'asc'],
+        "requirement_asc" => ['id', 'asc'],
             "requirement_desc" => ['id', 'desc'],
             "requirement_priority_asc" => ['priority', 'asc'],
             "requirement_priority_desc" => ['priority', 'desc'],
@@ -59,12 +67,12 @@ class ProjectKanbanService
         };
 
         return match ($group) {
-            "assign", "finished_by" => UserProfileResource::collection(User::query()->whereIn("id", $ids)->get()),
+        "assign", "finished_by" => User::query()->whereIn("id", $ids)->get(['id','name','username']),
             default => Requirement::query()->whereIn("id", $ids)
-                ->when($orderBy, fn($query) => $query->orderBy(...$orderBy))
+        ->when($orderBy, fn($query) => $query->orderBy(...$orderBy))
                 ->get([
                     'id', 'title', 'priority', 'status'
-                ]),
+                 ]),
         };
     }
 }

+ 3 - 0
app/helpers.php

@@ -49,3 +49,6 @@ if (!function_exists('text_diff')) {
 }
 
 
+
+
+

+ 5 - 0
config/autocde.php

@@ -0,0 +1,5 @@
+<?php
+
+return [
+    'company_default_storage_limit_size' => env("AUTOCDE_COMPANY_DEFAULT_STORAGE_LIMIT_SIZE", 10 * 1024), //公司默认存储容量单位 Mb
+];

+ 1 - 0
config/custom-field.php

@@ -7,5 +7,6 @@ return [
     'groups' => [
         'task',
         'project',
+        'container',
     ]
 ];

+ 38 - 0
database/migrations/2024_03_26_201122_create_files_table.php

@@ -0,0 +1,38 @@
+<?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('files', function (Blueprint $table) {
+            $table->id();
+            $table->integer("company_id")->index();
+            $table->string("pathname", 100);
+            $table->string("title");
+            $table->string("extension", 30);
+            $table->integer("size")->default(0);
+            $table->string("object_type", 30)->nullable();
+            $table->integer("object_id")->nullable();
+            $table->integer("created_by")->nullable();
+            $table->smallInteger("version")->default(1);
+            $table->integer("downloads")->default(0);
+            $table->softDeletes();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('files');
+    }
+};

+ 30 - 0
database/migrations/2024_03_27_151013_add_remark_to_custom_fields.php

@@ -0,0 +1,30 @@
+<?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('custom_fields', function (Blueprint $table) {
+            //
+            $table->string('remark')->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('custom_fields', function (Blueprint $table) {
+            //
+            $table->dropColumn('remark');
+        });
+    }
+};

Some files were not shown because too many files changed in this diff