Browse Source

Merge branch 'dev' into user_info

kely 8 months ago
parent
commit
5799603903

+ 3 - 2
app/Http/Controllers/API/NotificationController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\API;
 
 use App\Http\Controllers\Controller;
+use App\Http\Resources\API\NotificationCollection;
 use App\Http\Resources\API\NotificationResource;
 use App\Models\Enums\NotificationStatus;
 use App\Models\Notification;
@@ -17,7 +18,7 @@ class NotificationController extends Controller
      * 用户通知列表
      *
      * @param Request $request
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
+     * @return NotificationCollection
      */
     public function index(Request $request)
     {
@@ -29,7 +30,7 @@ class NotificationController extends Controller
             ->orderByDesc("created_at")
             ->paginate();
 
-        return NotificationResource::collection($notifications);
+        return new NotificationCollection($notifications);
     }
 
     /**

+ 75 - 0
app/Http/Resources/API/NotificationCollection.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use App\Models\Action;
+use App\Models\Enums\ApprovalObjectType;
+use App\Models\Enums\NotificationObjectType;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\ResourceCollection;
+
+class NotificationCollection extends ResourceCollection
+{
+    /**
+     * Transform the resource collection into an array.
+     *
+     * @return array<int|string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        list ($actions, $actionGroupObjects) = $this->widthActionObjects();
+
+        $items = [];
+        foreach ($this->resource->items() as $item) {
+            $object = null;
+            if ($item->object_type == NotificationObjectType::ACTION->value) {
+                $action = $actions[$item->object_id];
+
+                $objectAction = $item->extra_fields['action'] ?? $action->action;
+
+                $object = [
+                    'id' => $action->id,
+                    'action' => $objectAction,
+                    'action_label' => __(sprintf("action-labels.label.%s", $objectAction)),
+                    'created_by' => new UserProfileResource($action->createdBy),
+                    'comment' => $action->commtent,
+                    'object_type' => $action->object_type,
+                    'object' => [
+                        'id' => $action->object_id,
+                        'name' => data_get($actionGroupObjects, sprintf("%s.%s", $action->object_type, $action->object_id)),
+                    ]
+                ];
+            }
+            $row = [
+                'id' => $item->id,
+                'object_type' => $item->object_type,
+                'content' => $item->content,
+                'read_at' => (string)$item->read_at,
+                'read_status' => (bool)$item->read_at,
+                'object' => $object,
+            ];
+            $items[] = $row;
+        }
+
+        return $items;
+    }
+
+    protected function widthActionObjects(): array
+    {
+        $actionIDS = collect($this->resource->items())->where("object_type", NotificationObjectType::ACTION->value)->pluck("object_id");
+
+        $actions = Action::with(['createdBy'])->whereIn("id", $actionIDS->toArray())->get();
+
+        $actionGroupObjects = [];
+
+        foreach($actions->groupBy("object_type") as $objectType => $items) {
+            $approvalObjectType = ApprovalObjectType::from($objectType);
+            $actionGroupObjects[$objectType] = $approvalObjectType
+                ->modelBuilder()
+                ->whereIn("id", array_column($items->toArray(), "object_id"))
+                ->pluck($approvalObjectType->nameField(), "id");
+        };
+
+        return [$actions->keyBy("id"), $actionGroupObjects];
+    }
+}

+ 9 - 19
app/Listeners/SendActionBrowserNotification.php

@@ -9,6 +9,9 @@ use App\Models\Enums\ObjectAction;
 use App\Models\Notification;
 use App\Models\NotificationRecord;
 use App\Repositories\ConfigRepository;
+use App\Services\Notification\ActionBrowser\NormalNotification;
+use App\Services\Notification\ActionBrowser\RequirementNotification;
+use App\Services\Notification\ActionBrowser\TaskNotification;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Queue\InteractsWithQueue;
 
@@ -37,27 +40,14 @@ class SendActionBrowserNotification implements ShouldQueue
             return;
         }
 
-        $userIds = match ($actionObjectType) {
-            ActionObjectType::TASK => $object->assign > 0 ? [$object->assign] : [],
-            ActionObjectType::REQUIREMENT=>$object->reviewed_by > 0 ? [$object->reviewed_by ] :[],
-            default => [],
+        $notification = match ($actionObjectType) {
+            ActionObjectType::PROJECT, ActionObjectType::CONTAINER  => new NormalNotification($event->action, $object),
+            ActionObjectType::REQUIREMENT => new RequirementNotification($event->action, $object),
+            ActionObjectType::TASK => new TaskNotification($event->action, $object),
+            default => null,
         };
 
-        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,
-            ]);
-        }
+        $notification?->handle();
     }
 
     public function shouldQueue(ObjectActionCreate $event): bool

+ 58 - 0
app/Mail/ProjectAction.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Mail;
+
+use App\Models\Container;
+use App\Models\Enums\ObjectAction;
+use App\Models\Project;
+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 ProjectAction extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(public Project $project, protected ObjectAction $objectAction, public array $actions = [])
+    {
+
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: $this->project->name,
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            //view: 'view.name',
+            markdown: 'emails.actions.project',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 7 - 2
app/ModelFilters/ApprovalFilter.php

@@ -25,9 +25,14 @@ class ApprovalFilter extends ModelFilter
             return match ($tab) {
                 'wait_for_me' => $query->where("users", 'like', '%,'.Auth::id().',%')->where("status", ObjectApprovalStatus::DOING),
                 'approved' => $query,
-                'pr' => $query->where("created_at", Auth::id()),
-                default => $query->where("created_at", Auth::id())->orWhere("users", 'like', '%,'.Auth::id().',%'),
+                'pr' => $query->where("created_by", Auth::id()),
+                default => $query->where("created_by", Auth::id())->orWhere("users", 'like', '%,'.Auth::id().',%'),
             };
         });
     }
+
+    public function type($type): ModelFilter
+    {
+        return $this->where('object_type',$type);
+    }
 }

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

@@ -88,8 +88,25 @@ enum ObjectAction: string
 
     case APPROVED_CANCELED ='approvedCanceled';
 
+    public function isApproval()
+    {
+        return in_array($this->value, [
+            self::APPROVAL_REQUEST->value,
+            self::APPROVAL_APPROVED->value,
+            self::APPROVAL_REJECTED->value,
+            self::APPROVED_TO_NEXT_NODE->value,
+        ]);
+    }
+
     public static function messageNotificationItems()
     {
+        $approval = [
+            self::APPROVAL_REQUEST,
+            self::APPROVAL_APPROVED,
+            self::APPROVAL_REJECTED,
+            self::APPROVED_TO_NEXT_NODE,
+        ];
+
         return [
             ActionObjectType::REQUIREMENT->value => [
                 self::CREATED,
@@ -97,6 +114,7 @@ enum ObjectAction: string
                 self::STARTED,
                 self::CHANGED,
                 self::CLOSED,
+                ...$approval,
             ],
             ActionObjectType::TASK->value => [
                 ObjectAction::STARTED,
@@ -105,10 +123,15 @@ enum ObjectAction: string
                 ObjectAction::DONE,
                 ObjectAction::CANCELED,
                 ObjectAction::EDITED,
+                ...$approval,
             ],
             ActionObjectType::CONTAINER->value => [
                 ObjectAction::CREATED,
                 ObjectAction::EDITED,
+                ...$approval,
+            ],
+            ActionObjectType::PROJECT->value => [
+                ...$approval,
             ]
         ];
     }

+ 4 - 0
app/Models/Notification.php

@@ -11,4 +11,8 @@ class Notification extends Model
     use HasFactory, Filterable;
 
     protected $guarded = ['id'];
+
+    protected $casts = [
+        'extra_fields' => 'array'
+    ];
 }

+ 9 - 1
app/Services/Approval/ActionService.php

@@ -12,6 +12,8 @@ class ActionService
 {
     protected ?Approval $approval = null;
 
+    protected array $actionExtraFields = [];
+
     public function action(Approval $approval, int $status, string $comment = null): void
     {
         $this->approval = $approval;
@@ -54,7 +56,8 @@ class ActionService
         ActionRepository::createByApproval(
             $this->approval,
             $objectAction,
-            $comment
+            $comment,
+            extraFields: $this->actionExtraFields
         );
     }
 
@@ -69,6 +72,11 @@ class ActionService
             return;
         }
 
+        $this->actionExtraFields = [
+            'now' => $this->approval->node_level,
+            'next' =>  $nextNodeIndex,
+        ];
+
         $this->approval->node_level = $nextNodeIndex;
         $this->approval->users = sprintf(",%s,", implode(',', $nextNodes['approval_users']));
     }

+ 4 - 0
app/Services/Approval/StoreService.php

@@ -55,6 +55,10 @@ class StoreService
         ActionRepository::createByApproval(
             $approval,
             ObjectAction::APPROVAL_REQUEST,
+            extraFields:  [
+                'now' => 0,
+                'next' => 1,
+            ],
         );
     }
 

+ 94 - 0
app/Services/Notification/Abstracts/ActionBrowserNotificationAbstract.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace App\Services\Notification\Abstracts;
+
+use App\Models\Action;
+use App\Models\Approval;
+use App\Models\Enums\NotificationObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\Notification;
+use App\Models\NotificationRecord;
+use App\Services\Notification\Contacts\ActionBrowserNotificationContacts;
+use Illuminate\Database\Eloquent\Model;
+
+abstract class ActionBrowserNotificationAbstract implements ActionBrowserNotificationContacts
+{
+    protected ObjectAction $objectAction;
+
+    public function __construct(protected Action $action,  protected Model $object)
+    {
+        $this->objectAction = ObjectAction::tryFrom($this->action->action);
+    }
+
+    public function handle()
+    {
+        if ($this->objectAction->isApproval()) {
+            $this->handleByApprovalAction();
+        } else {
+            $this->handleByObjectAction($this->userIDs());
+        }
+    }
+
+    abstract protected function userIDs(): array;
+
+    protected function handleByApprovalAction()
+    {
+        $approval = Approval::query()->find($this->action->additional_id);
+        if (! $approval) {
+            return;
+        }
+
+        match ($this->objectAction) {
+            ObjectAction::APPROVAL_REQUEST => $this->notificationNextApprovalUsers($approval),
+            ObjectAction::APPROVED_TO_NEXT_NODE => $this->handleByApprovedToNextNode($approval),
+            ObjectAction::APPROVAL_APPROVED, ObjectAction::APPROVAL_REJECTED => $this->handleByObjectAction([$approval->created_by]),
+            default => '',
+        };
+    }
+
+    protected function handleByApprovedToNextNode(Approval $approval)
+    {
+        $this->notificationNextApprovalUsers($approval);
+
+        $this->handleByObjectAction([$approval->created_by]);
+    }
+
+    protected function handleByObjectAction(array $userIDs)
+    {
+        $this->storeNotification($userIDs);
+    }
+
+    protected function storeNotification(array $userIds, array $extraFields = [])
+    {
+        if (! $userIds) {
+            return;
+        }
+
+        $notification =  Notification::query()->create([
+            'object_type' => NotificationObjectType::ACTION->value,
+            'object_id' => $this->action->id,
+            'extra_fields' => $extraFields ?: null,
+        ]);
+
+        foreach ($userIds as $userId) {
+            NotificationRecord::query()->create([
+                'notification_id' => $notification->id,
+                'user_id' => $userId,
+            ]);
+        }
+    }
+
+    protected function notificationNextApprovalUsers(Approval $approval)
+    {
+        $approvalFlow = $approval->approvalFlow()->first();
+        if (! $approvalFlow) {
+            return;
+        }
+
+        $userIds = $approvalFlow->nodes[$this->action->extra_fields['next']]['approval_users'] ?? [];
+
+        $this->storeNotification($userIds, [
+            'action' => 'waitingForMyApproval'
+        ]);
+    }
+}

+ 13 - 0
app/Services/Notification/ActionBrowser/NormalNotification.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Services\Notification\ActionBrowser;
+
+use App\Services\Notification\Abstracts\ActionBrowserNotificationAbstract;
+
+class NormalNotification extends ActionBrowserNotificationAbstract
+{
+    protected function userIDs(): array
+    {
+        return [];
+    }
+}

+ 13 - 0
app/Services/Notification/ActionBrowser/RequirementNotification.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Services\Notification\ActionBrowser;
+
+use App\Services\Notification\Abstracts\ActionBrowserNotificationAbstract;
+
+class RequirementNotification extends ActionBrowserNotificationAbstract
+{
+    protected function userIDs(): array
+    {
+        return $this->object->reviewed_by > 0 ? [$this->object->reviewed_by ] :[];
+    }
+}

+ 13 - 0
app/Services/Notification/ActionBrowser/TaskNotification.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Services\Notification\ActionBrowser;
+
+use App\Services\Notification\Abstracts\ActionBrowserNotificationAbstract;
+
+class TaskNotification extends ActionBrowserNotificationAbstract
+{
+    protected function userIDs(): array
+    {
+        return $this->object->assign > 0 ? [$this->object->assign] : [];
+    }
+}

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

@@ -3,12 +3,15 @@
 namespace App\Services\Notification\ActionEmail;
 
 use App\Mail\ContainerAction;
+use App\Mail\ProjectAction;
 use App\Mail\RequirementAction;
 use App\Mail\TaskAction;
 use App\Models\Action;
+use App\Models\Approval;
 use App\Models\Container;
 use App\Models\Enums\ActionObjectType;
 use App\Models\Enums\ObjectAction;
+use App\Models\Project;
 use App\Models\Requirement;
 use App\Models\Task;
 use App\Models\User;
@@ -48,9 +51,17 @@ class ActionEmailService
             ActionObjectType::REQUIREMENT => $this->requirement($actionObjectModel),
             ActionObjectType::TASK => $this->task($actionObjectModel),
             ActionObjectType::CONTAINER => $this->container($actionObjectModel),
+            ActionObjectType::PROJECT => $this->project($actionObjectModel),
+            default => null,
         };
     }
 
+    protected function project(Project $project)
+    {
+        $this->dispatch([], new ProjectAction($project, $this->objectAction, $this->actions));
+    }
+
+
     protected function container(Container $container)
     {
         $this->dispatch($container->mailto, new ContainerAction($container, $this->objectAction, $this->actions));
@@ -70,6 +81,30 @@ class ActionEmailService
 
     protected function dispatch(array $userIds, Mailable $mailable)
     {
+        if ($this->objectAction->isApproval()) {
+            $approval = Approval::query()->find($this->action->additional_id);
+            if (! $approval) {
+                return;
+            }
+
+            $approvalFlow = $approval->approvalFlow()->first();
+
+            $nextApprovalUsers = $approvalFlow?->nodes[$this->action->extra_fields['next']]['approval_users'] ?? [];
+
+            $userIds = match ($this->objectAction) {
+                ObjectAction::APPROVAL_REQUEST => $nextApprovalUsers,
+                ObjectAction::APPROVED_TO_NEXT_NODE => [$approval->created_by, ...$nextApprovalUsers],
+                ObjectAction::APPROVAL_APPROVED, ObjectAction::APPROVAL_REJECTED => [$approval->created_by],
+                default => [],
+            };
+        }
+
+        $userIds = array_filter(array_unique($userIds));
+
+        if (! $userIds) {
+            return;
+        }
+
         $users = User::query()->whereIn("id", $userIds)->get();
         if ($users->isEmpty()) {
             return;

+ 13 - 0
app/Services/Notification/Contacts/ActionBrowserNotificationContacts.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Services\Notification\Contacts;
+
+use App\Models\Action;
+use Illuminate\Database\Eloquent\Model;
+
+interface ActionBrowserNotificationContacts
+{
+    public function __construct(Action $action,  Model $object);
+
+    public function handle();
+}

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

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

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

@@ -48,5 +48,7 @@ return[
         ObjectAction::APPROVAL_REJECTED->value => "approval rejected",
         ObjectAction::APPROVED_CANCELED->value => "approval canceled",
         ObjectAction::APPROVED_TO_NEXT_NODE->value => "approved to next step",
+
+        "waitingForMyApproval" => "Waiting for my approval",
     ]
 ];

+ 10 - 0
resources/views/emails/actions/project.blade.php

@@ -0,0 +1,10 @@
+<x-mail::message>
+# Project Name: {{ $project->name }}
+
+<x-mail::panel>{{ sprintf("%s %s by %s", data_get($actions, '0.created_at'), data_get($actions, '0.action_label'), data_get($actions, '0.created_by.name')) }}</x-mail::panel>
+
+<x-email.history :actions="$actions"></x-email.history>
+
+Thanks,<br>
+{{ config('app.name') }}
+</x-mail::message>