ProductionOrderController.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Http\Controllers;
  4. use App\Enums\ProductionOrderExecutionType;
  5. use App\Enums\ProductionOrderNotificationEvent;
  6. use App\Enums\ProductionOrderStatus;
  7. use App\Http\Requests\SaveProductionOrderRequest;
  8. use App\Jobs\Export\ExportProductionOrdersJob;
  9. use App\Models\CommonCatalogItem;
  10. use App\Models\ProductionOrder;
  11. use App\Models\Role;
  12. use App\Models\User;
  13. use App\Services\Access\FieldAccessService;
  14. use App\Services\NotificationService;
  15. use App\Services\ProductionOrderService;
  16. use Illuminate\Contracts\View\View;
  17. use Illuminate\Database\Eloquent\Builder;
  18. use Illuminate\Http\JsonResponse;
  19. use Illuminate\Http\RedirectResponse;
  20. use Illuminate\Http\Request;
  21. use Illuminate\Support\Arr;
  22. class ProductionOrderController extends Controller
  23. {
  24. private const INDEX_HEADER = [
  25. 'id' => 'ID',
  26. 'order_number' => 'Номер заказа',
  27. 'customer_name' => 'Заказчик',
  28. 'object_address' => 'Адрес объекта',
  29. 'invoice_number' => '№ счёта',
  30. 'payment_date' => 'Дата оплаты',
  31. 'supply_working_days' => 'Срок поставки',
  32. 'contract_shipment_date' => 'Дата отгрузки по договору',
  33. 'application_shipment_date' => 'Дата отгрузки по заявке',
  34. 'status_name' => 'Статус',
  35. 'delivery_dates' => 'Дата доставки',
  36. 'installation_dates' => 'Дата монтажа',
  37. 'manager_id' => 'Менеджер',
  38. 'note' => 'Примечание',
  39. ];
  40. private const SEARCH_FIELDS = [
  41. 'order_number',
  42. 'customer_name',
  43. 'object_address',
  44. 'invoice_number',
  45. 'contract_number',
  46. 'note',
  47. ];
  48. private const FIELD_MAP = [
  49. 'status_name' => 'status',
  50. 'execution_type_name' => 'execution_type',
  51. 'items_count' => 'id',
  52. 'delivery_dates' => 'delivery_date',
  53. 'installation_dates' => 'installation_date',
  54. ];
  55. protected array $data = [
  56. 'active' => 'schedule_orders',
  57. 'title' => 'График заказов',
  58. 'id' => 'schedule_orders',
  59. 'header' => self::INDEX_HEADER,
  60. 'searchFields' => self::SEARCH_FIELDS,
  61. ];
  62. public function index(Request $request): View
  63. {
  64. $this->data['header'] = self::INDEX_HEADER;
  65. $this->data['searchFields'] = self::SEARCH_FIELDS;
  66. unset($this->data['dates'], $this->data['ranges'], $this->data['filters']);
  67. session(['gp_schedule_orders' => $request->query()]);
  68. $nav = $this->startNavigationContext($request);
  69. $model = new ProductionOrder;
  70. $this->data['header'] = array_filter(
  71. $this->data['header'],
  72. fn (string $field): bool => $request->user()->canViewField(
  73. 'schedule-orders',
  74. self::FIELD_MAP[$field] ?? $field,
  75. ),
  76. ARRAY_FILTER_USE_KEY,
  77. );
  78. $this->data['searchFields'] = array_values(array_filter(
  79. $this->data['searchFields'],
  80. fn (string $field): bool => $request->user()->canViewField('schedule-orders', $field),
  81. ));
  82. $this->createDateFilters(
  83. $model,
  84. ...array_values(array_intersect([
  85. 'payment_date',
  86. 'contract_shipment_date',
  87. 'application_shipment_date',
  88. ], array_keys($this->data['header']))),
  89. );
  90. if (isset($this->data['header']['supply_working_days'])) {
  91. $this->createRangeFilters($model, 'supply_working_days');
  92. }
  93. foreach (['status_name', 'execution_type_name'] as $field) {
  94. if (isset($this->data['header'][$field])) {
  95. $this->data['filters'][$field] = [
  96. 'title' => $this->data['header'][$field],
  97. 'values' => [],
  98. ];
  99. }
  100. }
  101. $query = ProductionOrder::query()
  102. ->with([
  103. 'manager',
  104. 'deliveries:id,production_order_id,delivery_date',
  105. 'installations:id,production_order_id,installation_date',
  106. ])
  107. ->withCount('items')
  108. ->withMin('deliveries', 'delivery_date')
  109. ->withMin('installations', 'installation_date');
  110. $this->acceptOrderFilters($query, $request);
  111. $this->acceptSearch($query, $request);
  112. $requestedSort = $request->string('sortBy')->toString();
  113. $sortMap = [
  114. 'status_name' => 'status',
  115. 'execution_type_name' => 'execution_type',
  116. 'items_count' => 'items_count',
  117. 'delivery_dates' => 'deliveries_min_delivery_date',
  118. 'installation_dates' => 'installations_min_installation_date',
  119. ];
  120. if (isset($sortMap[$requestedSort])) {
  121. $request->merge(['sortBy' => $sortMap[$requestedSort]]);
  122. }
  123. $this->setSortAndOrderBy($model, $request);
  124. if ($requestedSort !== '' && isset($sortMap[$requestedSort])) {
  125. $this->data['sortBy'] = $requestedSort;
  126. $query->orderBy($sortMap[$requestedSort], $this->data['orderBy']);
  127. $query->orderBy('id', $this->data['orderBy']);
  128. } else {
  129. $this->applyStableSorting($query);
  130. }
  131. $this->data['orders'] = $query->paginate($this->data['per_page'])->withQueryString();
  132. $this->data['nav'] = $nav;
  133. return view('production_orders.index', $this->data);
  134. }
  135. public function create(Request $request, FieldAccessService $fieldAccess): View
  136. {
  137. return $this->orderView($request, $fieldAccess);
  138. }
  139. public function export(Request $request): RedirectResponse
  140. {
  141. $validated = $request->validate([
  142. 'filters' => ['nullable', 'array'],
  143. 'filters.*' => ['nullable', 'string', 'max:1000'],
  144. 's' => ['nullable', 'string', 'max:500'],
  145. ]);
  146. $request->merge([
  147. 'filters' => Arr::only($validated['filters'] ?? [], [
  148. 'id',
  149. 'order_number',
  150. 'customer_name',
  151. 'object_address',
  152. 'invoice_number',
  153. 'payment_date',
  154. 'payment_date_from',
  155. 'payment_date_to',
  156. 'supply_working_days',
  157. 'supply_working_days_from',
  158. 'supply_working_days_to',
  159. 'contract_shipment_date',
  160. 'contract_shipment_date_from',
  161. 'contract_shipment_date_to',
  162. 'application_shipment_date',
  163. 'application_shipment_date_from',
  164. 'application_shipment_date_to',
  165. 'status_name',
  166. 'execution_type_name',
  167. 'manager_id',
  168. 'delivery_dates',
  169. 'installation_dates',
  170. 'note',
  171. ]),
  172. 's' => $validated['s'] ?? null,
  173. ]);
  174. $this->data['dates'] = array_fill_keys([
  175. 'payment_date',
  176. 'contract_shipment_date',
  177. 'application_shipment_date',
  178. ], true);
  179. $this->data['ranges'] = ['supply_working_days' => true];
  180. $query = ProductionOrder::query();
  181. $this->acceptOrderFilters($query, $request);
  182. $this->acceptSearch($query, $request);
  183. $orderIds = $query->orderByDesc('created_at')->orderByDesc('id')->pluck('id')->all();
  184. ExportProductionOrdersJob::dispatch($orderIds, (int) $request->user()->id);
  185. return redirect()
  186. ->route('schedule.orders', session('gp_schedule_orders'))
  187. ->with('success', 'Задача экспорта графика заказов создана.');
  188. }
  189. public function show(
  190. Request $request,
  191. ProductionOrder $productionOrder,
  192. FieldAccessService $fieldAccess,
  193. ): View {
  194. return $this->orderView(
  195. $request,
  196. $fieldAccess,
  197. $productionOrder->load([
  198. 'manager',
  199. 'items.catalogItem.imageFile',
  200. 'items.passportFile',
  201. 'deliveries.driver',
  202. 'installations.brigadier',
  203. 'activities.user',
  204. 'documents',
  205. 'photos',
  206. 'chatMessages.user',
  207. 'chatMessages.targetUser',
  208. 'chatMessages.notifiedUsers',
  209. 'chatMessages.files',
  210. ]),
  211. );
  212. }
  213. public function store(
  214. SaveProductionOrderRequest $request,
  215. FieldAccessService $fieldAccess,
  216. ProductionOrderService $service,
  217. NotificationService $notificationService,
  218. ): RedirectResponse {
  219. $validated = $request->validated();
  220. $items = $validated['items'];
  221. $fields = array_keys(config('access.schedule-orders.fields', []));
  222. $payload = $fieldAccess->filterValidatedPayload(
  223. $request->user(),
  224. 'schedule-orders',
  225. Arr::only($validated, $fields),
  226. );
  227. $payload['items'] = $items;
  228. $order = $service->create($payload, $request->user());
  229. $notificationService->notifyProductionOrderEvent(
  230. $order,
  231. ProductionOrderNotificationEvent::Created,
  232. $request->user(),
  233. );
  234. return redirect()->route('schedule.orders.show', $this->withNav(
  235. ['productionOrder' => $order],
  236. $this->resolveNavToken($request),
  237. ))->with('success', 'Заказ создан.');
  238. }
  239. public function update(
  240. SaveProductionOrderRequest $request,
  241. ProductionOrder $productionOrder,
  242. FieldAccessService $fieldAccess,
  243. ProductionOrderService $service,
  244. NotificationService $notificationService,
  245. ): RedirectResponse {
  246. $validated = $request->validated();
  247. $fields = array_keys(config('access.schedule-orders.fields', []));
  248. $payload = $fieldAccess->filterValidatedPayload(
  249. $request->user(),
  250. 'schedule-orders',
  251. Arr::only($validated, $fields),
  252. );
  253. $canManageItems = $request->user()->hasPermission('schedule-orders.items.manage');
  254. $editableItemFields = array_values(array_filter(
  255. ['factory_number', 'manufacture_date'],
  256. fn (string $field): bool => $request->user()->canUpdateField('schedule-orders', $field),
  257. ));
  258. if (($canManageItems || $editableItemFields !== []) && isset($validated['items'])) {
  259. $payload['items'] = $validated['items'];
  260. }
  261. $previousStatus = $productionOrder->status;
  262. $updatedOrder = $service->update(
  263. $productionOrder,
  264. $payload,
  265. $request->user(),
  266. $canManageItems,
  267. $editableItemFields,
  268. );
  269. if ($updatedOrder->status !== $previousStatus) {
  270. $notificationService->notifyProductionOrderEvent(
  271. $updatedOrder,
  272. ProductionOrderNotificationEvent::StatusChanged,
  273. $request->user(),
  274. );
  275. }
  276. return redirect()->route('schedule.orders.show', $this->withNav(
  277. ['productionOrder' => $productionOrder],
  278. $this->resolveNavToken($request),
  279. ))->with('success', 'Заказ обновлён.');
  280. }
  281. public function destroy(Request $request, ProductionOrder $productionOrder): RedirectResponse
  282. {
  283. $productionOrder->delete();
  284. return redirect()
  285. ->route('schedule.orders', session('gp_schedule_orders'))
  286. ->with('success', 'Заказ удалён.');
  287. }
  288. public function searchCatalogItems(Request $request): JsonResponse
  289. {
  290. $validated = $request->validate([
  291. 'q' => ['nullable', 'string', 'max:200'],
  292. ]);
  293. $search = trim((string) ($validated['q'] ?? ''));
  294. if (mb_strlen($search) < 2) {
  295. return response()->json([]);
  296. }
  297. $items = CommonCatalogItem::query()
  298. ->with('imageFile')
  299. ->where(function ($query) use ($search): void {
  300. $query
  301. ->where('article', 'like', "%{$search}%")
  302. ->orWhere('calculator_name', 'like', "%{$search}%")
  303. ->orWhere('print_name', 'like', "%{$search}%");
  304. })
  305. ->orderBy('article')
  306. ->limit(50)
  307. ->get(['id', 'image_file_id', 'article', 'calculator_name', 'print_name'])
  308. ->map(static fn (CommonCatalogItem $item): array => [
  309. 'id' => $item->id,
  310. 'label' => $item->article.' — '.($item->calculator_name ?: $item->print_name ?: 'Без наименования'),
  311. 'image' => $item->imageFile?->thumbnail_link,
  312. 'image_full' => $item->imageFile?->link,
  313. ])
  314. ->values();
  315. return response()->json($items);
  316. }
  317. private function orderView(
  318. Request $request,
  319. FieldAccessService $fieldAccess,
  320. ?ProductionOrder $order = null,
  321. ): View {
  322. $nav = $this->resolveNavToken($request);
  323. $this->rememberNavigation($request, $nav);
  324. $fields = array_keys(config('access.schedule-orders.fields', []));
  325. $catalogItemIds = collect($order?->items ?? [])
  326. ->pluck('common_catalog_item_id')
  327. ->merge(collect($request->old('items', []))->pluck('common_catalog_item_id'))
  328. ->filter()
  329. ->unique()
  330. ->values();
  331. $chatUsers = User::query()->orderBy('name')->get(['id', 'name', 'role']);
  332. $responsibleUserIds = User::query()
  333. ->withPermission('schedule-orders.create')
  334. ->pluck('id')
  335. ->map(static fn ($id): int => (int) $id)
  336. ->merge([$order?->manager_id])
  337. ->merge($order?->deliveries->pluck('driver_id') ?? [])
  338. ->merge($order?->installations->pluck('brigadier_id') ?? [])
  339. ->filter()
  340. ->unique()
  341. ->values()
  342. ->all();
  343. return view('production_orders.edit', [
  344. 'active' => 'schedule_orders',
  345. 'title' => $order ? 'Заказ '.$order->order_number : 'Новый заказ',
  346. 'order' => $order,
  347. 'nav' => $nav,
  348. 'back_url' => $this->navigationBackUrl(
  349. $request,
  350. $nav,
  351. route('schedule.orders', session('gp_schedule_orders')),
  352. ),
  353. 'readableFields' => array_fill_keys(
  354. $fieldAccess->filterReadableFields($request->user(), 'schedule-orders', $fields),
  355. true,
  356. ),
  357. 'writableFields' => collect($fields)->mapWithKeys(fn (string $field): array => [
  358. $field => $request->user()->canUpdateField('schedule-orders', $field),
  359. ])->all(),
  360. 'canManageItems' => $request->user()->hasPermission('schedule-orders.items.manage'),
  361. 'canManageDeliveries' => $request->user()->hasPermission('schedule-orders.deliveries.manage'),
  362. 'canManageInstallations' => $request->user()->hasPermission('schedule-orders.installations.manage'),
  363. 'canManageDocuments' => $request->user()->hasPermission('schedule-orders.documents.manage'),
  364. 'canManagePhotos' => $request->user()->hasPermission('schedule-orders.photos.manage'),
  365. 'canManagePassports' => $request->user()->hasPermission('schedule-orders.passports.manage'),
  366. 'canCreateChatMessages' => $request->user()->hasPermission('schedule-orders.chat.create'),
  367. 'canDeleteChatMessages' => $request->user()->hasPermission('schedule-orders.chat.delete'),
  368. 'canUpdateFactoryNumber' => $request->user()->canUpdateField('schedule-orders', 'factory_number'),
  369. 'canUpdateManufactureDate' => $request->user()->canUpdateField('schedule-orders', 'manufacture_date'),
  370. 'managers' => User::query()
  371. ->where('role', Role::MANAGER)
  372. ->orderBy('name')
  373. ->pluck('name', 'id'),
  374. 'drivers' => User::query()
  375. ->whereIn('role', [Role::DRIVER, Role::BRIGADIER])
  376. ->orderBy('name')
  377. ->pluck('name', 'id'),
  378. 'brigadiers' => User::query()
  379. ->where('role', Role::BRIGADIER)
  380. ->orderBy('name')
  381. ->pluck('name', 'id'),
  382. 'catalogItems' => CommonCatalogItem::query()
  383. ->with('imageFile')
  384. ->whereKey($catalogItemIds)
  385. ->get(['id', 'image_file_id', 'article', 'calculator_name', 'print_name'])
  386. ->keyBy('id'),
  387. 'statuses' => ProductionOrderStatus::options(),
  388. 'executionTypes' => ProductionOrderExecutionType::options(),
  389. 'chatUsers' => $chatUsers->map(static fn (User $user): array => [
  390. 'id' => $user->id,
  391. 'name' => $user->name,
  392. 'role' => $user->role,
  393. ])->keyBy('id'),
  394. 'chatResponsibleUserIds' => $responsibleUserIds,
  395. ]);
  396. }
  397. private function acceptOrderFilters(Builder $query, Request $request): void
  398. {
  399. $filters = (array) $request->input('filters', []);
  400. $relationFilters = [
  401. 'delivery_dates' => ['deliveries', 'delivery_date'],
  402. 'installation_dates' => ['installations', 'installation_date'],
  403. ];
  404. foreach ($relationFilters as $filterName => [$relation, $column]) {
  405. $value = trim((string) ($filters[$filterName] ?? ''));
  406. if ($value === '') {
  407. continue;
  408. }
  409. $values = explode('||', $value);
  410. $dates = array_values(array_filter(
  411. $values,
  412. static fn (string $date): bool => $date !== '-пусто-',
  413. ));
  414. $includeEmpty = in_array('-пусто-', $values, true);
  415. $query->where(function ($relationQuery) use ($relation, $column, $dates, $includeEmpty): void {
  416. if ($dates !== []) {
  417. $relationQuery->whereHas(
  418. $relation,
  419. static fn ($dateQuery) => $dateQuery->whereIn($column, $dates),
  420. );
  421. }
  422. if ($includeEmpty) {
  423. $dates !== []
  424. ? $relationQuery->orWhereDoesntHave($relation)
  425. : $relationQuery->whereDoesntHave($relation);
  426. }
  427. });
  428. }
  429. $request->merge(['filters' => Arr::except($filters, array_keys($relationFilters))]);
  430. $this->acceptFilters($query, $request);
  431. $request->merge(['filters' => $filters]);
  432. }
  433. }