ProductionOrderController.php 16 KB

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