SparePartController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Requests\StoreSparePartRequest;
  4. use App\Jobs\Export\ExportSparePartsJob;
  5. use App\Jobs\Import\ImportSparePartsJob;
  6. use App\Models\Import;
  7. use App\Models\SparePart;
  8. use Illuminate\Http\RedirectResponse;
  9. use Illuminate\Http\Request;
  10. use Illuminate\Support\Facades\Log;
  11. use Illuminate\Support\Facades\Storage;
  12. class SparePartController extends Controller
  13. {
  14. protected array $data = [
  15. 'active' => 'spare_parts',
  16. 'title' => 'Каталог запчастей',
  17. 'id' => 'spare_parts',
  18. 'header' => [
  19. 'image' => 'Картинка',
  20. 'id' => 'ID',
  21. 'article' => 'Артикул',
  22. 'used_in_maf' => 'Где используется',
  23. 'quantity_without_docs' => 'Кол-во без док',
  24. 'quantity_with_docs' => 'Кол-во с док',
  25. 'total_quantity' => 'Кол-во общее',
  26. 'note' => 'Примечание',
  27. 'customer_price_txt' => 'Цена для заказчика',
  28. 'expertise_price_txt' => 'Цена экспертизы',
  29. 'tsn_number' => '№ по ТСН',
  30. 'pricing_codes_list' => 'Шифр расценки и коды ресурсов',
  31. 'min_stock' => 'Минимальный остаток',
  32. ],
  33. 'searchFields' => [
  34. 'article',
  35. 'used_in_maf',
  36. 'note',
  37. 'tsn_number',
  38. ],
  39. 'routeName' => 'spare_parts.show',
  40. ];
  41. public function index(Request $request)
  42. {
  43. session(['gp_spare_parts' => $request->query()]);
  44. $model = new SparePart();
  45. // Для админа добавляем колонку цены закупки
  46. if (hasRole('admin')) {
  47. $this->data['header'] = array_merge(
  48. array_slice($this->data['header'], 0, 8, true),
  49. ['purchase_price_txt' => 'Цена закупки'],
  50. array_slice($this->data['header'], 8, null, true)
  51. );
  52. }
  53. // Фильтры
  54. $this->createFilters($model, 'used_in_maf');
  55. // Для range фильтров нужно использовать реальные поля БД (без _txt)
  56. // но заголовки брать из header с _txt
  57. $this->createRangeFiltersForPrices($model, 'customer_price', 'expertise_price', 'min_stock');
  58. if (hasRole('admin')) {
  59. $this->createRangeFiltersForPrices($model, 'purchase_price');
  60. }
  61. // Запрос
  62. $q = $model::query();
  63. $this->acceptFilters($q, $request);
  64. $this->acceptSearch($q, $request);
  65. $this->setSortAndOrderBy($model, $request);
  66. $q->orderBy($this->data['sortBy'], $this->data['orderBy']);
  67. $this->data['spare_parts'] = $q->paginate(session('per_page', config('pagination.per_page')))->withQueryString();
  68. $this->data['strings'] = $this->data['spare_parts'];
  69. $this->data['tab'] = 'catalog';
  70. return view('spare_parts.index', $this->data);
  71. }
  72. public function show(Request $request, SparePart $sparePart)
  73. {
  74. $this->data['previous_url'] = $request->get('previous_url');
  75. $this->data['spare_part'] = $sparePart;
  76. return view('spare_parts.edit', $this->data);
  77. }
  78. public function create()
  79. {
  80. $this->data['spare_part'] = null;
  81. return view('spare_parts.edit', $this->data);
  82. }
  83. public function store(StoreSparePartRequest $request): RedirectResponse
  84. {
  85. $sparePart = SparePart::create($request->validated());
  86. $this->syncPricingCodes($sparePart, $request);
  87. $previous_url = $request->get('previous_url') ?? route('spare_parts.index', session('gp_spare_parts'));
  88. return redirect()->to($previous_url)->with(['success' => 'Запчасть успешно создана!']);
  89. }
  90. public function update(StoreSparePartRequest $request, SparePart $sparePart): RedirectResponse
  91. {
  92. $sparePart->update($request->validated());
  93. $this->syncPricingCodes($sparePart, $request);
  94. $previous_url = $request->get('previous_url') ?? route('spare_parts.index', session('gp_spare_parts'));
  95. return redirect()->to($previous_url)->with(['success' => 'Запчасть успешно обновлена!']);
  96. }
  97. protected function syncPricingCodes(SparePart $sparePart, StoreSparePartRequest $request): void
  98. {
  99. $codes = $request->input('pricing_codes', []);
  100. $descriptions = $request->input('pricing_codes_descriptions', []);
  101. $pricingCodeIds = [];
  102. foreach ($codes as $index => $code) {
  103. $code = trim($code);
  104. if (empty($code)) {
  105. continue;
  106. }
  107. $description = trim($descriptions[$index] ?? '');
  108. // Находим или создаём PricingCode
  109. $pricingCode = \App\Models\PricingCode::where('type', \App\Models\PricingCode::TYPE_PRICING_CODE)
  110. ->where('code', $code)
  111. ->first();
  112. if ($pricingCode) {
  113. // Обновляем расшифровку, если предоставлена
  114. if ($description && $description !== $pricingCode->description) {
  115. $pricingCode->update(['description' => $description]);
  116. }
  117. } else {
  118. // Создаём новый код
  119. $pricingCode = \App\Models\PricingCode::create([
  120. 'type' => \App\Models\PricingCode::TYPE_PRICING_CODE,
  121. 'code' => $code,
  122. 'description' => $description ?: null,
  123. ]);
  124. }
  125. $pricingCodeIds[] = $pricingCode->id;
  126. }
  127. // Синхронизируем связи
  128. $sparePart->pricingCodes()->sync($pricingCodeIds);
  129. }
  130. public function destroy(SparePart $sparePart): RedirectResponse
  131. {
  132. // Проверка на наличие заказов
  133. if ($sparePart->orders()->count() > 0) {
  134. return redirect()->route('spare_parts.index', session('gp_spare_parts'))
  135. ->with(['error' => 'Невозможно удалить запчасть, т.к. для неё есть заказы!']);
  136. }
  137. $sparePart->delete();
  138. return redirect()->route('spare_parts.index', session('gp_spare_parts'))
  139. ->with(['success' => 'Запчасть успешно удалена!']);
  140. }
  141. public function export(Request $request): RedirectResponse
  142. {
  143. // Запускаем Job для экспорта
  144. ExportSparePartsJob::dispatch(auth()->id());
  145. Log::info('ExportSparePartsJob created!');
  146. return redirect()->route('spare_parts.index', session('gp_spare_parts'))
  147. ->with(['success' => 'Задача экспорта успешно создана!']);
  148. }
  149. public function import(Request $request): RedirectResponse
  150. {
  151. $request->validate([
  152. 'file' => 'required|mimes:xlsx,xls|max:10240',
  153. ]);
  154. try {
  155. // Сохраняем временный файл
  156. $file = $request->file('file');
  157. $tempPath = $file->storeAs('temp/imports', 'spare_parts_import_' . time() . '.xlsx');
  158. $fullPath = Storage::path($tempPath);
  159. // Создаём запись импорта
  160. $import = Import::create([
  161. 'user_id' => auth()->id(),
  162. 'type' => 'spare_parts',
  163. 'status' => Import::STATUS_PENDING,
  164. 'file_path' => $tempPath,
  165. 'original_filename' => $file->getClientOriginalName(),
  166. ]);
  167. // Запускаем Job для импорта
  168. ImportSparePartsJob::dispatch($fullPath, auth()->id(), $import->id);
  169. Log::info('ImportSparePartsJob created!', ['import_id' => $import->id]);
  170. return redirect()->route('spare_parts.index', session('gp_spare_parts'))
  171. ->with(['success' => 'Задача импорта успешно создана! Следите за статусом в разделе импорта.']);
  172. } catch (\Exception $e) {
  173. Log::error('Ошибка создания задачи импорта: ' . $e->getMessage());
  174. return redirect()->route('spare_parts.index', session('gp_spare_parts'))
  175. ->with(['error' => 'Ошибка импорта: ' . $e->getMessage()]);
  176. }
  177. }
  178. public function uploadImage(Request $request, SparePart $sparePart): RedirectResponse
  179. {
  180. $request->validate([
  181. 'image' => 'required|image|mimes:jpeg,jpg,png|max:2048',
  182. ]);
  183. if ($request->hasFile('image')) {
  184. $image = $request->file('image');
  185. $filename = $sparePart->article . '.jpg';
  186. // Создаём директорию если её нет
  187. $directory = public_path('images/spare_parts');
  188. if (!file_exists($directory)) {
  189. mkdir($directory, 0755, true);
  190. }
  191. // Сохраняем изображение
  192. $image->move($directory, $filename);
  193. return redirect()->route('spare_parts.show', $sparePart)
  194. ->with(['success' => 'Изображение успешно загружено!']);
  195. }
  196. return redirect()->route('spare_parts.show', $sparePart)
  197. ->with(['error' => 'Ошибка загрузки изображения!']);
  198. }
  199. /**
  200. * API метод для поиска запчастей (autocomplete)
  201. */
  202. public function search(Request $request)
  203. {
  204. $query = $request->get('query', '');
  205. $spareParts = SparePart::query()
  206. ->where(function ($q) use ($query) {
  207. $q->where('article', 'LIKE', '%' . $query . '%')
  208. ->orWhere('used_in_maf', 'LIKE', '%' . $query . '%');
  209. })
  210. ->orderBy('article')
  211. ->limit(20)
  212. ->get(['id', 'article', 'used_in_maf']);
  213. return response()->json($spareParts);
  214. }
  215. /**
  216. * Создание range фильтров для полей с ценами
  217. * Использует правильные заголовки из header (_txt версии)
  218. */
  219. protected function createRangeFiltersForPrices(SparePart $model, string ...$columnNames): void
  220. {
  221. foreach ($columnNames as $columnName) {
  222. // Определяем ключ заголовка
  223. $headerKey = str_ends_with($columnName, '_price') ? $columnName . '_txt' : $columnName;
  224. // Проверяем, есть ли заголовок
  225. if (!isset($this->data['header'][$headerKey])) {
  226. continue;
  227. }
  228. if (str_ends_with($columnName, '_price')) {
  229. $min = $model::query()->min($columnName);
  230. $max = $model::query()->max($columnName);
  231. $this->data['ranges'][$columnName] = [
  232. 'title' => $this->data['header'][$headerKey],
  233. 'min' => $min ? $min / 100 : 0,
  234. 'max' => $max ? $max / 100 : 0,
  235. ];
  236. } else {
  237. $this->data['ranges'][$columnName] = [
  238. 'title' => $this->data['header'][$headerKey],
  239. 'min' => $model::query()->min($columnName) ?? 0,
  240. 'max' => $model::query()->max($columnName) ?? 0,
  241. ];
  242. }
  243. }
  244. }
  245. }