ReclamationController.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Requests\CreateReclamationRequest;
  4. use App\Http\Requests\StoreReclamationDetailsRequest;
  5. use App\Http\Requests\StoreReclamationRequest;
  6. use App\Http\Requests\StoreReclamationSparePartsRequest;
  7. use App\Jobs\ExportReclamationsJob;
  8. use App\Jobs\GenerateFilesPack;
  9. use App\Jobs\GenerateReclamationPaymentPack;
  10. use App\Jobs\GenerateReclamationPack;
  11. use App\Models\File;
  12. use App\Models\Order;
  13. use App\Models\Reclamation;
  14. use App\Models\ReclamationDetail;
  15. use App\Models\ReclamationStatus;
  16. use App\Models\ReclamationView;
  17. use App\Models\Role;
  18. use App\Models\User;
  19. use App\Services\FileService;
  20. use App\Services\SparePartReservationService;
  21. use Illuminate\Http\Request;
  22. use Illuminate\Support\Carbon;
  23. use Illuminate\Support\Facades\Storage;
  24. use Throwable;
  25. class ReclamationController extends Controller
  26. {
  27. protected array $data = [
  28. 'active' => 'reclamations',
  29. 'title' => 'Рекламации',
  30. 'id' => 'reclamations',
  31. 'header' => [
  32. 'id' => 'ID',
  33. 'user_name' => 'Менеджер',
  34. 'status_name' => 'Статус',
  35. 'district_name' => 'Округ',
  36. 'area_name' => 'Район',
  37. 'object_address' => 'Адрес объекта',
  38. 'maf_installation_year' => 'Год установки МАФ',
  39. 'create_date' => 'Дата создания',
  40. 'finish_date' => 'Дата завершения',
  41. 'start_work_date' => 'Дата начала работ',
  42. 'work_days' => 'Срок работ, дней',
  43. 'brigadier_name' => 'Бригадир',
  44. 'reason' => 'Причина',
  45. 'guarantee' => 'Гарантии',
  46. 'whats_done' => 'Что сделано',
  47. 'comment' => 'Комментарий',
  48. ],
  49. 'searchFields' => [
  50. 'reason',
  51. 'guarantee',
  52. 'whats_done',
  53. 'comment',
  54. ],
  55. 'ranges' => [],
  56. ];
  57. public function __construct()
  58. {
  59. $this->data['users'] = User::query()->whereIn('role', [Role::MANAGER, Role::ADMIN])->get()->pluck('name', 'id');
  60. $this->data['statuses'] = ReclamationStatus::query()->get()->pluck('name', 'id');
  61. }
  62. public function index(Request $request)
  63. {
  64. session(['gp_reclamations' => $request->all()]);
  65. $model = new ReclamationView();
  66. // fill filters
  67. $this->createFilters($model, 'user_name', 'status_name');
  68. $this->createDateFilters($model, 'create_date', 'finish_date');
  69. $q = $model::query();
  70. $this->acceptFilters($q, $request);
  71. $this->acceptSearch($q, $request);
  72. $this->setSortAndOrderBy($model, $request);
  73. if (hasRole(Role::BRIGADIER)) {
  74. $q->where('brigadier_id', auth()->id());
  75. }
  76. $this->applyStableSorting($q);
  77. $this->data['reclamations'] = $q->paginate($this->data['per_page'])->withQueryString();
  78. return view('reclamations.index', $this->data);
  79. }
  80. public function export(Request $request)
  81. {
  82. $gp = session('gp_reclamations') ?? [];
  83. $filterRequest = new Request($gp);
  84. $model = new ReclamationView();
  85. $this->createFilters($model, 'user_name', 'status_name');
  86. $this->createDateFilters($model, 'create_date', 'finish_date');
  87. $q = $model::query();
  88. $this->acceptFilters($q, $filterRequest);
  89. $this->acceptSearch($q, $filterRequest);
  90. $this->setSortAndOrderBy($model, $filterRequest);
  91. $this->applyStableSorting($q);
  92. $reclamationIds = $q->pluck('id')->toArray();
  93. ExportReclamationsJob::dispatch($reclamationIds, $request->user()->id);
  94. return redirect()->route('reclamations.index', session('gp_reclamations'))
  95. ->with(['success' => 'Задача экспорта рекламаций создана!']);
  96. }
  97. public function create(CreateReclamationRequest $request, Order $order)
  98. {
  99. $reclamation = Reclamation::query()->create([
  100. 'order_id' => $order->id,
  101. 'user_id' => $request->user()->id,
  102. 'status_id' => Reclamation::STATUS_NEW,
  103. 'create_date' => Carbon::now(),
  104. 'finish_date' => Carbon::now()->addDays(30),
  105. ]);
  106. $skus = $request->validated('skus');
  107. $reclamation->skus()->attach($skus);
  108. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => url()->previous()]);
  109. }
  110. public function show(Request $request, Reclamation $reclamation)
  111. {
  112. $this->ensureCanViewReclamation($reclamation);
  113. $this->data['brigadiers'] = User::query()->where('role', Role::BRIGADIER)->get()->pluck('name', 'id');
  114. $this->data['reclamation'] = $reclamation;
  115. $this->data['previous_url'] = $this->resolvePreviousUrl(
  116. $request,
  117. 'previous_url_reclamations',
  118. route('reclamations.index', session('gp_reclamations'))
  119. );
  120. return view('reclamations.edit', $this->data);
  121. }
  122. public function update(StoreReclamationRequest $request, Reclamation $reclamation)
  123. {
  124. $data = $request->validated();
  125. $reclamation->update($data);
  126. $previousUrl = $this->previousUrlForRedirect($request, 'previous_url_reclamations');
  127. if (!empty($previousUrl)) {
  128. return redirect()->route('reclamations.show', [
  129. 'reclamation' => $reclamation,
  130. 'previous_url' => $previousUrl,
  131. ]);
  132. }
  133. return redirect()->route('reclamations.show', $reclamation->id);
  134. }
  135. public function updateStatus(Request $request, Reclamation $reclamation)
  136. {
  137. if (!hasRole('admin,manager')) {
  138. abort(403);
  139. }
  140. $validated = $request->validate([
  141. 'status_id' => 'required|exists:reclamation_statuses,id',
  142. ]);
  143. $reclamation->update(['status_id' => $validated['status_id']]);
  144. return response()->noContent();
  145. }
  146. public function delete(Reclamation $reclamation)
  147. {
  148. $reclamation->delete();
  149. return redirect()->route('reclamations.index');
  150. }
  151. public function uploadPhotoBefore(Request $request, Reclamation $reclamation, FileService $fileService)
  152. {
  153. $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
  154. $this->ensureCanViewReclamation($reclamation);
  155. $data = $request->validate([
  156. 'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
  157. ]);
  158. try {
  159. $f = [];
  160. foreach ($data['photo'] as $photo) {
  161. $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/photo_before', $photo);
  162. }
  163. $reclamation->photos_before()->syncWithoutDetaching($f);
  164. } catch (Throwable $e) {
  165. report($e);
  166. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  167. ->with(['error' => 'Ошибка загрузки фотографий проблемы. Проверьте имя файла и повторите попытку.']);
  168. }
  169. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  170. ->with(['success' => 'Фотографии проблемы успешно загружены!']);
  171. }
  172. public function uploadPhotoAfter(Request $request, Reclamation $reclamation, FileService $fileService)
  173. {
  174. $this->ensureCanViewReclamation($reclamation);
  175. $data = $request->validate([
  176. 'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
  177. ]);
  178. try {
  179. $f = [];
  180. foreach ($data['photo'] as $photo) {
  181. $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/photo_after', $photo);
  182. }
  183. $reclamation->photos_after()->syncWithoutDetaching($f);
  184. } catch (Throwable $e) {
  185. report($e);
  186. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  187. ->with(['error' => 'Ошибка загрузки фотографий после устранения. Проверьте имя файла и повторите попытку.']);
  188. }
  189. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  190. ->with(['success' => 'Фотографии после устранения успешно загружены!']);
  191. }
  192. public function deletePhotoBefore(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
  193. {
  194. $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
  195. $this->ensureCanViewReclamation($reclamation);
  196. $reclamation->photos_before()->detach($file);
  197. Storage::disk('public')->delete($file->path);
  198. $file->delete();
  199. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]);
  200. }
  201. public function deletePhotoAfter(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
  202. {
  203. $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
  204. $this->ensureCanViewReclamation($reclamation);
  205. $reclamation->photos_after()->detach($file);
  206. Storage::disk('public')->delete($file->path);
  207. $file->delete();
  208. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]);
  209. }
  210. public function uploadDocument(Request $request, Reclamation $reclamation, FileService $fileService)
  211. {
  212. $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
  213. $this->ensureCanViewReclamation($reclamation);
  214. $data = $request->validate([
  215. 'document.*' => 'file',
  216. ]);
  217. try {
  218. $f = [];
  219. $i = 0;
  220. foreach ($data['document'] as $document) {
  221. if ($i++ >= 5) break;
  222. $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/document', $document);
  223. }
  224. $reclamation->documents()->syncWithoutDetaching($f);
  225. } catch (Throwable $e) {
  226. report($e);
  227. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  228. ->with(['error' => 'Ошибка загрузки документов рекламации. Проверьте имя файла и повторите попытку.']);
  229. }
  230. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  231. ->with(['success' => 'Документы рекламации успешно загружены!']);
  232. }
  233. public function deleteDocument(Request $request, Reclamation $reclamation, File $file)
  234. {
  235. $this->ensureHasRole([Role::ADMIN]);
  236. $this->ensureCanViewReclamation($reclamation);
  237. $reclamation->documents()->detach($file);
  238. Storage::disk('public')->delete($file->path);
  239. $file->delete();
  240. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]);
  241. }
  242. public function uploadAct(Request $request, Reclamation $reclamation, FileService $fileService)
  243. {
  244. $this->ensureHasRole([Role::ADMIN, Role::MANAGER, Role::BRIGADIER, Role::WAREHOUSE_HEAD]);
  245. $this->ensureCanViewReclamation($reclamation);
  246. $data = $request->validate([
  247. 'acts.*' => 'file',
  248. ]);
  249. try {
  250. $f = [];
  251. $i = 0;
  252. foreach ($data['acts'] as $document) {
  253. if ($i++ >= 5) break;
  254. $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/act', $document);
  255. }
  256. $reclamation->acts()->syncWithoutDetaching($f);
  257. } catch (Throwable $e) {
  258. report($e);
  259. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  260. ->with(['error' => 'Ошибка загрузки актов. Проверьте имя файла и повторите попытку.']);
  261. }
  262. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  263. ->with(['success' => 'Акты успешно загружены!']);
  264. }
  265. public function deleteAct(Request $request, Reclamation $reclamation, File $file)
  266. {
  267. $this->ensureHasRole([Role::ADMIN]);
  268. $this->ensureCanViewReclamation($reclamation);
  269. $reclamation->acts()->detach($file);
  270. Storage::disk('public')->delete($file->path);
  271. $file->delete();
  272. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]);
  273. }
  274. public function updateDetails(StoreReclamationDetailsRequest $request, Reclamation $reclamation)
  275. {
  276. $names = $request->validated('name');
  277. $quantity = $request->validated('quantity');
  278. $withDocuments = $request->validated('with_documents');
  279. $reservationService = app(SparePartReservationService::class);
  280. foreach ($names as $key => $name) {
  281. if (!$name) continue;
  282. if ((int)$quantity[$key] >= 1) {
  283. // Проверяем, является ли это запчастью
  284. $sparePart = \App\Models\SparePart::where('article', $name)->first();
  285. if ($sparePart) {
  286. // Резервирование вместо прямого списания
  287. $withDocs = isset($withDocuments[$key]) && $withDocuments[$key];
  288. $qty = (int)$quantity[$key];
  289. // Получаем текущее количество в pivot
  290. $currentPivot = $reclamation->spareParts()->find($sparePart->id);
  291. $currentQty = $currentPivot?->pivot->quantity ?? 0;
  292. $diff = $qty - $currentQty;
  293. if ($diff > 0) {
  294. // Нужно зарезервировать дополнительное количество
  295. $result = $reservationService->reserve(
  296. $sparePart->id,
  297. $diff,
  298. $withDocs,
  299. $reclamation->id
  300. );
  301. // Обновляем pivot с учётом результата
  302. $reclamation->spareParts()->syncWithoutDetaching([
  303. $sparePart->id => [
  304. 'quantity' => $qty,
  305. 'with_documents' => $withDocs,
  306. 'status' => $result->isFullyReserved() ? 'reserved' : 'pending',
  307. 'reserved_qty' => $currentQty + $result->reserved,
  308. ]
  309. ]);
  310. } elseif ($diff < 0) {
  311. // Уменьшение — отменяем часть резерва
  312. $reservationService->adjustReservation(
  313. $reclamation->id,
  314. $sparePart->id,
  315. $withDocs,
  316. $qty
  317. );
  318. $reclamation->spareParts()->syncWithoutDetaching([
  319. $sparePart->id => [
  320. 'quantity' => $qty,
  321. 'with_documents' => $withDocs,
  322. 'reserved_qty' => $qty,
  323. ]
  324. ]);
  325. } else {
  326. // Количество не изменилось, возможно изменился with_documents
  327. $reclamation->spareParts()->syncWithoutDetaching([
  328. $sparePart->id => [
  329. 'quantity' => $qty,
  330. 'with_documents' => $withDocs,
  331. ]
  332. ]);
  333. }
  334. } else {
  335. // Обычная деталь
  336. ReclamationDetail::query()->updateOrCreate(
  337. ['reclamation_id' => $reclamation->id, 'name' => $name],
  338. ['quantity' => $quantity[$key]]
  339. );
  340. }
  341. } else {
  342. // Удаление
  343. // Проверяем, является ли это запчастью — отменяем резервы
  344. $sparePartToRemove = \App\Models\SparePart::where('article', $name)->first();
  345. if ($sparePartToRemove) {
  346. // Отменяем все резервы для этой запчасти в рекламации
  347. $reservationService->cancelForReclamation(
  348. $reclamation->id,
  349. $sparePartToRemove->id
  350. );
  351. // Удаляем связь
  352. $reclamation->spareParts()->detach($sparePartToRemove->id);
  353. } else {
  354. // Обычная деталь
  355. ReclamationDetail::query()
  356. ->where('reclamation_id', $reclamation->id)
  357. ->where('name', $name)
  358. ->delete();
  359. }
  360. }
  361. }
  362. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]);
  363. }
  364. public function updateSpareParts(StoreReclamationSparePartsRequest $request, Reclamation $reclamation)
  365. {
  366. $rows = $request->validated('rows') ?? [];
  367. $reservationService = app(SparePartReservationService::class);
  368. // Получаем текущие привязки для сравнения
  369. $currentSpareParts = $reclamation->spareParts->keyBy('id');
  370. // Определяем какие запчасти были удалены
  371. $newSparePartIds = collect($rows)->pluck('spare_part_id')->filter()->toArray();
  372. $removedIds = $currentSpareParts->keys()->diff($newSparePartIds);
  373. // Отменяем резервы для удалённых запчастей
  374. foreach ($removedIds as $removedId) {
  375. $current = $currentSpareParts->get($removedId);
  376. if ($current) {
  377. $reservationService->cancelForReclamation(
  378. $reclamation->id,
  379. $removedId,
  380. $current->pivot->with_documents
  381. );
  382. }
  383. }
  384. // Собираем новые привязки
  385. $newSpareParts = [];
  386. foreach ($rows as $row) {
  387. $sparePartId = $row['spare_part_id'] ?? null;
  388. if (empty($sparePartId)) continue;
  389. $quantity = (int)($row['quantity'] ?? 0);
  390. if ($quantity < 1) continue;
  391. $withDocs = !empty($row['with_documents']) && $row['with_documents'] != '0';
  392. // Проверяем, изменилось ли количество
  393. $currentQty = $currentSpareParts->get($sparePartId)?->pivot->quantity ?? 0;
  394. $currentReserved = $currentSpareParts->get($sparePartId)?->pivot->reserved_qty ?? 0;
  395. $diff = $quantity - $currentQty;
  396. $status = 'pending';
  397. $reservedQty = $currentReserved;
  398. if ($diff > 0) {
  399. // Нужно зарезервировать дополнительное количество
  400. $result = $reservationService->reserve(
  401. $sparePartId,
  402. $diff,
  403. $withDocs,
  404. $reclamation->id
  405. );
  406. $reservedQty = $currentReserved + $result->reserved;
  407. $status = $reservedQty >= $quantity ? 'reserved' : 'pending';
  408. } elseif ($diff < 0) {
  409. // Уменьшение — отменяем часть резерва
  410. $reservationService->adjustReservation(
  411. $reclamation->id,
  412. $sparePartId,
  413. $withDocs,
  414. $quantity
  415. );
  416. $reservedQty = $quantity;
  417. $status = 'reserved';
  418. } else {
  419. // Количество не изменилось
  420. $status = $currentReserved >= $quantity ? 'reserved' : 'pending';
  421. }
  422. $newSpareParts[$sparePartId] = [
  423. 'quantity' => $quantity,
  424. 'with_documents' => $withDocs,
  425. 'status' => $status,
  426. 'reserved_qty' => $reservedQty,
  427. ];
  428. }
  429. // Синхронизируем (заменяем все старые привязки новыми)
  430. $reclamation->spareParts()->sync($newSpareParts);
  431. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]);
  432. }
  433. public function generateReclamationPack(Request $request, Reclamation $reclamation)
  434. {
  435. GenerateReclamationPack::dispatch($reclamation, auth()->user()->id);
  436. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  437. ->with(['success' => 'Задача генерации документов создана!']);
  438. }
  439. public function generateReclamationPaymentPack(Request $request, Reclamation $reclamation)
  440. {
  441. GenerateReclamationPaymentPack::dispatch($reclamation, auth()->user()->id);
  442. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  443. ->with(['success' => 'Задача генерации пакета документов на оплату создана!']);
  444. }
  445. public function generatePhotosBeforePack(Request $request, Reclamation $reclamation)
  446. {
  447. GenerateFilesPack::dispatch($reclamation, $reclamation->photos_before, auth()->user()->id, 'Фото проблемы');
  448. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  449. ->with(['success' => 'Задача архивации создана!']);
  450. }
  451. public function generatePhotosAfterPack(Request $request, Reclamation $reclamation)
  452. {
  453. GenerateFilesPack::dispatch($reclamation, $reclamation->photos_after, auth()->user()->id, 'Фото после');
  454. return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
  455. ->with(['success' => 'Задача архивации создана!']);
  456. }
  457. private function ensureCanViewReclamation(Reclamation $reclamation): void
  458. {
  459. if (hasRole(Role::BRIGADIER) && (int)$reclamation->brigadier_id !== (int)auth()->id()) {
  460. abort(403);
  461. }
  462. }
  463. private function ensureHasRole(array $roles): void
  464. {
  465. if (!count(array_intersect($roles, Role::effectiveRoles((string)auth()->user()?->role)))) {
  466. abort(403);
  467. }
  468. }
  469. }