edit.blade.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. @extends('layouts.app')
  2. @section('content')
  3. @php
  4. $canView = static fn (string $field): bool => (bool) ($readableFields[$field] ?? false);
  5. $canUpdate = static fn (string $field): bool => (bool) ($writableFields[$field] ?? false);
  6. $itemRows = old('items');
  7. if ($itemRows === null) {
  8. $itemRows = $order
  9. ? $order->items->map(static fn ($item): array => [
  10. 'id' => $item->id,
  11. 'common_catalog_item_id' => $item->common_catalog_item_id,
  12. 'order_item_number' => $item->order_item_number,
  13. 'factory_number' => $item->factory_number,
  14. 'manufacture_date' => $item->manufacture_date?->toDateString(),
  15. 'passport_file_id' => $item->passport_file_id,
  16. ])->values()->all()
  17. : [];
  18. }
  19. @endphp
  20. <div class="px-3">
  21. <div class="row mb-3">
  22. <div class="col-md-7 d-flex align-items-center">
  23. <h3 class="mb-0">{{ $order ? 'Заказ '.$order->order_number : 'Новый заказ' }}</h3>
  24. </div>
  25. @if($order && hasPermission('schedule-orders.delete'))
  26. <div class="col-md-5 text-md-end">
  27. <form action="{{ route('schedule.orders.destroy', $order) }}" method="POST"
  28. onsubmit="return confirm('Удалить заказ?')">
  29. @csrf
  30. @method('DELETE')
  31. <button type="submit" class="btn btn-sm btn-outline-danger">Удалить</button>
  32. </form>
  33. </div>
  34. @endif
  35. </div>
  36. <form action="{{ $order ? route('schedule.orders.update', $order) : route('schedule.orders.store') }}"
  37. method="POST">
  38. @csrf
  39. @if($order) @method('PUT') @endif
  40. <input type="hidden" name="nav" value="{{ $nav ?? '' }}">
  41. <div class="card mb-3">
  42. <div class="card-header"><strong>Заказ</strong></div>
  43. <div class="card-body">
  44. <div class="row">
  45. <div class="col-xl-6">
  46. @if($canView('order_number'))
  47. @include('partials.input', ['name' => 'order_number', 'title' => 'Номер заказа', 'required' => true, 'value' => $order?->order_number, 'disabled' => !$canUpdate('order_number')])
  48. @endif
  49. @if($canView('order_year'))
  50. @include('partials.input', ['name' => 'order_year', 'title' => 'Год заказа', 'type' => 'number', 'min' => 2000, 'max' => 2100, 'required' => true, 'value' => $order?->order_year ?? now()->year, 'disabled' => !$canUpdate('order_year')])
  51. @endif
  52. @if($canView('customer_name'))
  53. @include('partials.input', ['name' => 'customer_name', 'title' => 'Заказчик', 'required' => true, 'value' => $order?->customer_name, 'disabled' => !$canUpdate('customer_name')])
  54. @endif
  55. @if($canView('object_address'))
  56. <div class="row mb-2">
  57. <label for="object_address" class="col-form-label small col-md-4 text-md-end">Адрес объекта <sup>*</sup></label>
  58. <div class="col-md-8">
  59. <textarea name="object_address" id="object_address" rows="3" required
  60. @disabled(!$canUpdate('object_address'))
  61. class="form-control form-control-sm @error('object_address') is-invalid @enderror">{{ old('object_address', $order?->object_address) }}</textarea>
  62. @error('object_address') <div class="invalid-feedback"><strong>{{ $message }}</strong></div> @enderror
  63. </div>
  64. </div>
  65. @endif
  66. @if($canView('manager_id'))
  67. @include('partials.select', ['name' => 'manager_id', 'title' => 'Менеджер', 'required' => true, 'first_empty' => true, 'options' => $managers, 'value' => old('manager_id', $order?->manager_id), 'disabled' => !$canUpdate('manager_id')])
  68. @endif
  69. @if($canView('status'))
  70. @include('partials.select', ['name' => 'status', 'title' => 'Статус', 'required' => true, 'options' => $statuses, 'value' => old('status', $order?->status?->value ?? \App\Enums\ProductionOrderStatus::Placed->value), 'disabled' => !$canUpdate('status')])
  71. @endif
  72. @if($canView('execution_type'))
  73. @include('partials.select', ['name' => 'execution_type', 'title' => 'Тип исполнения', 'required' => true, 'first_empty' => true, 'options' => $executionTypes, 'value' => old('execution_type', $order?->execution_type?->value), 'disabled' => !$canUpdate('execution_type')])
  74. @endif
  75. </div>
  76. <div class="col-xl-6">
  77. @if($canView('invoice_number'))
  78. @include('partials.input', ['name' => 'invoice_number', 'title' => '№ счёта', 'required' => true, 'value' => $order?->invoice_number, 'disabled' => !$canUpdate('invoice_number')])
  79. @endif
  80. @if($canView('invoice_date'))
  81. @include('partials.input', ['name' => 'invoice_date', 'title' => 'Дата счёта', 'type' => 'date', 'value' => $order?->invoice_date?->toDateString(), 'disabled' => !$canUpdate('invoice_date')])
  82. @endif
  83. @if($canView('contract_number'))
  84. @include('partials.input', ['name' => 'contract_number', 'title' => '№ договора', 'value' => $order?->contract_number, 'disabled' => !$canUpdate('contract_number')])
  85. @endif
  86. @if($canView('contract_date'))
  87. @include('partials.input', ['name' => 'contract_date', 'title' => 'Дата договора', 'type' => 'date', 'value' => $order?->contract_date?->toDateString(), 'disabled' => !$canUpdate('contract_date')])
  88. @endif
  89. @if($canView('payment_date'))
  90. @include('partials.input', ['name' => 'payment_date', 'title' => 'Дата оплаты', 'type' => 'date', 'required' => true, 'value' => $order?->payment_date?->toDateString(), 'disabled' => !$canUpdate('payment_date')])
  91. @endif
  92. @if($canView('supply_working_days'))
  93. @include('partials.input', ['name' => 'supply_working_days', 'title' => 'Срок поставки, раб. дней', 'type' => 'number', 'min' => 0, 'max' => 2000, 'required' => true, 'value' => $order?->supply_working_days, 'disabled' => !$canUpdate('supply_working_days')])
  94. @endif
  95. @if($order && $canView('contract_shipment_date'))
  96. @include('partials.input', ['name' => 'contract_shipment_date_display', 'title' => 'Дата отгрузки по договору', 'type' => 'date', 'value' => $order->contract_shipment_date?->toDateString(), 'disabled' => true])
  97. @endif
  98. @if($canView('application_shipment_date'))
  99. @include('partials.input', ['name' => 'application_shipment_date', 'title' => 'Дата отгрузки по заявке', 'type' => 'date', 'value' => $order?->application_shipment_date?->toDateString(), 'disabled' => !$canUpdate('application_shipment_date')])
  100. @endif
  101. @if($canView('note'))
  102. <div class="row mb-2">
  103. <label for="note" class="col-form-label small col-md-4 text-md-end">Примечание</label>
  104. <div class="col-md-8">
  105. <textarea name="note" id="note" rows="3" @disabled(!$canUpdate('note'))
  106. class="form-control form-control-sm @error('note') is-invalid @enderror">{{ old('note', $order?->note) }}</textarea>
  107. @error('note') <div class="invalid-feedback"><strong>{{ $message }}</strong></div> @enderror
  108. </div>
  109. </div>
  110. @endif
  111. </div>
  112. </div>
  113. </div>
  114. </div>
  115. <div class="card mb-3">
  116. <div class="card-header d-flex justify-content-between align-items-center">
  117. <strong>Оборудование</strong>
  118. </div>
  119. <div class="card-body {{ $canManageItems ? 'pb-2' : 'p-0' }}">
  120. @if($canManageItems)
  121. <div class="mb-3">
  122. <label for="production-order-catalog-search" class="form-label small mb-1">
  123. Добавить МАФ из общего каталога
  124. </label>
  125. <div class="row g-2 mb-2">
  126. <div class="col-md-9">
  127. <input type="search" id="production-order-catalog-search"
  128. class="form-control form-control-sm"
  129. placeholder="Введите артикул или наименование">
  130. </div>
  131. <div class="col-md-3">
  132. <div class="input-group input-group-sm">
  133. <span class="input-group-text">Количество</span>
  134. <input type="number" id="production-order-catalog-quantity"
  135. class="form-control" min="1" max="100" value="1">
  136. </div>
  137. </div>
  138. </div>
  139. <select id="production-order-catalog-results" class="form-select form-select-sm" size="6"
  140. aria-label="Результаты поиска МАФ"></select>
  141. <div class="form-text">
  142. Введите не менее двух символов и выберите позицию. Для каждой единицы будет создана отдельная строка.
  143. </div>
  144. </div>
  145. @endif
  146. <div class="table-responsive">
  147. <table class="table table-sm align-middle mb-0">
  148. <thead>
  149. <tr>
  150. <th><input type="checkbox" class="form-check-input" id="production-order-items-select-all" title="Выбрать все"></th>
  151. <th>Позиция общего каталога</th>
  152. <th>Номер заказа МАФ</th>
  153. <th>Заводской номер</th>
  154. <th>Дата производства</th>
  155. <th>Паспорт</th>
  156. @if($canManageItems)<th></th>@endif
  157. </tr>
  158. </thead>
  159. <tbody id="production-order-items">
  160. @foreach($itemRows as $index => $itemRow)
  161. @include('production_orders.partials.item-row', ['index' => $index, 'itemRow' => $itemRow])
  162. @endforeach
  163. </tbody>
  164. </table>
  165. </div>
  166. @error('items') <div class="text-danger small p-2"><strong>{{ $message }}</strong></div> @enderror
  167. </div>
  168. </div>
  169. <div class="d-flex flex-wrap gap-2">
  170. @if(($order && hasPermission('schedule-orders.update')) || (!$order && hasPermission('schedule-orders.create')))
  171. <button type="submit" class="btn btn-sm btn-primary">Сохранить</button>
  172. @endif
  173. <a href="{{ $back_url }}" class="btn btn-sm btn-outline-secondary">Назад</a>
  174. </div>
  175. </form>
  176. @if($order)
  177. <div class="d-flex flex-wrap gap-2 mt-3">
  178. @if(hasPermission('schedule-orders.export'))
  179. <form action="{{ route('schedule.orders.export-items', $order) }}" method="POST">
  180. @csrf
  181. <button type="submit" class="btn btn-sm btn-outline-primary">
  182. <i class="bi bi-file-earmark-spreadsheet"></i> Экспорт МАФ
  183. </button>
  184. </form>
  185. @endif
  186. @if(hasPermission('schedule-orders.documents.generate'))
  187. <form id="production-order-technical-documents"
  188. action="{{ route('schedule.orders.technical-documents', $order) }}" method="POST"
  189. onsubmit="if (!document.querySelector('.production-order-item-select:checked')) { alert('Выберите хотя бы один МАФ'); return false; }">
  190. @csrf
  191. <button type="submit" class="btn btn-sm btn-outline-primary">
  192. <i class="bi bi-file-earmark-zip"></i> Скачать техдокументацию
  193. </button>
  194. </form>
  195. @endif
  196. </div>
  197. @foreach($order->items as $orderItem)
  198. @if($canManagePassports)
  199. <form id="production-order-passport-upload-{{ $orderItem->id }}"
  200. action="{{ route('schedule.orders.items.passport.store', [$order, $orderItem]) }}"
  201. method="POST" enctype="multipart/form-data" class="visually-hidden">
  202. @csrf
  203. </form>
  204. <form id="production-order-passport-delete-{{ $orderItem->id }}"
  205. action="{{ route('schedule.orders.items.passport.destroy', [$order, $orderItem]) }}"
  206. method="POST" class="visually-hidden">
  207. @csrf
  208. @method('DELETE')
  209. </form>
  210. @endif
  211. @endforeach
  212. <div class="row g-3 mt-1">
  213. <div class="col-xl-6">
  214. <div class="card h-100">
  215. <div class="card-header d-flex justify-content-between align-items-center">
  216. <strong>Документы</strong>
  217. @if($canManageDocuments)
  218. <label class="btn btn-sm btn-outline-primary mb-0">
  219. <i class="bi bi-upload"></i> Загрузить
  220. <input type="file" name="documents[]" multiple class="visually-hidden"
  221. form="production-order-documents-upload"
  222. onchange="this.form.requestSubmit()">
  223. </label>
  224. @endif
  225. </div>
  226. <div class="card-body">
  227. @forelse($order->documents as $file)
  228. <div class="d-flex justify-content-between align-items-start gap-2 border-bottom py-2">
  229. <div class="text-break">
  230. @include('production_orders.partials.file-link', [
  231. 'file' => $file,
  232. 'gallery' => 'production-order-documents-'.$order->id,
  233. ])
  234. </div>
  235. @if($canManageDocuments)
  236. <form action="{{ route('schedule.orders.documents.destroy', [$order, $file]) }}" method="POST">
  237. @csrf
  238. @method('DELETE')
  239. <button type="submit" class="btn btn-sm text-danger p-0" title="Удалить">
  240. <i class="bi bi-x-circle-fill"></i>
  241. </button>
  242. </form>
  243. @endif
  244. </div>
  245. @empty
  246. <span class="text-muted">Документы не загружены.</span>
  247. @endforelse
  248. </div>
  249. </div>
  250. </div>
  251. <div class="col-xl-6">
  252. <div class="card h-100">
  253. <div class="card-header d-flex justify-content-between align-items-center">
  254. <strong>Фотографии</strong>
  255. @if($canManagePhotos)
  256. <label class="btn btn-sm btn-outline-primary mb-0">
  257. <i class="bi bi-upload"></i> Загрузить
  258. <input type="file" name="photos[]" multiple accept=".jpg,.jpeg,.png,.webp"
  259. class="visually-hidden" form="production-order-photos-upload"
  260. onchange="this.form.requestSubmit()">
  261. </label>
  262. @endif
  263. </div>
  264. <div class="card-body">
  265. <div class="row g-2">
  266. @forelse($order->photos as $file)
  267. <div class="col-6 col-md-4 position-relative">
  268. <a href="{{ $file->link }}" data-toggle="lightbox"
  269. data-gallery="production-order-photos-{{ $order->id }}" data-size="fullscreen">
  270. <img src="{{ $file->thumbnail_link }}" alt="{{ $file->original_name }}"
  271. class="img-thumbnail w-100">
  272. </a>
  273. @if($canManagePhotos)
  274. <form action="{{ route('schedule.orders.photos.destroy', [$order, $file]) }}"
  275. method="POST" class="position-absolute top-0 end-0 me-2 mt-1">
  276. @csrf
  277. @method('DELETE')
  278. <button type="submit" class="btn btn-sm text-danger bg-white p-0" title="Удалить">
  279. <i class="bi bi-x-circle-fill"></i>
  280. </button>
  281. </form>
  282. @endif
  283. </div>
  284. @empty
  285. <div class="col-12 text-muted">Фотографии не загружены.</div>
  286. @endforelse
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. </div>
  292. @if($canManageDocuments)
  293. <form id="production-order-documents-upload"
  294. action="{{ route('schedule.orders.documents.store', $order) }}"
  295. method="POST" enctype="multipart/form-data" class="visually-hidden">@csrf</form>
  296. @endif
  297. @if($canManagePhotos)
  298. <form id="production-order-photos-upload"
  299. action="{{ route('schedule.orders.photos.store', $order) }}"
  300. method="POST" enctype="multipart/form-data" class="visually-hidden">@csrf</form>
  301. @endif
  302. <div class="card mt-3">
  303. <div class="card-body pt-1">
  304. @include('partials.chat', [
  305. 'title' => 'Чат площадки',
  306. 'messages' => $order->chatMessages,
  307. 'users' => $chatUsers,
  308. 'responsibleUserIds' => $chatResponsibleUserIds,
  309. 'managerUserId' => $order->manager_id,
  310. 'brigadierUserId' => null,
  311. 'action' => route('schedule.orders.chat-messages.store', $order),
  312. 'contextKey' => 'production-order-'.$order->id,
  313. 'submitLabel' => 'Отправить в чат',
  314. 'canCreateMessages' => $canCreateChatMessages,
  315. 'canDeleteMessages' => $canDeleteChatMessages,
  316. 'canSendNotifications' => false,
  317. ])
  318. </div>
  319. </div>
  320. @include('production_orders.partials.planning')
  321. @endif
  322. @if($canManageItems)
  323. <template id="production-order-item-template">
  324. @include('production_orders.partials.item-row', [
  325. 'index' => '__INDEX__',
  326. 'itemRow' => ['common_catalog_item_id' => '', 'order_item_number' => '', 'factory_number' => '', 'manufacture_date' => '', 'passport_file_id' => null],
  327. ])
  328. </template>
  329. @endif
  330. </div>
  331. @if($canManageItems)
  332. @push('scripts')
  333. <script>
  334. document.addEventListener('DOMContentLoaded', () => {
  335. const rows = document.getElementById('production-order-items');
  336. const template = document.getElementById('production-order-item-template');
  337. const search = document.getElementById('production-order-catalog-search');
  338. const results = document.getElementById('production-order-catalog-results');
  339. const quantity = document.getElementById('production-order-catalog-quantity');
  340. let nextIndex = rows.querySelectorAll('tr').length;
  341. let searchTimer;
  342. let requestNumber = 0;
  343. search.addEventListener('input', () => {
  344. clearTimeout(searchTimer);
  345. const query = search.value.trim();
  346. results.replaceChildren();
  347. const currentRequest = ++requestNumber;
  348. if (query.length < 2) return;
  349. searchTimer = setTimeout(async () => {
  350. try {
  351. const url = new URL(@json(route('schedule.orders.catalog-search')), window.location.origin);
  352. url.searchParams.set('q', query);
  353. const response = await fetch(url, {headers: {'Accept': 'application/json'}});
  354. if (!response.ok || currentRequest !== requestNumber) return;
  355. const items = await response.json();
  356. results.replaceChildren(...items.map((item) => {
  357. const option = document.createElement('option');
  358. option.value = item.id;
  359. option.textContent = item.label;
  360. return option;
  361. }));
  362. } catch (error) {
  363. if (currentRequest === requestNumber) results.replaceChildren();
  364. }
  365. }, 250);
  366. });
  367. results.addEventListener('change', () => {
  368. const option = results.selectedOptions[0];
  369. if (!option) return;
  370. const count = Math.min(100, Math.max(1, Number.parseInt(quantity.value, 10) || 1));
  371. for (let i = 0; i < count; i++) {
  372. const fragment = template.content.cloneNode(true);
  373. const row = fragment.querySelector('tr');
  374. row.innerHTML = row.innerHTML.replaceAll('__INDEX__', nextIndex++);
  375. row.querySelector('.production-order-item-catalog-id').value = option.value;
  376. row.querySelector('.production-order-item-catalog-label').textContent = option.textContent;
  377. rows.appendChild(fragment);
  378. }
  379. search.value = '';
  380. quantity.value = '1';
  381. results.replaceChildren();
  382. });
  383. rows.addEventListener('click', (event) => {
  384. const button = event.target.closest('.remove-production-order-item');
  385. if (!button) return;
  386. button.closest('tr').remove();
  387. });
  388. });
  389. </script>
  390. @endpush
  391. @endif
  392. @if($order)
  393. @push('scripts')
  394. <script>
  395. document.addEventListener('DOMContentLoaded', () => {
  396. const selectAll = document.getElementById('production-order-items-select-all');
  397. if (!selectAll) return;
  398. selectAll.addEventListener('change', () => {
  399. document.querySelectorAll('.production-order-item-select').forEach((checkbox) => {
  400. checkbox.checked = selectAll.checked;
  401. });
  402. });
  403. });
  404. </script>
  405. @endpush
  406. @endif
  407. @endsection