FilterController.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Requests\FilterRequest;
  4. use App\Models\CommonCatalogItem;
  5. use App\Models\SparePartsView;
  6. use App\Models\StockOrder;
  7. use Illuminate\Http\JsonResponse;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Schema;
  10. class FilterController extends Controller
  11. {
  12. const DB_TABLES = [
  13. 'orders' => 'orders_view',
  14. 'product_sku' => 'mafs_view',
  15. 'products' => 'products',
  16. 'reclamations' => 'reclamations_view',
  17. 'maf_order' => 'maf_orders_view',
  18. 'import' => 'imports',
  19. 'responsibles' => 'responsibles_view',
  20. 'users' => 'users',
  21. 'contracts' => 'contracts',
  22. 'spare_parts' => 'spare_parts_view',
  23. 'spare_part_orders' => 'spare_part_orders_view',
  24. 'notifications' => 'user_notifications',
  25. 'notification_logs' => 'notification_delivery_logs',
  26. 'common_catalog_items' => 'common_catalog_items',
  27. 'stock_availability' => 'common_catalog_items',
  28. 'stock_orders' => 'stock_orders',
  29. 'schedule_orders' => 'production_orders',
  30. ];
  31. const SKIP_YEAR_FILTER = [
  32. 'reclamations',
  33. 'notifications',
  34. 'notification_logs',
  35. ];
  36. /**
  37. * Маппинг виртуальных столбцов (аксессоров Eloquent) на реальные столбцы БД.
  38. * Ключ — имя таблицы из DB_TABLES, значение — массив 'виртуальный_столбец' => 'реальный_столбец'.
  39. */
  40. const COLUMN_MAP = [
  41. 'spare_part_orders' => [
  42. 'status_name' => 'status',
  43. 'with_documents_text' => 'with_documents',
  44. ],
  45. 'spare_parts' => [
  46. 'customer_price_txt' => 'customer_price',
  47. 'expertise_price_txt' => 'expertise_price',
  48. 'purchase_price_txt' => 'purchase_price',
  49. ],
  50. 'products' => [
  51. 'product_price_txt' => 'product_price',
  52. 'installation_price_txt' => 'installation_price',
  53. 'total_price_txt' => 'total_price',
  54. ],
  55. 'responsibles' => [
  56. 'area-name' => 'area_name',
  57. ],
  58. 'common_catalog_items' => [
  59. 'calculator_enabled_txt' => 'calculator_enabled',
  60. 'builders_price_txt' => 'builders_price',
  61. 'wholesale_price_txt' => 'wholesale_price',
  62. 'recommended_price_txt' => 'recommended_price',
  63. 'retail_price_txt' => 'retail_price',
  64. 'project_price_txt' => 'project_price',
  65. 'project_with_installation_price_txt' => 'project_with_installation_price',
  66. 'pik_price_txt' => 'pik_price',
  67. 'recommended_plus_10_price_txt' => 'recommended_plus_10_price',
  68. ],
  69. 'stock_orders' => [
  70. 'status_name' => 'status',
  71. ],
  72. 'schedule_orders' => [
  73. 'status_name' => 'status',
  74. 'execution_type_name' => 'execution_type',
  75. 'delivery_dates' => 'delivery_date',
  76. 'installation_dates' => 'installation_date',
  77. ],
  78. ];
  79. /**
  80. * Маппинг значений: реальное значение БД => отображаемое значение.
  81. * Ключ — имя таблицы, затем реальный столбец.
  82. */
  83. const VALUE_MAP = [
  84. 'users' => [
  85. 'role' => \App\Models\Role::NAMES,
  86. ],
  87. 'notifications' => [
  88. 'type' => [
  89. 'platform' => 'Площадки',
  90. 'reclamation' => 'Рекламации',
  91. 'schedule' => 'График монтажей',
  92. 'production_order' => 'График заказов',
  93. ],
  94. 'event' => [
  95. 'created' => 'Создание',
  96. 'status_changed' => 'Смена статуса',
  97. 'schedule_added' => 'Добавлено в график',
  98. 'delivery_added' => 'Добавлена доставка',
  99. 'installation_added' => 'Добавлен монтаж',
  100. 'reclamation_added' => 'Добавлена рекламация',
  101. ],
  102. ],
  103. 'spare_part_orders' => [
  104. 'with_documents' => [
  105. 0 => 'Нет',
  106. 1 => 'Да',
  107. ],
  108. 'status' => [
  109. 'ordered' => 'Заказано',
  110. 'in_stock' => 'На складе',
  111. 'shipped' => 'Отгружено',
  112. ],
  113. ],
  114. 'notification_logs' => [
  115. 'channel' => [
  116. 'in_app' => 'Браузер',
  117. 'browser' => 'Браузер',
  118. 'push' => 'Android/iOS',
  119. 'email' => 'Email',
  120. ],
  121. 'status' => [
  122. 'sent' => 'Отправлено',
  123. 'failed' => 'Ошибка',
  124. 'skipped' => 'Пропущено',
  125. 'dead_letter' => 'Dead letter',
  126. ],
  127. ],
  128. 'common_catalog_items' => [
  129. 'calculator_enabled' => [
  130. 0 => 'нет',
  131. 1 => 'да',
  132. ],
  133. ],
  134. 'stock_orders' => [
  135. 'status' => StockOrder::STATUS_NAMES,
  136. ],
  137. 'schedule_orders' => [
  138. 'status' => \App\Enums\ProductionOrderStatus::LABELS,
  139. 'execution_type' => \App\Enums\ProductionOrderExecutionType::LABELS,
  140. ],
  141. ];
  142. private const FIELD_ACCESS_MODULES = [
  143. 'common_catalog_items' => 'common-catalog',
  144. 'schedule_orders' => 'schedule-orders',
  145. ];
  146. private const SESSION_KEYS = [
  147. 'common_catalog_items' => 'gp_common_catalog',
  148. ];
  149. public function getFilters(FilterRequest $request): JsonResponse
  150. {
  151. $table = $request->validated('table');
  152. $column = $request->validated('column');
  153. if (! array_key_exists($table, self::DB_TABLES)) {
  154. abort(400, 'Table not found');
  155. }
  156. $this->assertCanViewColumn($request, $table, $column);
  157. if ($table === 'stock_availability') {
  158. return $this->stockAvailabilityFilters($column);
  159. }
  160. if ($table === 'stock_orders') {
  161. return $this->stockOrderFilters($column);
  162. }
  163. if ($table === 'schedule_orders' && in_array($column, ['delivery_dates', 'installation_dates'], true)) {
  164. return $this->productionOrderPlanningFilters($column);
  165. }
  166. $gp = session(self::SESSION_KEYS[$table] ?? 'gp_'.$table);
  167. if ($table === 'spare_parts' && $column === 'pricing_codes_list') {
  168. $result = DB::table('pricing_codes as pc')
  169. ->join('spare_part_pricing_code as sppc', 'sppc.pricing_code_id', '=', 'pc.id')
  170. ->select('pc.code')
  171. ->distinct()
  172. ->orderBy('pc.code')
  173. ->pluck('pc.code')
  174. ->toArray();
  175. $hasEmptyValue = SparePartsView::query()
  176. ->doesntHave('pricingCodes')
  177. ->exists();
  178. if ($hasEmptyValue) {
  179. array_unshift($result, '-пусто-');
  180. }
  181. return response()->json($result, 200, [], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
  182. }
  183. $dbTable = self::DB_TABLES[$table];
  184. // Определяем реальный столбец БД
  185. $dbColumn = self::resolveDbColumn($table, $dbTable, $column);
  186. if ($dbColumn && Schema::hasColumn($dbTable, $dbColumn)) {
  187. $normalizedColumn = self::normalizedSelectExpression($dbColumn);
  188. $q = DB::table($dbTable)->selectRaw($normalizedColumn.' as filter_value')->distinct();
  189. if (! in_array($table, self::SKIP_YEAR_FILTER) && Schema::hasColumn($dbTable, 'year')) {
  190. $q->where('year', year());
  191. }
  192. if (Schema::hasColumn($dbTable, 'deleted_at')) {
  193. $q->whereNull('deleted_at');
  194. }
  195. if (isset($gp['filters']) && is_array($gp['filters']) && count($gp['filters'])) {
  196. foreach ($gp['filters'] as $colName => $vals) {
  197. if ($colName === $column) {
  198. continue;
  199. }
  200. $filterDbColumn = self::resolveDbColumn($table, $dbTable, $colName);
  201. if (! $filterDbColumn || ! Schema::hasColumn($dbTable, $filterDbColumn)) {
  202. continue;
  203. }
  204. $q->where(function ($query) use ($filterDbColumn, $vals) {
  205. foreach (explode('||', $vals) as $val) {
  206. if ($val == '-пусто-') {
  207. self::applyEmptyFilterConditionForFilterQuery($query, $filterDbColumn);
  208. } else {
  209. $query->orWhere($filterDbColumn, '=', $val);
  210. }
  211. }
  212. });
  213. }
  214. }
  215. $result = $q->orderBy('filter_value')->get()->pluck('filter_value')->toArray();
  216. // Конвертация цен из копеек в рубли для отображения
  217. if (str_ends_with($dbColumn, '_price')) {
  218. $result = array_map(function ($val) {
  219. if ($val === null || $val === '-пусто-') {
  220. return $val;
  221. }
  222. return $val / 100;
  223. }, $result);
  224. }
  225. // Применяем маппинг значений, если есть
  226. if (isset(self::VALUE_MAP[$table][$dbColumn])) {
  227. $map = self::VALUE_MAP[$table][$dbColumn];
  228. $result = array_map(fn ($val) => $map[$val] ?? $val, $result);
  229. }
  230. } else {
  231. $result = [];
  232. }
  233. return response()->json($result, 200, [], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
  234. }
  235. private static function normalizedSelectExpression(string $column): string
  236. {
  237. return "CASE WHEN {$column} IS NULL OR TRIM(CAST({$column} AS CHAR)) = '' THEN '-пусто-' ELSE CAST({$column} AS CHAR) END";
  238. }
  239. private static function applyEmptyFilterConditionForFilterQuery($query, string $column): void
  240. {
  241. $query->orWhereNull($column)
  242. ->orWhereRaw("TRIM(CAST({$column} AS CHAR)) = ''");
  243. }
  244. private function assertCanViewColumn(FilterRequest $request, string $table, string $column): void
  245. {
  246. if ($table === 'stock_availability') {
  247. abort_unless($request->user()->hasPermission('stock.view'), 403);
  248. return;
  249. }
  250. if ($table === 'stock_orders') {
  251. abort_unless($request->user()->hasPermission('stock.orders.view'), 403);
  252. return;
  253. }
  254. $module = self::FIELD_ACCESS_MODULES[$table] ?? null;
  255. if ($module === null) {
  256. return;
  257. }
  258. $field = self::COLUMN_MAP[$table][$column] ?? $column;
  259. abort_unless($request->user()->canViewField($module, $field), 403);
  260. }
  261. private function stockAvailabilityFilters(string $column): JsonResponse
  262. {
  263. $values = match ($column) {
  264. 'article', 'calculator_name', 'kind', 'unit' => CommonCatalogItem::query()
  265. ->whereHas('stockOrders')
  266. ->pluck($column),
  267. 'latest_order_note' => CommonCatalogItem::query()
  268. ->whereHas('stockOrders')
  269. ->with('latestStockOrder:id,common_catalog_item_id,note')
  270. ->get()
  271. ->pluck('latestStockOrder.note'),
  272. default => [],
  273. };
  274. return $this->filterValuesResponse($values);
  275. }
  276. private function stockOrderFilters(string $column): JsonResponse
  277. {
  278. $values = match ($column) {
  279. 'order_number', 'note' => StockOrder::query()->pluck($column),
  280. 'created_at' => StockOrder::query()
  281. ->selectRaw('DATE(created_at) as filter_date')
  282. ->pluck('filter_date'),
  283. 'item_article' => StockOrder::query()
  284. ->join('common_catalog_items', 'common_catalog_items.id', '=', 'stock_orders.common_catalog_item_id')
  285. ->pluck('common_catalog_items.article'),
  286. 'status_name' => StockOrder::query()
  287. ->pluck('status')
  288. ->map(fn (string $status): string => StockOrder::STATUS_NAMES[$status] ?? $status),
  289. default => [],
  290. };
  291. return $this->filterValuesResponse($values);
  292. }
  293. private function productionOrderPlanningFilters(string $column): JsonResponse
  294. {
  295. $values = match ($column) {
  296. 'delivery_dates' => DB::table('production_order_deliveries')
  297. ->whereNull('deleted_at')
  298. ->pluck('delivery_date'),
  299. 'installation_dates' => DB::table('production_order_installations')
  300. ->whereNull('deleted_at')
  301. ->pluck('installation_date'),
  302. default => [],
  303. };
  304. return $this->filterValuesResponse($values);
  305. }
  306. private function filterValuesResponse(iterable $values): JsonResponse
  307. {
  308. $normalized = collect($values)
  309. ->map(static fn (mixed $value): string => $value === null || trim((string) $value) === ''
  310. ? '-пусто-'
  311. : (string) $value)
  312. ->unique()
  313. ->sort(static fn (string $left, string $right): int => strnatcasecmp($left, $right))
  314. ->values()
  315. ->all();
  316. return response()->json($normalized, 200, [], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
  317. }
  318. /**
  319. * Определяет реальный столбец БД по имени столбца из заголовка.
  320. * Приоритет: прямое совпадение в БД → COLUMN_MAP для конкретной таблицы.
  321. */
  322. public static function resolveDbColumn(string $table, string $dbTable, string $column): ?string
  323. {
  324. // 1. Прямое совпадение — столбец существует в БД
  325. if (Schema::hasColumn($dbTable, $column)) {
  326. return $column;
  327. }
  328. // 2. Явный маппинг для конкретной таблицы
  329. if (isset(self::COLUMN_MAP[$table][$column])) {
  330. return self::COLUMN_MAP[$table][$column];
  331. }
  332. return null;
  333. }
  334. }