index.blade.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. @extends('layouts.app')
  2. @section('content')
  3. <div class="row mb-2">
  4. <div class="col-md-6">
  5. <h3>Ответственные</h3>
  6. </div>
  7. <div class="col-md-6 text-end">
  8. <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
  9. Добавить
  10. </button>
  11. </div>
  12. </div>
  13. @include('partials.table', [
  14. 'id' => $id,
  15. 'header' => $header,
  16. 'strings' => $responsibles,
  17. 'routeName' => 'responsible.show',
  18. 'routeParam' => 'responsible',
  19. 'nav' => $nav ?? null,
  20. ])
  21. @include('partials.pagination', ['items' => $responsibles])
  22. <!-- Модальное окно добавления-->
  23. <div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
  24. <div class="modal-dialog modal-fullscreen-sm-down modal-lg">
  25. <div class="modal-content">
  26. <div class="modal-header">
  27. <h1 class="modal-title fs-5" id="addModalLabel">Добавить ответственного</h1>
  28. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
  29. </div>
  30. <div class="modal-body">
  31. <form action="{{ route('responsible.store') }}" method="post">
  32. @csrf
  33. <input type="hidden" name="nav" value="{{ $nav ?? '' }}">
  34. <div class="row mb-2">
  35. <label for="area_search" class="col-form-label small col-md-4 text-md-end">
  36. Район
  37. <sup>*</sup>
  38. </label>
  39. <div class="col-md-8">
  40. <div class="position-relative">
  41. <input type="text"
  42. class="form-control form-control-sm"
  43. id="area_search"
  44. placeholder="Введите район..."
  45. autocomplete="off"
  46. required>
  47. <input type="hidden" name="area_id" id="area_id" value="{{ old('area_id', '') }}" required>
  48. <div class="autocomplete-dropdown autocomplete-dropdown--order" id="area_dropdown"></div>
  49. </div>
  50. <div class="form-text" id="area_hint"></div>
  51. </div>
  52. </div>
  53. @include('partials.input', ['name' => 'name', 'title' => 'ФИО', 'required' => true])
  54. @include('partials.input', ['name' => 'phone', 'title' => 'Телефон', 'required' => true])
  55. @include('partials.input', ['name' => 'post', 'title' => 'Должность'])
  56. @include('partials.submit', ['name' => 'Добавить', 'back_url' => route('responsible.index', session('gp_responsibles', []))])
  57. </form>
  58. </div>
  59. </div>
  60. </div>
  61. </div>
  62. @endsection
  63. @push('scripts')
  64. <script>
  65. waitForJQuery(function () {
  66. const $modal = $('#addModal');
  67. const $input = $modal.find('#area_search');
  68. const $dropdown = $modal.find('#area_dropdown');
  69. const $hiddenInput = $modal.find('#area_id');
  70. const $hint = $modal.find('#area_hint');
  71. const $form = $modal.find('form');
  72. let currentFocus = -1;
  73. const areas = @json($areasForSearch ?? []);
  74. function escapeHtml(text) {
  75. return String(text)
  76. .replace(/&/g, '&amp;')
  77. .replace(/</g, '&lt;')
  78. .replace(/>/g, '&gt;')
  79. .replace(/"/g, '&quot;')
  80. .replace(/'/g, '&#039;');
  81. }
  82. function escapeRegex(str) {
  83. return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  84. }
  85. function highlightMatch(text, query) {
  86. if (!text || !query) {
  87. return escapeHtml(text || '');
  88. }
  89. const safeText = escapeHtml(text);
  90. const regex = new RegExp('(' + escapeRegex(query) + ')', 'gi');
  91. return safeText.replace(regex, '<strong>$1</strong>');
  92. }
  93. function showDropdown(items, query) {
  94. $dropdown.empty();
  95. currentFocus = -1;
  96. if (!items.length) {
  97. $dropdown.html('<div class="autocomplete-item text-muted">Ничего не найдено</div>');
  98. $dropdown.show();
  99. return;
  100. }
  101. items.forEach(function (item) {
  102. const html = '<div class="autocomplete-item" data-id="' + item.id + '" data-name="' + escapeHtml(item.name) + '">' +
  103. '<div class="article">' + highlightMatch(item.name, query) + '</div>' +
  104. '</div>';
  105. $dropdown.append(html);
  106. });
  107. $dropdown.show();
  108. }
  109. function hideDropdown() {
  110. $dropdown.hide();
  111. }
  112. function selectItem($item) {
  113. const id = String($item.data('id') || '');
  114. const name = String($item.data('name') || '');
  115. $input.val(name);
  116. $hiddenInput.val(id);
  117. $hint.html('<i class="bi bi-check-circle text-success"></i> Выбрано: ' + name);
  118. $input.removeClass('is-invalid');
  119. hideDropdown();
  120. }
  121. function setActive($items) {
  122. $items.removeClass('active');
  123. if (currentFocus >= 0 && currentFocus < $items.length) {
  124. const $active = $items.eq(currentFocus);
  125. $active.addClass('active');
  126. $active[0].scrollIntoView({block: 'nearest'});
  127. }
  128. }
  129. function searchAreas(query) {
  130. const normalized = String(query || '').trim().toLowerCase();
  131. if (normalized.length < 1) {
  132. hideDropdown();
  133. return;
  134. }
  135. const items = areas.filter(function (item) {
  136. return String(item.name).toLowerCase().includes(normalized);
  137. });
  138. showDropdown(items.slice(0, 50), normalized);
  139. }
  140. $input.on('input', function () {
  141. $hiddenInput.val('');
  142. $hint.html('');
  143. $input.removeClass('is-invalid');
  144. searchAreas($(this).val());
  145. });
  146. $input.on('focus', function () {
  147. if (!$hiddenInput.val()) {
  148. searchAreas($(this).val());
  149. }
  150. });
  151. $input.on('keydown', function (e) {
  152. const $items = $dropdown.find('.autocomplete-item:not(.text-muted)');
  153. if (e.key === 'ArrowDown') {
  154. e.preventDefault();
  155. currentFocus++;
  156. if (currentFocus >= $items.length) {
  157. currentFocus = 0;
  158. }
  159. setActive($items);
  160. } else if (e.key === 'ArrowUp') {
  161. e.preventDefault();
  162. currentFocus--;
  163. if (currentFocus < 0) {
  164. currentFocus = $items.length - 1;
  165. }
  166. setActive($items);
  167. } else if (e.key === 'Enter') {
  168. if ($items.length > 0 && currentFocus > -1) {
  169. e.preventDefault();
  170. selectItem($items.eq(currentFocus));
  171. }
  172. } else if (e.key === 'Escape') {
  173. hideDropdown();
  174. }
  175. });
  176. $dropdown.on('click', '.autocomplete-item:not(.text-muted)', function () {
  177. selectItem($(this));
  178. });
  179. $(document).on('click', function (e) {
  180. if (!$(e.target).closest('#area_search, #area_dropdown').length) {
  181. hideDropdown();
  182. }
  183. });
  184. $form.on('submit', function (e) {
  185. if (!$hiddenInput.val()) {
  186. e.preventDefault();
  187. $input.addClass('is-invalid').focus();
  188. $hint.html('<span class="text-danger">Выберите район из списка</span>');
  189. }
  190. });
  191. @if(old('area_id'))
  192. const oldAreaId = '{{ old('area_id') }}';
  193. const oldArea = areas.find(function (item) {
  194. return String(item.id) === String(oldAreaId);
  195. });
  196. if (oldArea) {
  197. $input.val(oldArea.name);
  198. $hint.html('<i class="bi bi-check-circle text-success"></i> Выбрано: ' + oldArea.name);
  199. }
  200. @endif
  201. $modal.on('hidden.bs.modal', function () {
  202. hideDropdown();
  203. });
  204. });
  205. </script>
  206. @endpush