| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- @extends('layouts.app')
- @section('content')
- @php
- $canView = static fn (string $field): bool => (bool) ($readableFields[$field] ?? false);
- $canUpdate = static fn (string $field): bool => (bool) ($writableFields[$field] ?? false);
- $itemRows = old('items');
- if ($itemRows === null) {
- $itemRows = $order
- ? $order->items->map(static fn ($item): array => [
- 'id' => $item->id,
- 'common_catalog_item_id' => $item->common_catalog_item_id,
- 'order_item_number' => $item->order_item_number,
- 'factory_number' => $item->factory_number,
- 'manufacture_date' => $item->manufacture_date?->toDateString(),
- 'passport_file_id' => $item->passport_file_id,
- ])->values()->all()
- : [];
- }
- @endphp
- <div class="px-3">
- <div class="row mb-3">
- <div class="col-md-7 d-flex align-items-center">
- <h3 class="mb-0">{{ $order ? 'Заказ '.$order->order_number : 'Новый заказ' }}</h3>
- </div>
- @if($order && hasPermission('schedule-orders.delete'))
- <div class="col-md-5 text-md-end">
- <form action="{{ route('schedule.orders.destroy', $order) }}" method="POST"
- onsubmit="return confirm('Удалить заказ?')">
- @csrf
- @method('DELETE')
- <button type="submit" class="btn btn-sm btn-outline-danger">Удалить</button>
- </form>
- </div>
- @endif
- </div>
- <form action="{{ $order ? route('schedule.orders.update', $order) : route('schedule.orders.store') }}"
- method="POST">
- @csrf
- @if($order) @method('PUT') @endif
- <input type="hidden" name="nav" value="{{ $nav ?? '' }}">
- <div class="row g-3 mb-3 align-items-start">
- <div class="col-xl-5">
- <div class="card h-100">
- <div class="card-header"><strong>Заказ</strong></div>
- <div class="card-body">
- <div class="row">
- <div class="col-12">
- @if($canView('order_number'))
- @include('partials.input', ['name' => 'order_number', 'title' => 'Номер заказа', 'required' => true, 'value' => $order?->order_number, 'disabled' => !$canUpdate('order_number')])
- @endif
- @if($canView('order_year'))
- @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')])
- @endif
- @if($canView('customer_name'))
- @include('partials.input', ['name' => 'customer_name', 'title' => 'Заказчик', 'required' => true, 'value' => $order?->customer_name, 'disabled' => !$canUpdate('customer_name')])
- @endif
- @if($canView('object_address'))
- <div class="row mb-2">
- <label for="object_address" class="col-form-label small col-md-4 text-md-end">Адрес объекта <sup>*</sup></label>
- <div class="col-md-8">
- <textarea name="object_address" id="object_address" rows="3" required
- @disabled(!$canUpdate('object_address'))
- class="form-control form-control-sm @error('object_address') is-invalid @enderror">{{ old('object_address', $order?->object_address) }}</textarea>
- @error('object_address') <div class="invalid-feedback"><strong>{{ $message }}</strong></div> @enderror
- </div>
- </div>
- @endif
- @if($canView('manager_id'))
- @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')])
- @endif
- @if($canView('status'))
- @include('partials.select', ['name' => 'status', 'title' => 'Статус', 'required' => true, 'options' => $statuses, 'value' => old('status', $order?->status?->value ?? \App\Enums\ProductionOrderStatus::Placed->value), 'disabled' => !$canUpdate('status')])
- @endif
- @if($canView('execution_type'))
- @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')])
- @endif
- </div>
- <div class="col-12">
- @if($canView('invoice_number'))
- @include('partials.input', ['name' => 'invoice_number', 'title' => '№ счёта', 'required' => true, 'value' => $order?->invoice_number, 'disabled' => !$canUpdate('invoice_number')])
- @endif
- @if($canView('invoice_date'))
- @include('partials.input', ['name' => 'invoice_date', 'title' => 'Дата счёта', 'type' => 'date', 'value' => $order?->invoice_date?->toDateString(), 'disabled' => !$canUpdate('invoice_date')])
- @endif
- @if($canView('contract_number'))
- @include('partials.input', ['name' => 'contract_number', 'title' => '№ договора', 'value' => $order?->contract_number, 'disabled' => !$canUpdate('contract_number')])
- @endif
- @if($canView('contract_date'))
- @include('partials.input', ['name' => 'contract_date', 'title' => 'Дата договора', 'type' => 'date', 'value' => $order?->contract_date?->toDateString(), 'disabled' => !$canUpdate('contract_date')])
- @endif
- @if($canView('payment_date'))
- @include('partials.input', ['name' => 'payment_date', 'title' => 'Дата оплаты', 'type' => 'date', 'required' => true, 'value' => $order?->payment_date?->toDateString(), 'disabled' => !$canUpdate('payment_date')])
- @endif
- @if($canView('supply_working_days'))
- @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')])
- @endif
- @if($order && $canView('contract_shipment_date'))
- @include('partials.input', ['name' => 'contract_shipment_date_display', 'title' => 'Дата отгрузки по договору', 'type' => 'date', 'value' => $order->contract_shipment_date?->toDateString(), 'disabled' => true])
- @endif
- @if($canView('application_shipment_date'))
- @include('partials.input', ['name' => 'application_shipment_date', 'title' => 'Дата отгрузки по заявке', 'type' => 'date', 'value' => $order?->application_shipment_date?->toDateString(), 'disabled' => !$canUpdate('application_shipment_date')])
- @endif
- @if($canView('note'))
- <div class="row mb-2">
- <label for="note" class="col-form-label small col-md-4 text-md-end">Примечание</label>
- <div class="col-md-8">
- <textarea name="note" id="note" rows="3" @disabled(!$canUpdate('note'))
- class="form-control form-control-sm @error('note') is-invalid @enderror">{{ old('note', $order?->note) }}</textarea>
- @error('note') <div class="invalid-feedback"><strong>{{ $message }}</strong></div> @enderror
- </div>
- </div>
- @endif
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="col-xl-7">
- <div class="card h-100 overflow-hidden">
- <div class="card-header d-flex justify-content-between align-items-center">
- <strong>Оборудование</strong>
- </div>
- <div class="card-body {{ $canManageItems ? 'pb-2' : 'p-0' }}">
- @if($canManageItems)
- <div class="mb-3">
- <label for="production-order-catalog-search" class="form-label small mb-1">
- Добавить МАФ из общего каталога
- </label>
- <div class="row g-2 mb-2">
- <div class="col-md-9">
- <input type="search" id="production-order-catalog-search"
- class="form-control form-control-sm"
- placeholder="Введите артикул или наименование">
- </div>
- <div class="col-md-3">
- <div class="input-group input-group-sm">
- <span class="input-group-text">Количество</span>
- <input type="number" id="production-order-catalog-quantity"
- class="form-control" min="1" max="100" value="1">
- </div>
- </div>
- </div>
- <select id="production-order-catalog-results" class="form-select form-select-sm" size="6"
- aria-label="Результаты поиска МАФ"></select>
- <div class="form-text">
- Введите не менее двух символов и выберите позицию. Для каждой единицы будет создана отдельная строка.
- </div>
- </div>
- @endif
- <div class="table-responsive">
- <table class="table table-sm align-middle mb-0">
- <thead>
- <tr>
- <th><input type="checkbox" class="form-check-input" id="production-order-items-select-all" title="Выбрать все"></th>
- <th>Картинка</th>
- <th>Позиция общего каталога</th>
- <th>Номер заказа МАФ</th>
- <th>Заводской номер</th>
- <th>Дата производства</th>
- <th>Паспорт</th>
- @if($canManageItems)<th></th>@endif
- </tr>
- </thead>
- <tbody id="production-order-items">
- @foreach($itemRows as $index => $itemRow)
- @include('production_orders.partials.item-row', ['index' => $index, 'itemRow' => $itemRow])
- @endforeach
- </tbody>
- </table>
- </div>
- @error('items') <div class="text-danger small p-2"><strong>{{ $message }}</strong></div> @enderror
- </div>
- </div>
- </div>
- </div>
- <div class="d-flex flex-wrap gap-2">
- @if(($order && hasPermission('schedule-orders.update')) || (!$order && hasPermission('schedule-orders.create')))
- <button type="submit" class="btn btn-sm btn-primary">Сохранить</button>
- @endif
- <a href="{{ $back_url }}" class="btn btn-sm btn-outline-secondary">Назад</a>
- </div>
- </form>
- @if($order)
- <div class="d-flex flex-wrap gap-2 mt-3">
- @if($canManageDeliveries)
- <a href="#production-order-delivery-new" class="btn btn-sm btn-outline-primary">
- <i class="bi bi-truck"></i> Перенести в график доставок
- </a>
- @endif
- @if($canManageInstallations)
- <a href="#production-order-installation-new" class="btn btn-sm btn-outline-primary">
- <i class="bi bi-tools"></i> Перенести в график монтажей
- </a>
- @endif
- @if(hasPermission('schedule-orders.export'))
- <form action="{{ route('schedule.orders.export-items', $order) }}" method="POST">
- @csrf
- <button type="submit" class="btn btn-sm btn-outline-primary">
- <i class="bi bi-file-earmark-spreadsheet"></i> Экспорт МАФ
- </button>
- </form>
- @endif
- @if(hasPermission('schedule-orders.documents.generate'))
- <form id="production-order-technical-documents"
- action="{{ route('schedule.orders.technical-documents', $order) }}" method="POST"
- onsubmit="if (!document.querySelector('.production-order-item-select:checked')) { alert('Выберите хотя бы один МАФ'); return false; }">
- @csrf
- <button type="submit" class="btn btn-sm btn-outline-primary">
- <i class="bi bi-file-earmark-zip"></i> Скачать техдокументацию
- </button>
- </form>
- @endif
- @if(hasPermission('reclamations.create'))
- <form id="production-order-reclamation"
- action="{{ route('schedule.orders.reclamations.store', $order) }}" method="POST">
- @csrf
- <input type="hidden" name="nav" value="{{ $nav ?? '' }}">
- <button type="submit" class="btn btn-sm btn-outline-danger">
- <i class="bi bi-exclamation-triangle"></i> Создать рекламацию
- </button>
- </form>
- @endif
- </div>
- @foreach($order->items as $orderItem)
- @if($canManagePassports)
- <form id="production-order-passport-upload-{{ $orderItem->id }}"
- action="{{ route('schedule.orders.items.passport.store', [$order, $orderItem]) }}"
- method="POST" enctype="multipart/form-data" class="visually-hidden">
- @csrf
- </form>
- <form id="production-order-passport-delete-{{ $orderItem->id }}"
- action="{{ route('schedule.orders.items.passport.destroy', [$order, $orderItem]) }}"
- method="POST" class="visually-hidden">
- @csrf
- @method('DELETE')
- </form>
- @endif
- @endforeach
- <div class="row g-3 mt-1">
- <div class="col-xl-6">
- <div class="card h-100">
- <div class="card-header d-flex justify-content-between align-items-center">
- <strong>Документы</strong>
- @if($canManageDocuments)
- <button type="button" class="btn btn-sm text-success"
- onclick="$('#production-order-documents-input').trigger('click')">
- <i class="bi bi-plus-circle-fill"></i> Загрузить
- </button>
- @endif
- </div>
- <div class="card-body">
- @forelse($order->documents as $file)
- <div class="d-flex justify-content-between align-items-start gap-2 border-bottom py-2">
- <div class="text-break">
- @include('production_orders.partials.file-link', [
- 'file' => $file,
- 'gallery' => 'production-order-documents-'.$order->id,
- ])
- </div>
- @if($canManageDocuments)
- <form action="{{ route('schedule.orders.documents.destroy', [$order, $file]) }}" method="POST">
- @csrf
- @method('DELETE')
- <button type="submit" class="btn btn-sm text-danger p-0" title="Удалить">
- <i class="bi bi-x-circle-fill"></i>
- </button>
- </form>
- @endif
- </div>
- @empty
- <span class="text-muted">Документы не загружены.</span>
- @endforelse
- </div>
- </div>
- </div>
- <div class="col-xl-6">
- <div class="card">
- <div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
- <strong>Фотографии ({{ $order->photos->count() }})</strong>
- <div class="d-flex flex-wrap gap-1">
- @if($canManagePhotos)
- <button type="button" class="btn btn-sm text-success"
- onclick="$('#production-order-photos-input').trigger('click')">
- <i class="bi bi-plus-circle-fill"></i> Загрузить
- </button>
- @endif
- <button type="button" class="btn btn-sm btn-outline-secondary collapsed"
- data-bs-toggle="collapse"
- data-bs-target="#production-order-photos-{{ $order->id }}"
- aria-expanded="false"
- aria-controls="production-order-photos-{{ $order->id }}">
- <i class="bi bi-images"></i> Показать все
- </button>
- </div>
- </div>
- <div id="production-order-photos-{{ $order->id }}" class="card-body collapse"
- data-lazy-images>
- <div class="row g-2">
- @forelse($order->photos as $file)
- <div class="col-auto position-relative">
- <a href="{{ $file->link }}" data-toggle="lightbox"
- data-gallery="production-order-photos-{{ $order->id }}" data-size="fullscreen">
- <img data-src="{{ $file->thumbnail_link }}" alt="{{ $file->original_name }}"
- class="img-thumbnail maf-img">
- </a>
- @if($canManagePhotos)
- <form action="{{ route('schedule.orders.photos.destroy', [$order, $file]) }}"
- method="POST" class="position-absolute top-0 end-0 me-2 mt-1">
- @csrf
- @method('DELETE')
- <button type="submit" class="btn btn-sm text-danger bg-white p-0" title="Удалить">
- <i class="bi bi-x-circle-fill"></i>
- </button>
- </form>
- @endif
- </div>
- @empty
- <div class="col-12 text-muted">Фотографии не загружены.</div>
- @endforelse
- </div>
- </div>
- </div>
- </div>
- </div>
- @if($canManageDocuments)
- <form id="production-order-documents-upload"
- action="{{ route('schedule.orders.documents.store', $order) }}"
- method="POST" enctype="multipart/form-data" class="visually-hidden">
- @csrf
- <input required type="file" id="production-order-documents-input"
- name="documents[]" multiple class="form-control form-control-sm"
- onchange="$(this).parent().submit()">
- </form>
- @endif
- @if($canManagePhotos)
- <form id="production-order-photos-upload"
- action="{{ route('schedule.orders.photos.store', $order) }}"
- method="POST" enctype="multipart/form-data" class="visually-hidden">
- @csrf
- <input required type="file" id="production-order-photos-input"
- name="photos[]" multiple accept=".jpg,.jpeg,.png,.webp"
- class="form-control form-control-sm"
- onchange="$(this).parent().submit()">
- </form>
- @endif
- <div class="card mt-3">
- <div class="card-body pt-1">
- @include('partials.chat', [
- 'title' => 'Чат площадки',
- 'messages' => $order->chatMessages,
- 'users' => $chatUsers,
- 'responsibleUserIds' => $chatResponsibleUserIds,
- 'managerUserId' => $order->manager_id,
- 'brigadierUserId' => null,
- 'action' => route('schedule.orders.chat-messages.store', $order),
- 'contextKey' => 'production-order-'.$order->id,
- 'submitLabel' => 'Отправить в чат',
- 'canCreateMessages' => $canCreateChatMessages,
- 'canDeleteMessages' => $canDeleteChatMessages,
- 'canSendNotifications' => false,
- ])
- </div>
- </div>
- @include('production_orders.partials.planning')
- @endif
- @if($canManageItems)
- <template id="production-order-item-template">
- @include('production_orders.partials.item-row', [
- 'index' => '__INDEX__',
- 'itemRow' => ['common_catalog_item_id' => '', 'order_item_number' => '', 'factory_number' => '', 'manufacture_date' => '', 'passport_file_id' => null],
- ])
- </template>
- @endif
- </div>
- @if($canManageItems)
- @push('scripts')
- <script>
- document.addEventListener('DOMContentLoaded', () => {
- const rows = document.getElementById('production-order-items');
- const template = document.getElementById('production-order-item-template');
- const search = document.getElementById('production-order-catalog-search');
- const results = document.getElementById('production-order-catalog-results');
- const quantity = document.getElementById('production-order-catalog-quantity');
- let nextIndex = rows.querySelectorAll('tr').length;
- let searchTimer;
- let requestNumber = 0;
- search.addEventListener('input', () => {
- clearTimeout(searchTimer);
- const query = search.value.trim();
- results.replaceChildren();
- const currentRequest = ++requestNumber;
- if (query.length < 2) return;
- searchTimer = setTimeout(async () => {
- try {
- const url = new URL(@json(route('schedule.orders.catalog-search')), window.location.origin);
- url.searchParams.set('q', query);
- const response = await fetch(url, {headers: {'Accept': 'application/json'}});
- if (!response.ok || currentRequest !== requestNumber) return;
- const items = await response.json();
- results.replaceChildren(...items.map((item) => {
- const option = document.createElement('option');
- option.value = item.id;
- option.textContent = item.label;
- option.dataset.image = item.image || '';
- option.dataset.imageFull = item.image_full || item.image || '';
- return option;
- }));
- } catch (error) {
- if (currentRequest === requestNumber) results.replaceChildren();
- }
- }, 250);
- });
- results.addEventListener('change', () => {
- const option = results.selectedOptions[0];
- if (!option) return;
- const count = Math.min(100, Math.max(1, Number.parseInt(quantity.value, 10) || 1));
- for (let i = 0; i < count; i++) {
- const fragment = template.content.cloneNode(true);
- const row = fragment.querySelector('tr');
- row.innerHTML = row.innerHTML.replaceAll('__INDEX__', nextIndex++);
- row.querySelector('.production-order-item-catalog-id').value = option.value;
- row.querySelector('.production-order-item-catalog-label').textContent = option.textContent;
- if (option.dataset.image) {
- const imageLink = row.querySelector('.production-order-item-image-link');
- const image = row.querySelector('.production-order-item-image');
- imageLink.href = option.dataset.imageFull;
- imageLink.classList.remove('d-none');
- image.src = option.dataset.image;
- image.alt = option.textContent;
- row.querySelector('.production-order-item-image-empty')?.classList.add('d-none');
- }
- rows.appendChild(fragment);
- }
- search.value = '';
- quantity.value = '1';
- results.replaceChildren();
- });
- rows.addEventListener('click', (event) => {
- const button = event.target.closest('.remove-production-order-item');
- if (!button) return;
- button.closest('tr').remove();
- });
- });
- </script>
- @endpush
- @endif
- @if($order)
- @push('scripts')
- <script>
- document.addEventListener('DOMContentLoaded', () => {
- const selectAll = document.getElementById('production-order-items-select-all');
- if (!selectAll) return;
- selectAll.addEventListener('change', () => {
- document.querySelectorAll('.production-order-item-select').forEach((checkbox) => {
- checkbox.checked = selectAll.checked;
- });
- });
- const reclamationForm = document.getElementById('production-order-reclamation');
- reclamationForm?.addEventListener('submit', (event) => {
- reclamationForm.querySelectorAll('input[name="item_ids[]"]').forEach((input) => input.remove());
- const selectedItems = document.querySelectorAll('.production-order-item-select:checked');
- if (!selectedItems.length) {
- event.preventDefault();
- alert('Выберите хотя бы один МАФ');
- return;
- }
- selectedItems.forEach((checkbox) => {
- const input = document.createElement('input');
- input.type = 'hidden';
- input.name = 'item_ids[]';
- input.value = checkbox.value;
- reclamationForm.appendChild(input);
- });
- });
- });
- </script>
- @endpush
- @endif
- @endsection
|