FilterController.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Models\SparePartsView;
  4. use App\Http\Requests\FilterRequest;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Schema;
  7. class FilterController extends Controller
  8. {
  9. const DB_TABLES = [
  10. 'orders' => 'orders_view',
  11. 'product_sku' => 'mafs_view',
  12. 'products' => 'products',
  13. 'reclamations' => 'reclamations_view',
  14. 'maf_order' => 'maf_orders_view',
  15. 'import' => 'imports',
  16. 'responsibles' => 'responsibles_view',
  17. 'users' => 'users',
  18. 'contracts' => 'contracts',
  19. 'spare_parts' => 'spare_parts_view',
  20. 'spare_part_orders' => 'spare_part_orders_view',
  21. 'notifications' => 'user_notifications',
  22. 'notification_logs' => 'notification_delivery_logs',
  23. ];
  24. const SKIP_YEAR_FILTER = [
  25. 'reclamations',
  26. 'notifications',
  27. 'notification_logs',
  28. ];
  29. /**
  30. * Маппинг виртуальных столбцов (аксессоров Eloquent) на реальные столбцы БД.
  31. * Ключ — имя таблицы из DB_TABLES, значение — массив 'виртуальный_столбец' => 'реальный_столбец'.
  32. */
  33. const COLUMN_MAP = [
  34. 'spare_part_orders' => [
  35. 'status_name' => 'status',
  36. 'with_documents_text' => 'with_documents',
  37. ],
  38. 'spare_parts' => [
  39. 'customer_price_txt' => 'customer_price',
  40. 'expertise_price_txt' => 'expertise_price',
  41. 'purchase_price_txt' => 'purchase_price',
  42. ],
  43. 'products' => [
  44. 'product_price_txt' => 'product_price',
  45. 'installation_price_txt' => 'installation_price',
  46. 'total_price_txt' => 'total_price',
  47. ],
  48. 'responsibles' => [
  49. 'area-name' => 'area_name',
  50. ],
  51. ];
  52. /**
  53. * Маппинг значений: реальное значение БД => отображаемое значение.
  54. * Ключ — имя таблицы, затем реальный столбец.
  55. */
  56. const VALUE_MAP = [
  57. 'users' => [
  58. 'role' => \App\Models\Role::NAMES,
  59. ],
  60. 'notifications' => [
  61. 'type' => [
  62. 'platform' => 'Площадки',
  63. 'reclamation' => 'Рекламации',
  64. 'schedule' => 'График монтажей',
  65. ],
  66. 'event' => [
  67. 'created' => 'Создание',
  68. 'status_changed' => 'Смена статуса',
  69. 'schedule_added' => 'Добавлено в график',
  70. ],
  71. ],
  72. 'spare_part_orders' => [
  73. 'with_documents' => [
  74. 0 => 'Нет',
  75. 1 => 'Да',
  76. ],
  77. 'status' => [
  78. 'ordered' => 'Заказано',
  79. 'in_stock' => 'На складе',
  80. 'shipped' => 'Отгружено',
  81. ],
  82. ],
  83. 'notification_logs' => [
  84. 'channel' => [
  85. 'in_app' => 'Браузер',
  86. 'browser' => 'Браузер',
  87. 'push' => 'Android/iOS',
  88. 'email' => 'Email',
  89. ],
  90. 'status' => [
  91. 'sent' => 'Отправлено',
  92. 'failed' => 'Ошибка',
  93. 'skipped' => 'Пропущено',
  94. 'dead_letter' => 'Dead letter',
  95. ],
  96. ],
  97. ];
  98. public function getFilters(FilterRequest $request)
  99. {
  100. $table = $request->validated('table');
  101. $column = $request->validated('column');
  102. if(!array_key_exists($table, self::DB_TABLES)) {
  103. abort(400, 'Table not found');
  104. }
  105. $gp = session('gp_' . $table);
  106. if ($table === 'spare_parts' && $column === 'pricing_codes_list') {
  107. $result = DB::table('pricing_codes as pc')
  108. ->join('spare_part_pricing_code as sppc', 'sppc.pricing_code_id', '=', 'pc.id')
  109. ->select('pc.code')
  110. ->distinct()
  111. ->orderBy('pc.code')
  112. ->pluck('pc.code')
  113. ->toArray();
  114. $hasEmptyValue = SparePartsView::query()
  115. ->doesntHave('pricingCodes')
  116. ->exists();
  117. if ($hasEmptyValue) {
  118. array_unshift($result, '-пусто-');
  119. }
  120. return response()->json($result, 200, [], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
  121. }
  122. $dbTable = self::DB_TABLES[$table];
  123. // Определяем реальный столбец БД
  124. $dbColumn = self::resolveDbColumn($table, $dbTable, $column);
  125. if ($dbColumn && Schema::hasColumn($dbTable, $dbColumn)) {
  126. $normalizedColumn = self::normalizedSelectExpression($dbColumn);
  127. $q = DB::table($dbTable)->selectRaw($normalizedColumn . ' as filter_value')->distinct();
  128. if (!in_array($table, self::SKIP_YEAR_FILTER) && Schema::hasColumn($dbTable, 'year')) {
  129. $q->where('year' , year());
  130. }
  131. if (Schema::hasColumn($dbTable, 'deleted_at')) {
  132. $q->whereNull('deleted_at');
  133. }
  134. if(isset($gp['filters']) && is_array($gp['filters']) && count($gp['filters'])) {
  135. foreach ($gp['filters'] as $colName => $vals) {
  136. if ($colName === $column) continue;
  137. $filterDbColumn = self::resolveDbColumn($table, $dbTable, $colName);
  138. if (!$filterDbColumn || !Schema::hasColumn($dbTable, $filterDbColumn)) continue;
  139. $q->where(function ($query) use ($filterDbColumn, $vals) {
  140. foreach (explode('||', $vals) as $val) {
  141. if($val == '-пусто-') {
  142. self::applyEmptyFilterConditionForFilterQuery($query, $filterDbColumn);
  143. } else {
  144. $query->orWhere($filterDbColumn, '=', $val);
  145. }
  146. }
  147. });
  148. }
  149. }
  150. $result = $q->orderBy('filter_value')->get()->pluck('filter_value')->toArray();
  151. // Конвертация цен из копеек в рубли для отображения
  152. if (str_ends_with($dbColumn, '_price')) {
  153. $result = array_map(function ($val) {
  154. if ($val === null || $val === '-пусто-') {
  155. return $val;
  156. }
  157. return $val / 100;
  158. }, $result);
  159. }
  160. // Применяем маппинг значений, если есть
  161. if (isset(self::VALUE_MAP[$table][$dbColumn])) {
  162. $map = self::VALUE_MAP[$table][$dbColumn];
  163. $result = array_map(fn($val) => $map[$val] ?? $val, $result);
  164. }
  165. } else {
  166. $result = [];
  167. }
  168. return response()->json($result, 200, [], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
  169. }
  170. private static function normalizedSelectExpression(string $column): string
  171. {
  172. return "CASE WHEN {$column} IS NULL OR TRIM(CAST({$column} AS CHAR)) = '' THEN '-пусто-' ELSE CAST({$column} AS CHAR) END";
  173. }
  174. private static function applyEmptyFilterConditionForFilterQuery($query, string $column): void
  175. {
  176. $query->orWhereNull($column)
  177. ->orWhereRaw("TRIM(CAST({$column} AS CHAR)) = ''");
  178. }
  179. /**
  180. * Определяет реальный столбец БД по имени столбца из заголовка.
  181. * Приоритет: прямое совпадение в БД → COLUMN_MAP для конкретной таблицы.
  182. */
  183. public static function resolveDbColumn(string $table, string $dbTable, string $column): ?string
  184. {
  185. // 1. Прямое совпадение — столбец существует в БД
  186. if (Schema::hasColumn($dbTable, $column)) {
  187. return $column;
  188. }
  189. // 2. Явный маппинг для конкретной таблицы
  190. if (isset(self::COLUMN_MAP[$table][$column])) {
  191. return self::COLUMN_MAP[$table][$column];
  192. }
  193. return null;
  194. }
  195. }