ChatMessageController.php 12 KB

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