ChatMessageController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Models\ChatMessage;
  4. use App\Models\Order;
  5. use App\Models\Reclamation;
  6. use App\Models\Role;
  7. use App\Models\User;
  8. use App\Services\FileService;
  9. use App\Services\NotificationService;
  10. use Illuminate\Http\RedirectResponse;
  11. use Illuminate\Http\Request;
  12. use Illuminate\Support\Facades\DB;
  13. use Throwable;
  14. class ChatMessageController extends Controller
  15. {
  16. public function storeForOrder(
  17. Request $request,
  18. Order $order,
  19. FileService $fileService,
  20. NotificationService $notificationService,
  21. ): RedirectResponse {
  22. $this->ensureCanViewOrder($order);
  23. if ($this->isDeleteRequest($request)) {
  24. $chatMessage = $this->resolveMessageForDeletion($request);
  25. $this->ensureAdminCanDeleteMessage($chatMessage, $order, null);
  26. $this->deleteMessage($chatMessage);
  27. return redirect()->back()->with(['success' => 'Сообщение удалено.']);
  28. }
  29. return $this->storeMessage(
  30. $request,
  31. $fileService,
  32. $notificationService,
  33. $order,
  34. null,
  35. 'chat/orders/' . $order->id,
  36. );
  37. }
  38. public function storeForReclamation(
  39. Request $request,
  40. Reclamation $reclamation,
  41. FileService $fileService,
  42. NotificationService $notificationService,
  43. ): RedirectResponse {
  44. $this->ensureCanViewReclamation($reclamation);
  45. if ($this->isDeleteRequest($request)) {
  46. $chatMessage = $this->resolveMessageForDeletion($request);
  47. $this->ensureAdminCanDeleteMessage($chatMessage, null, $reclamation);
  48. $this->deleteMessage($chatMessage);
  49. return redirect()->back()->with(['success' => 'Сообщение удалено.']);
  50. }
  51. return $this->storeMessage(
  52. $request,
  53. $fileService,
  54. $notificationService,
  55. null,
  56. $reclamation,
  57. 'chat/reclamations/' . $reclamation->id,
  58. );
  59. }
  60. public function destroyForOrder(Order $order, ChatMessage $chatMessage): RedirectResponse
  61. {
  62. $this->ensureAdminCanDeleteMessage($chatMessage, $order, null);
  63. $this->deleteMessage($chatMessage);
  64. return redirect()->back()->with(['success' => 'Сообщение удалено.']);
  65. }
  66. public function destroyForReclamation(Reclamation $reclamation, ChatMessage $chatMessage): RedirectResponse
  67. {
  68. $this->ensureAdminCanDeleteMessage($chatMessage, null, $reclamation);
  69. $this->deleteMessage($chatMessage);
  70. return redirect()->back()->with(['success' => 'Сообщение удалено.']);
  71. }
  72. private function storeMessage(
  73. Request $request,
  74. FileService $fileService,
  75. NotificationService $notificationService,
  76. ?Order $order,
  77. ?Reclamation $reclamation,
  78. string $filePath,
  79. ): RedirectResponse {
  80. $isPrivileged = hasRole(Role::ADMIN . ',' . Role::MANAGER);
  81. $isBrigadier = hasRole(Role::BRIGADIER);
  82. $validated = $request->validate([
  83. 'message' => 'nullable|string',
  84. 'notification_type' => 'nullable|string|in:none,responsibles,all,user',
  85. 'target_user_id' => 'nullable|integer|exists:users,id,deleted_at,NULL',
  86. 'target_user_ids' => 'nullable|array',
  87. 'target_user_ids.*' => 'integer|exists:users,id,deleted_at,NULL',
  88. 'attachments' => 'nullable|array|max:5',
  89. 'attachments.*' => 'file|max:10240',
  90. ]);
  91. $notificationType = $validated['notification_type'] ?? ChatMessage::NOTIFICATION_NONE;
  92. if ($isBrigadier) {
  93. $notificationType = ChatMessage::NOTIFICATION_RESPONSIBLES;
  94. } elseif (!$isPrivileged) {
  95. $notificationType = ChatMessage::NOTIFICATION_NONE;
  96. }
  97. $messageText = trim((string) ($validated['message'] ?? ''));
  98. $attachments = $request->file('attachments', []);
  99. if ($messageText === '' && empty($attachments)) {
  100. return redirect()->back()->with(['danger' => 'Нужно указать сообщение или добавить файл.']);
  101. }
  102. $recipientIds = [];
  103. $targetUserId = null;
  104. if ($isBrigadier) {
  105. $recipientIds = $this->chatBrigadierRecipientIds($order, $reclamation, (int) auth()->id());
  106. } elseif ($notificationType === ChatMessage::NOTIFICATION_USER) {
  107. $targetUserId = (int) ($validated['target_user_id'] ?? 0);
  108. if ($targetUserId < 1) {
  109. return redirect()->back()->with(['danger' => 'Нужно выбрать пользователя для уведомления.']);
  110. }
  111. $recipientIds = [$targetUserId];
  112. }
  113. if (in_array($notificationType, [
  114. ChatMessage::NOTIFICATION_RESPONSIBLES,
  115. ChatMessage::NOTIFICATION_ALL,
  116. ], true)) {
  117. $recipientIds = $this->resolveTargetUserIds(
  118. $notificationType,
  119. $validated['target_user_ids'] ?? [],
  120. $order,
  121. $reclamation,
  122. (int) auth()->id(),
  123. );
  124. if (empty($recipientIds)) {
  125. return redirect()->back()->with(['danger' => 'Нужно выбрать хотя бы одного получателя уведомления.']);
  126. }
  127. }
  128. if (!in_array($notificationType, [ChatMessage::NOTIFICATION_USER], true)) {
  129. $targetUserId = null;
  130. }
  131. try {
  132. $chatMessage = ChatMessage::query()->create([
  133. 'order_id' => $order?->id,
  134. 'reclamation_id' => $reclamation?->id,
  135. 'user_id' => (int) auth()->id(),
  136. 'target_user_id' => $targetUserId,
  137. 'notification_type' => $notificationType,
  138. 'message' => $messageText !== '' ? $messageText : null,
  139. ]);
  140. if (!empty($recipientIds)) {
  141. $chatMessage->notifiedUsers()->syncWithoutDetaching($recipientIds);
  142. }
  143. $files = [];
  144. foreach ($attachments as $attachment) {
  145. $files[] = $fileService->saveUploadedFile($filePath, $attachment);
  146. }
  147. if (!empty($files)) {
  148. $chatMessage->files()->syncWithoutDetaching(collect($files)->pluck('id')->all());
  149. }
  150. if ($notificationType !== ChatMessage::NOTIFICATION_NONE) {
  151. $notificationService->notifyChatMessage($chatMessage->fresh([
  152. 'user',
  153. 'targetUser',
  154. 'files',
  155. 'order.user',
  156. 'order.brigadier',
  157. 'reclamation.order',
  158. 'reclamation.user',
  159. 'reclamation.brigadier',
  160. ]), $recipientIds, $isBrigadier);
  161. }
  162. } catch (Throwable $exception) {
  163. report($exception);
  164. return redirect()->back()->with(['error' => 'Не удалось отправить сообщение в чат.']);
  165. }
  166. return redirect()->back()->with(['success' => 'Сообщение отправлено.']);
  167. }
  168. private function ensureAdminCanDeleteMessage(
  169. ChatMessage $chatMessage,
  170. ?Order $order,
  171. ?Reclamation $reclamation,
  172. ): void {
  173. abort_unless(hasRole(Role::ADMIN), 403);
  174. if ($order && (int) $chatMessage->order_id !== (int) $order->id) {
  175. abort(404);
  176. }
  177. if ($reclamation && (int) $chatMessage->reclamation_id !== (int) $reclamation->id) {
  178. abort(404);
  179. }
  180. }
  181. private function deleteMessage(ChatMessage $chatMessage): void
  182. {
  183. DB::transaction(function () use ($chatMessage): void {
  184. $chatMessage->notifiedUsers()->detach();
  185. $chatMessage->files()->detach();
  186. $chatMessage->delete();
  187. });
  188. }
  189. private function isDeleteRequest(Request $request): bool
  190. {
  191. return $request->boolean('delete_message');
  192. }
  193. private function resolveMessageForDeletion(Request $request): ChatMessage
  194. {
  195. $validated = $request->validate([
  196. 'chat_message_id' => 'required|integer|exists:chat_messages,id',
  197. ]);
  198. return ChatMessage::query()->findOrFail((int) $validated['chat_message_id']);
  199. }
  200. private function ensureCanViewOrder(Order $order): void
  201. {
  202. if (hasRole(Role::BRIGADIER)) {
  203. $canView = (int) $order->brigadier_id === (int) auth()->id()
  204. && in_array((int) $order->order_status_id, Order::visibleStatusIdsForBrigadier(), true);
  205. if (!$canView) {
  206. abort(403);
  207. }
  208. }
  209. }
  210. private function ensureCanViewReclamation(Reclamation $reclamation): void
  211. {
  212. if (hasRole(Role::BRIGADIER)) {
  213. $canView = (int) $reclamation->brigadier_id === (int) auth()->id()
  214. && in_array((int) $reclamation->status_id, Reclamation::visibleStatusIdsForBrigadier(), true);
  215. if (!$canView) {
  216. abort(403);
  217. }
  218. }
  219. if (hasRole(Role::WAREHOUSE_HEAD)) {
  220. $canView = $reclamation->brigadier_id !== null
  221. && in_array((int) $reclamation->status_id, Reclamation::visibleStatusIdsForBrigadier(), true);
  222. if (!$canView) {
  223. abort(403);
  224. }
  225. }
  226. }
  227. private function resolveTargetUserIds(
  228. string $notificationType,
  229. array $targetUserIds,
  230. ?Order $order,
  231. ?Reclamation $reclamation,
  232. int $senderId,
  233. ): array {
  234. $selectedIds = array_values(array_unique(array_map(static fn ($id) => (int) $id, $targetUserIds)));
  235. $allowedIds = match ($notificationType) {
  236. ChatMessage::NOTIFICATION_RESPONSIBLES => $this->chatResponsibleRecipientIds($order, $reclamation),
  237. ChatMessage::NOTIFICATION_ALL => User::query()->pluck('id')->map(static fn ($id) => (int) $id)->all(),
  238. default => [],
  239. };
  240. $selectedIds = array_values(array_intersect($selectedIds, $allowedIds));
  241. return array_values(array_diff($selectedIds, [$senderId]));
  242. }
  243. private function chatResponsibleRecipientIds(?Order $order, ?Reclamation $reclamation): array
  244. {
  245. $adminIds = User::query()
  246. ->where('role', Role::ADMIN)
  247. ->pluck('id')
  248. ->map(static fn ($id) => (int) $id)
  249. ->all();
  250. if ($order) {
  251. return array_values(array_unique(array_filter(array_merge($adminIds, [
  252. $order->user_id ? (int) $order->user_id : null,
  253. $order->brigadier_id ? (int) $order->brigadier_id : null,
  254. ]))));
  255. }
  256. if ($reclamation) {
  257. return array_values(array_unique(array_filter(array_merge($adminIds, [
  258. $reclamation->user_id ? (int) $reclamation->user_id : null,
  259. $reclamation->brigadier_id ? (int) $reclamation->brigadier_id : null,
  260. ]))));
  261. }
  262. return $adminIds;
  263. }
  264. private function chatBrigadierRecipientIds(?Order $order, ?Reclamation $reclamation, int $senderId): array
  265. {
  266. $adminIds = User::query()
  267. ->where('role', Role::ADMIN)
  268. ->pluck('id')
  269. ->map(static fn ($id) => (int) $id)
  270. ->all();
  271. $managerId = $order?->user_id ?: $reclamation?->user_id;
  272. return array_values(array_unique(array_diff(array_filter([
  273. ...$adminIds,
  274. $managerId ? (int) $managerId : null,
  275. ]), [$senderId])));
  276. }
  277. }