CommonCatalogController.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Http\Controllers;
  4. use App\Http\Requests\StoreCommonCatalogItemRequest;
  5. use App\Jobs\Export\ExportCommonCatalogJob;
  6. use App\Jobs\Import\ImportJob;
  7. use App\Models\CommonCatalogItem;
  8. use App\Models\File;
  9. use App\Models\Import;
  10. use App\Services\Access\FieldAccessService;
  11. use App\Services\FileService;
  12. use Illuminate\Contracts\View\View;
  13. use Illuminate\Http\RedirectResponse;
  14. use Illuminate\Http\Request;
  15. use Illuminate\Support\Facades\Storage;
  16. use Illuminate\Support\Str;
  17. use Throwable;
  18. class CommonCatalogController extends Controller
  19. {
  20. protected array $data = [
  21. 'active' => 'common_catalog',
  22. 'title' => 'Каталог общий',
  23. 'id' => 'common_catalog_items',
  24. 'header' => [
  25. 'id' => 'ID',
  26. 'image' => 'Внешний вид',
  27. 'article' => 'Артикул',
  28. 'calculator_name' => 'Наименование',
  29. 'kind' => 'Вид',
  30. 'dimension_length' => 'Габариты: длина',
  31. 'dimension_width' => 'Габариты: ширина',
  32. 'dimension_height' => 'Габариты: высота',
  33. 'site_length' => 'Участок: длина',
  34. 'site_width' => 'Участок: ширина',
  35. 'fall_height' => 'Высота падения',
  36. 'additional_info' => 'Дополнительные сведения',
  37. 'dimension_unit' => 'Единица измерения габаритов',
  38. 'weight' => 'Вес, кг.',
  39. 'volume' => 'Объем, м3',
  40. 'places' => 'Места',
  41. 'composition' => 'Состав',
  42. 'age_group' => 'Возрастная группа',
  43. 'max_users' => 'Макс. кол-во пользователей',
  44. 'unit' => 'Ед.',
  45. 'series' => 'Серия',
  46. 'trademark' => 'ТМ',
  47. 'calculator_enabled_txt' => 'Калькулятор',
  48. 'builders_price_txt' => 'строители',
  49. 'wholesale_price_txt' => 'опт',
  50. 'recommended_price_txt' => 'рек',
  51. 'retail_price_txt' => 'розница',
  52. 'project_price_txt' => 'проект',
  53. 'project_with_installation_price_txt' => 'проект+м',
  54. 'pik_price_txt' => 'пик',
  55. 'recommended_plus_10_price_txt' => 'рек+10',
  56. ],
  57. 'searchFields' => [
  58. 'article',
  59. 'calculator_name',
  60. 'kind',
  61. 'dimension_length',
  62. 'dimension_width',
  63. 'dimension_height',
  64. 'site_length',
  65. 'site_width',
  66. 'additional_info',
  67. 'composition',
  68. 'series',
  69. 'trademark',
  70. ],
  71. ];
  72. public function index(Request $request, FieldAccessService $fieldAccess): View
  73. {
  74. session(['gp_common_catalog' => $request->query()]);
  75. $nav = $this->startNavigationContext($request);
  76. $model = new CommonCatalogItem;
  77. $readableFields = $this->readableFields($request, $fieldAccess);
  78. $request = $this->sanitizeListRequest($request, $readableFields);
  79. $this->data['header'] = $fieldAccess->visibleHeaders(
  80. $request->user(),
  81. 'common-catalog',
  82. $this->data['header'],
  83. );
  84. $this->data['searchFields'] = array_values(array_intersect(
  85. $this->data['searchFields'],
  86. $readableFields,
  87. ));
  88. $this->createFilters(
  89. $model,
  90. ...array_values(array_intersect([
  91. 'kind',
  92. 'dimension_unit',
  93. 'age_group',
  94. 'unit',
  95. 'series',
  96. 'trademark',
  97. ], $readableFields)),
  98. );
  99. $this->createRangeFilters(
  100. $model,
  101. ...array_values(array_intersect([
  102. 'fall_height',
  103. 'weight',
  104. 'volume',
  105. 'places',
  106. 'max_users',
  107. 'builders_price',
  108. 'wholesale_price',
  109. 'recommended_price',
  110. 'retail_price',
  111. 'project_price',
  112. 'project_with_installation_price',
  113. 'pik_price',
  114. 'recommended_plus_10_price',
  115. ], $readableFields)),
  116. );
  117. $query = CommonCatalogItem::query()->with('imageFile');
  118. $this->acceptFilters($query, $request);
  119. $this->acceptSearch($query, $request);
  120. $this->setSortAndOrderBy($model, $request);
  121. $this->applyStableSorting($query);
  122. $this->data['items'] = $query->paginate($this->data['per_page'])->withQueryString();
  123. $this->data['nav'] = $nav;
  124. return view('common_catalog.index', $this->data);
  125. }
  126. public function create(Request $request, FieldAccessService $fieldAccess): View
  127. {
  128. return $this->itemView($request, $fieldAccess);
  129. }
  130. public function show(
  131. Request $request,
  132. CommonCatalogItem $commonCatalogItem,
  133. FieldAccessService $fieldAccess,
  134. ): View
  135. {
  136. return $this->itemView(
  137. $request,
  138. $fieldAccess,
  139. $commonCatalogItem->load(['imageFile', 'documents']),
  140. );
  141. }
  142. public function store(
  143. StoreCommonCatalogItemRequest $request,
  144. FieldAccessService $fieldAccess,
  145. ): RedirectResponse
  146. {
  147. $item = CommonCatalogItem::query()->create(
  148. $fieldAccess->filterValidatedPayload(
  149. $request->user(),
  150. 'common-catalog',
  151. $request->validated(),
  152. ),
  153. );
  154. return redirect()
  155. ->route('common-catalog.show', $this->withNav(
  156. ['commonCatalogItem' => $item],
  157. $this->resolveNavToken($request),
  158. ))
  159. ->with('success', 'Позиция общего каталога создана.');
  160. }
  161. public function update(
  162. StoreCommonCatalogItemRequest $request,
  163. CommonCatalogItem $commonCatalogItem,
  164. FieldAccessService $fieldAccess,
  165. ): RedirectResponse {
  166. $commonCatalogItem->update($fieldAccess->filterValidatedPayload(
  167. $request->user(),
  168. 'common-catalog',
  169. $request->validated(),
  170. ));
  171. return $this->redirectToItem($request, $commonCatalogItem)
  172. ->with('success', 'Позиция общего каталога обновлена.');
  173. }
  174. public function destroy(
  175. Request $request,
  176. CommonCatalogItem $commonCatalogItem,
  177. FileService $fileService,
  178. ): RedirectResponse {
  179. $commonCatalogItem->load(['imageFile', 'documents']);
  180. $files = $commonCatalogItem->documents;
  181. if ($commonCatalogItem->imageFile) {
  182. $files->push($commonCatalogItem->imageFile);
  183. }
  184. $commonCatalogItem->documents()->detach();
  185. $commonCatalogItem->update(['image_file_id' => null]);
  186. $commonCatalogItem->delete();
  187. foreach ($files->unique('id') as $file) {
  188. $fileService->deleteFileWithThumbnail($file);
  189. $file->delete();
  190. }
  191. return redirect()
  192. ->route('common-catalog.index', session('gp_common_catalog'))
  193. ->with('success', 'Позиция общего каталога удалена.');
  194. }
  195. public function uploadImage(
  196. Request $request,
  197. CommonCatalogItem $commonCatalogItem,
  198. FileService $fileService,
  199. ): RedirectResponse {
  200. $data = $request->validate([
  201. 'image' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:10240'],
  202. ]);
  203. try {
  204. $oldImage = $commonCatalogItem->imageFile;
  205. $image = $fileService->saveUploadedFile(
  206. "common_catalog/items/{$commonCatalogItem->id}/image",
  207. $data['image'],
  208. );
  209. $commonCatalogItem->update(['image_file_id' => $image->id]);
  210. if ($oldImage) {
  211. $fileService->deleteFileWithThumbnail($oldImage);
  212. $oldImage->delete();
  213. }
  214. } catch (Throwable $exception) {
  215. report($exception);
  216. return $this->redirectToItem($request, $commonCatalogItem)
  217. ->with('error', 'Не удалось загрузить изображение.');
  218. }
  219. return $this->redirectToItem($request, $commonCatalogItem)
  220. ->with('success', 'Изображение загружено.');
  221. }
  222. public function deleteImage(
  223. Request $request,
  224. CommonCatalogItem $commonCatalogItem,
  225. FileService $fileService,
  226. ): RedirectResponse {
  227. $image = $commonCatalogItem->imageFile;
  228. if ($image) {
  229. $commonCatalogItem->update(['image_file_id' => null]);
  230. $fileService->deleteFileWithThumbnail($image);
  231. $image->delete();
  232. }
  233. return $this->redirectToItem($request, $commonCatalogItem)
  234. ->with('success', 'Изображение удалено.');
  235. }
  236. public function uploadDocument(
  237. Request $request,
  238. CommonCatalogItem $commonCatalogItem,
  239. FileService $fileService,
  240. ): RedirectResponse {
  241. $data = $request->validate([
  242. 'document' => ['required', 'file', 'max:20480'],
  243. ]);
  244. try {
  245. $document = $fileService->saveUploadedFile(
  246. "common_catalog/items/{$commonCatalogItem->id}/documents",
  247. $data['document'],
  248. );
  249. $commonCatalogItem->documents()->attach($document);
  250. } catch (Throwable $exception) {
  251. report($exception);
  252. return $this->redirectToItem($request, $commonCatalogItem)
  253. ->with('error', 'Не удалось загрузить документ.');
  254. }
  255. return $this->redirectToItem($request, $commonCatalogItem)
  256. ->with('success', 'Документ загружен.');
  257. }
  258. public function deleteDocument(
  259. Request $request,
  260. CommonCatalogItem $commonCatalogItem,
  261. File $file,
  262. FileService $fileService,
  263. ): RedirectResponse {
  264. abort_unless($commonCatalogItem->documents()->whereKey($file->getKey())->exists(), 404);
  265. $commonCatalogItem->documents()->detach($file);
  266. $fileService->deleteFileWithThumbnail($file);
  267. $file->delete();
  268. return $this->redirectToItem($request, $commonCatalogItem)
  269. ->with('success', 'Документ удалён.');
  270. }
  271. public function export(Request $request): RedirectResponse
  272. {
  273. ExportCommonCatalogJob::dispatch((int) $request->user()->getKey());
  274. return redirect()
  275. ->route('common-catalog.index', session('gp_common_catalog'))
  276. ->with('success', 'Задача экспорта общего каталога создана.');
  277. }
  278. public function import(Request $request): RedirectResponse
  279. {
  280. $data = $request->validate([
  281. 'import_file' => ['required', 'file', 'mimes:xlsx', 'max:20480'],
  282. ]);
  283. $path = Str::random(2).'/'.Str::uuid().'.xlsx';
  284. Storage::disk('upload')->put($path, $data['import_file']->getContent());
  285. $import = Import::query()->create([
  286. 'type' => 'common_catalog',
  287. 'year' => null,
  288. 'filename' => $path,
  289. 'status' => 'new',
  290. 'user_id' => $request->user()->getKey(),
  291. 'original_filename' => $data['import_file']->getClientOriginalName(),
  292. ]);
  293. ImportJob::dispatch($import, (int) $request->user()->getKey());
  294. return redirect()
  295. ->route('common-catalog.index', session('gp_common_catalog'))
  296. ->with('success', 'Задача импорта общего каталога создана.');
  297. }
  298. private function itemView(
  299. Request $request,
  300. FieldAccessService $fieldAccess,
  301. ?CommonCatalogItem $item = null,
  302. ): View
  303. {
  304. $nav = $this->resolveNavToken($request);
  305. $this->rememberNavigation($request, $nav);
  306. $this->data['nav'] = $nav;
  307. $this->data['back_url'] = $this->navigationBackUrl(
  308. $request,
  309. $nav,
  310. route('common-catalog.index', session('gp_common_catalog')),
  311. );
  312. $this->data['item'] = $item;
  313. $fields = array_keys(config('access.common-catalog.fields', []));
  314. $this->data['commonCatalogReadableFields'] = array_fill_keys(
  315. $fieldAccess->filterReadableFields($request->user(), 'common-catalog', $fields),
  316. true,
  317. );
  318. $this->data['commonCatalogWritableFields'] = [];
  319. foreach ($fields as $field) {
  320. $this->data['commonCatalogWritableFields'][$field] = $request->user()
  321. ->canUpdateField('common-catalog', $field);
  322. }
  323. return view('common_catalog.edit', $this->data);
  324. }
  325. private function readableFields(Request $request, FieldAccessService $fieldAccess): array
  326. {
  327. return $fieldAccess->filterReadableFields(
  328. $request->user(),
  329. 'common-catalog',
  330. array_keys(config('access.common-catalog.fields', [])),
  331. );
  332. }
  333. private function sanitizeListRequest(Request $request, array $readableFields): Request
  334. {
  335. if (
  336. $request->filled('sortBy')
  337. && ! in_array($this->normalizeField($request->string('sortBy')->toString()), $readableFields, true)
  338. ) {
  339. $request->merge(['sortBy' => CommonCatalogItem::DEFAULT_SORT_BY]);
  340. }
  341. if ($request->has('filters') && is_array($request->input('filters'))) {
  342. $request->merge([
  343. 'filters' => array_filter(
  344. $request->input('filters'),
  345. fn (mixed $value, string $field): bool => in_array(
  346. $this->normalizeField($field),
  347. $readableFields,
  348. true,
  349. ),
  350. ARRAY_FILTER_USE_BOTH,
  351. ),
  352. ]);
  353. }
  354. return $request;
  355. }
  356. private function normalizeField(string $field): string
  357. {
  358. $field = preg_replace('/_(from|to)$/', '', $field) ?: $field;
  359. return str_ends_with($field, '_txt') ? substr($field, 0, -4) : $field;
  360. }
  361. private function redirectToItem(
  362. Request $request,
  363. CommonCatalogItem $commonCatalogItem,
  364. ): RedirectResponse {
  365. return redirect()->route('common-catalog.show', $this->withNav(
  366. ['commonCatalogItem' => $commonCatalogItem],
  367. $this->resolveNavToken($request),
  368. ));
  369. }
  370. }