فهرست منبع

Доработки модуля запчастей: несколько шифров расценки у запчасти + экспорт/импорт

Alexander Musikhin 4 روز پیش
والد
کامیت
ed6d019ca1

+ 45 - 3
app/Http/Controllers/SparePartController.php

@@ -30,7 +30,7 @@ class SparePartController extends Controller
             'customer_price_txt' => 'Цена для заказчика',
             'expertise_price_txt' => 'Цена экспертизы',
             'tsn_number' => '№ по ТСН',
-            'pricing_code' => 'Шифр расценки',
+            'pricing_codes_list' => 'Шифр расценки и коды ресурсов',
             'min_stock' => 'Минимальный остаток',
         ],
         'searchFields' => [
@@ -38,7 +38,6 @@ class SparePartController extends Controller
             'used_in_maf',
             'note',
             'tsn_number',
-            'pricing_code',
         ],
         'routeName' => 'spare_parts.show',
     ];
@@ -98,7 +97,8 @@ class SparePartController extends Controller
 
     public function store(StoreSparePartRequest $request): RedirectResponse
     {
-        SparePart::create($request->validated());
+        $sparePart = SparePart::create($request->validated());
+        $this->syncPricingCodes($sparePart, $request);
         $previous_url = $request->get('previous_url') ?? route('spare_parts.index', session('gp_spare_parts'));
         return redirect()->to($previous_url)->with(['success' => 'Запчасть успешно создана!']);
     }
@@ -106,10 +106,52 @@ class SparePartController extends Controller
     public function update(StoreSparePartRequest $request, SparePart $sparePart): RedirectResponse
     {
         $sparePart->update($request->validated());
+        $this->syncPricingCodes($sparePart, $request);
         $previous_url = $request->get('previous_url') ?? route('spare_parts.index', session('gp_spare_parts'));
         return redirect()->to($previous_url)->with(['success' => 'Запчасть успешно обновлена!']);
     }
 
+    protected function syncPricingCodes(SparePart $sparePart, StoreSparePartRequest $request): void
+    {
+        $codes = $request->input('pricing_codes', []);
+        $descriptions = $request->input('pricing_codes_descriptions', []);
+
+        $pricingCodeIds = [];
+
+        foreach ($codes as $index => $code) {
+            $code = trim($code);
+            if (empty($code)) {
+                continue;
+            }
+
+            $description = trim($descriptions[$index] ?? '');
+
+            // Находим или создаём PricingCode
+            $pricingCode = \App\Models\PricingCode::where('type', \App\Models\PricingCode::TYPE_PRICING_CODE)
+                ->where('code', $code)
+                ->first();
+
+            if ($pricingCode) {
+                // Обновляем расшифровку, если предоставлена
+                if ($description && $description !== $pricingCode->description) {
+                    $pricingCode->update(['description' => $description]);
+                }
+            } else {
+                // Создаём новый код
+                $pricingCode = \App\Models\PricingCode::create([
+                    'type' => \App\Models\PricingCode::TYPE_PRICING_CODE,
+                    'code' => $code,
+                    'description' => $description ?: null,
+                ]);
+            }
+
+            $pricingCodeIds[] = $pricingCode->id;
+        }
+
+        // Синхронизируем связи
+        $sparePart->pricingCodes()->sync($pricingCodeIds);
+    }
+
     public function destroy(SparePart $sparePart): RedirectResponse
     {
         // Проверка на наличие заказов

+ 6 - 14
app/Http/Requests/StoreSparePartRequest.php

@@ -31,8 +31,10 @@ class StoreSparePartRequest extends FormRequest
             'expertise_price' => 'nullable|numeric|min:0',
             'tsn_number' => 'nullable|string|max:255',
             'tsn_number_description' => 'nullable|string',
-            'pricing_code' => 'nullable|string',
-            'pricing_code_description' => 'nullable|string',
+            'pricing_codes' => 'nullable|array',
+            'pricing_codes.*' => 'nullable|string|max:255',
+            'pricing_codes_descriptions' => 'nullable|array',
+            'pricing_codes_descriptions.*' => 'nullable|string',
             'min_stock' => 'nullable|integer|min:0',
         ];
     }
@@ -55,17 +57,7 @@ class StoreSparePartRequest extends FormRequest
             }
         }
 
-        // Сохраняем/обновляем расшифровку шифра расценки в справочник
-        if ($this->filled('pricing_code')) {
-            $description = $this->input('pricing_code_description');
-            // Сохраняем только если есть расшифровка
-            if ($description) {
-                \App\Models\PricingCode::createOrUpdate(
-                    \App\Models\PricingCode::TYPE_PRICING_CODE,
-                    $this->input('pricing_code'),
-                    $description
-                );
-            }
-        }
+        // Примечание: сохранение pricing_codes и их расшифровок
+        // происходит в контроллере через syncPricingCodes()
     }
 }

+ 7 - 0
app/Models/PricingCode.php

@@ -3,6 +3,7 @@
 namespace App\Models;
 
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
 class PricingCode extends Model
 {
@@ -15,6 +16,12 @@ class PricingCode extends Model
         'description',
     ];
 
+    public function spareParts(): BelongsToMany
+    {
+        return $this->belongsToMany(SparePart::class, 'spare_part_pricing_code')
+            ->withTimestamps();
+    }
+
     /**
      * Получить расшифровку для № по ТСН
      */

+ 19 - 2
app/Models/SparePart.php

@@ -26,7 +26,6 @@ class SparePart extends Model
         'customer_price',
         'expertise_price',
         'tsn_number',
-        'pricing_code',
         'min_stock',
     ];
 
@@ -43,6 +42,7 @@ class SparePart extends Model
         'total_quantity',
         'tsn_number_description',
         'pricing_code_description',
+        'pricing_codes_list',
     ];
 
     // Аксессоры для цен (копейки -> рубли)
@@ -126,6 +126,12 @@ class SparePart extends Model
             ->withTimestamps();
     }
 
+    public function pricingCodes(): BelongsToMany
+    {
+        return $this->belongsToMany(PricingCode::class, 'spare_part_pricing_code')
+            ->withTimestamps();
+    }
+
     // ВЫЧИСЛЯЕМЫЕ ПОЛЯ (с учетом текущего года!)
     // Примечание: YearScope автоматически применяется к SparePartOrder,
     // поэтому явный фильтр по году НЕ нужен
@@ -176,7 +182,18 @@ class SparePart extends Model
     protected function pricingCodeDescription(): Attribute
     {
         return Attribute::make(
-            get: fn() => PricingCode::getPricingCodeDescription($this->pricing_code)
+            get: function () {
+                $codes = $this->pricingCodes;
+                if ($codes->isEmpty()) {
+                    return null;
+                }
+                return $codes->map(fn($code) => $code->code . ($code->description ? ': ' . $code->description : ''))->implode("\n");
+            }
         );
     }
+
+    public function getPricingCodesListAttribute(): string
+    {
+        return $this->pricingCodes->pluck('code')->implode(', ');
+    }
 }

+ 3 - 2
app/Services/Export/ExportSparePartsService.php

@@ -24,7 +24,7 @@ class ExportSparePartsService
 
         // Получаем данные из модели (НЕ view!)
         // ВАЖНО: Вычисляемые поля учитывают текущий год из сессии
-        $spareParts = SparePart::orderBy('article')->get();
+        $spareParts = SparePart::with('pricingCodes')->orderBy('article')->get();
 
         $i = 2;
         foreach ($spareParts as $sp) {
@@ -39,7 +39,8 @@ class ExportSparePartsService
             $sheet->setCellValue('I' . $i, $sp->customer_price);
             $sheet->setCellValue('J' . $i, $sp->expertise_price);
             $sheet->setCellValue('K' . $i, $sp->tsn_number);
-            $sheet->setCellValue('L' . $i, $sp->pricing_code);
+            // Экспортируем множественные шифры расценки через запятую
+            $sheet->setCellValue('L' . $i, $sp->pricingCodes->pluck('code')->implode(', '));
             $sheet->setCellValue('M' . $i, $sp->min_stock);
             $i++;
         }

+ 41 - 2
app/Services/Import/ImportSparePartsService.php

@@ -81,6 +81,15 @@ class ImportSparePartsService
                 continue;
             }
 
+            // Парсим множественные шифры расценки (через запятую, точку с запятой или перенос строки)
+            $pricingCodesRaw = $sheet->getCell('L' . $row)->getValue();
+            $pricingCodeStrings = [];
+            if (!empty($pricingCodesRaw)) {
+                $pricingCodeStrings = preg_split('/[,;\n]+/', $pricingCodesRaw);
+                $pricingCodeStrings = array_map('trim', $pricingCodeStrings);
+                $pricingCodeStrings = array_filter($pricingCodeStrings);
+            }
+
             $data = [
                 'article' => $article,
                 'used_in_maf' => $sheet->getCell('C' . $row)->getValue(),
@@ -89,7 +98,6 @@ class ImportSparePartsService
                 'customer_price' => $this->parsePrice($sheet->getCell('I' . $row)->getValue()),
                 'expertise_price' => $this->parsePrice($sheet->getCell('J' . $row)->getValue()),
                 'tsn_number' => $sheet->getCell('K' . $row)->getValue(),
-                'pricing_code' => $sheet->getCell('L' . $row)->getValue(),
                 'min_stock' => (int)$sheet->getCell('M' . $row)->getValue() ?: 0,
             ];
 
@@ -104,9 +112,12 @@ class ImportSparePartsService
                 $sparePart->update($data);
                 $updated++;
             } else {
-                SparePart::create($data);
+                $sparePart = SparePart::create($data);
                 $imported++;
             }
+
+            // Синхронизируем связи с pricing_codes
+            $this->syncPricingCodes($sparePart, $pricingCodeStrings);
         }
 
         $this->log("Каталог запчастей: импортировано {$imported}, обновлено {$updated}, пропущено {$skipped}");
@@ -188,6 +199,34 @@ class ImportSparePartsService
         return round($price * 100);
     }
 
+    private function syncPricingCodes(SparePart $sparePart, array $codeStrings): void
+    {
+        $pricingCodeIds = [];
+
+        foreach ($codeStrings as $code) {
+            if (empty($code)) {
+                continue;
+            }
+
+            // Находим или создаём PricingCode
+            $pricingCode = PricingCode::where('type', PricingCode::TYPE_PRICING_CODE)
+                ->where('code', $code)
+                ->first();
+
+            if (!$pricingCode) {
+                $pricingCode = PricingCode::create([
+                    'type' => PricingCode::TYPE_PRICING_CODE,
+                    'code' => $code,
+                    'description' => null,
+                ]);
+            }
+
+            $pricingCodeIds[] = $pricingCode->id;
+        }
+
+        $sparePart->pricingCodes()->sync($pricingCodeIds);
+    }
+
     private function log(string $message): void
     {
         $this->logs[] = '[' . date('H:i:s') . '] ' . $message;

+ 102 - 0
database/migrations/2026_01_24_150000_create_spare_part_pricing_code_table.php

@@ -0,0 +1,102 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        // 1. Создаём pivot-таблицу
+        Schema::create('spare_part_pricing_code', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('spare_part_id')->constrained()->onDelete('cascade');
+            $table->foreignId('pricing_code_id')->constrained()->onDelete('cascade');
+            $table->timestamps();
+
+            $table->unique(['spare_part_id', 'pricing_code_id']);
+        });
+
+        // 2. Мигрируем существующие данные из spare_parts.pricing_code
+        $spareParts = DB::table('spare_parts')
+            ->whereNotNull('pricing_code')
+            ->where('pricing_code', '!=', '')
+            ->get(['id', 'pricing_code']);
+
+        foreach ($spareParts as $sparePart) {
+            // Находим pricing_code в справочнике по коду
+            $pricingCode = DB::table('pricing_codes')
+                ->where('type', 'pricing_code')
+                ->where('code', $sparePart->pricing_code)
+                ->first();
+
+            if ($pricingCode) {
+                // Создаём связь
+                DB::table('spare_part_pricing_code')->insert([
+                    'spare_part_id' => $sparePart->id,
+                    'pricing_code_id' => $pricingCode->id,
+                    'created_at' => now(),
+                    'updated_at' => now(),
+                ]);
+            } else {
+                // Создаём новую запись в справочнике и связываем
+                $newPricingCodeId = DB::table('pricing_codes')->insertGetId([
+                    'type' => 'pricing_code',
+                    'code' => $sparePart->pricing_code,
+                    'description' => null,
+                    'created_at' => now(),
+                    'updated_at' => now(),
+                ]);
+
+                DB::table('spare_part_pricing_code')->insert([
+                    'spare_part_id' => $sparePart->id,
+                    'pricing_code_id' => $newPricingCodeId,
+                    'created_at' => now(),
+                    'updated_at' => now(),
+                ]);
+            }
+        }
+
+        // 3. Удаляем колонку pricing_code из spare_parts
+        Schema::table('spare_parts', function (Blueprint $table) {
+            $table->dropColumn('pricing_code');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        // 1. Восстанавливаем колонку pricing_code
+        Schema::table('spare_parts', function (Blueprint $table) {
+            $table->text('pricing_code')->nullable()->after('tsn_number');
+        });
+
+        // 2. Восстанавливаем данные из pivot-таблицы (берём первый код)
+        $pivots = DB::table('spare_part_pricing_code')
+            ->select('spare_part_id', DB::raw('MIN(pricing_code_id) as pricing_code_id'))
+            ->groupBy('spare_part_id')
+            ->get();
+
+        foreach ($pivots as $pivot) {
+            $pricingCode = DB::table('pricing_codes')
+                ->where('id', $pivot->pricing_code_id)
+                ->first();
+
+            if ($pricingCode) {
+                DB::table('spare_parts')
+                    ->where('id', $pivot->spare_part_id)
+                    ->update(['pricing_code' => $pricingCode->code]);
+            }
+        }
+
+        // 3. Удаляем pivot-таблицу
+        Schema::dropIfExists('spare_part_pricing_code');
+    }
+};

+ 270 - 26
resources/views/spare_parts/edit.blade.php

@@ -131,28 +131,74 @@
                         </div>
 
                         <div class="mb-3">
-                            <label for="pricing_code" class="form-label">Шифр расценки</label>
-                            <div class="row">
-                                <div class="col-md-5 position-relative">
-                                    <input type="text"
-                                           class="form-control"
-                                           id="pricing_code"
-                                           name="pricing_code"
-                                           value="{{ old('pricing_code', $spare_part->pricing_code ?? '') }}"
-                                           autocomplete="off"
-                                           {{ hasRole('admin') ? '' : 'readonly' }}>
-                                    <div class="autocomplete-dropdown" id="pricing_code_dropdown"></div>
-                                </div>
-                                <div class="col-md-7">
-                                    <input type="text"
-                                           class="form-control"
-                                           id="pricing_code_description"
-                                           name="pricing_code_description"
-                                           placeholder="Расшифровка"
-                                           {{ hasRole('admin') ? '' : 'readonly' }}>
-                                </div>
+                            <label class="form-label">Шифр расценки и коды ресурсов</label>
+                            <div id="pricing_codes_container">
+                                @php
+                                    $existingCodes = old('pricing_codes', $spare_part ? $spare_part->pricingCodes->pluck('code')->toArray() : []);
+                                @endphp
+                                @forelse($existingCodes as $index => $code)
+                                    <div class="pricing-code-row row mb-2" data-index="{{ $index }}">
+                                        <div class="col-md-5 position-relative">
+                                            <input type="text"
+                                                   class="form-control pricing-code-input"
+                                                   name="pricing_codes[]"
+                                                   value="{{ $code }}"
+                                                   autocomplete="off"
+                                                   placeholder="Код"
+                                                   {{ hasRole('admin') ? '' : 'readonly' }}>
+                                            <div class="autocomplete-dropdown pricing-code-dropdown"></div>
+                                        </div>
+                                        <div class="col-md-5">
+                                            <input type="text"
+                                                   class="form-control pricing-code-description-input"
+                                                   name="pricing_codes_descriptions[]"
+                                                   placeholder="Расшифровка"
+                                                   {{ hasRole('admin') ? '' : 'readonly' }}>
+                                        </div>
+                                        <div class="col-md-2">
+                                            @if(hasRole('admin'))
+                                                <button type="button" class="btn btn-outline-danger btn-remove-pricing-code" title="Удалить">
+                                                    <i class="bi bi-trash"></i>
+                                                </button>
+                                            @endif
+                                        </div>
+                                        <div class="col-12"><div class="form-text pricing-code-hint"></div></div>
+                                    </div>
+                                @empty
+                                    <div class="pricing-code-row row mb-2" data-index="0">
+                                        <div class="col-md-5 position-relative">
+                                            <input type="text"
+                                                   class="form-control pricing-code-input"
+                                                   name="pricing_codes[]"
+                                                   value=""
+                                                   autocomplete="off"
+                                                   placeholder="Код"
+                                                   {{ hasRole('admin') ? '' : 'readonly' }}>
+                                            <div class="autocomplete-dropdown pricing-code-dropdown"></div>
+                                        </div>
+                                        <div class="col-md-5">
+                                            <input type="text"
+                                                   class="form-control pricing-code-description-input"
+                                                   name="pricing_codes_descriptions[]"
+                                                   placeholder="Расшифровка"
+                                                   {{ hasRole('admin') ? '' : 'readonly' }}>
+                                        </div>
+                                        <div class="col-md-2">
+                                            @if(hasRole('admin'))
+                                                <button type="button" class="btn btn-outline-danger btn-remove-pricing-code" title="Удалить">
+                                                    <i class="bi bi-trash"></i>
+                                                </button>
+                                            @endif
+                                        </div>
+                                        <div class="col-12"><div class="form-text pricing-code-hint"></div></div>
+                                    </div>
+                                @endforelse
                             </div>
-                            <div class="form-text" id="pricing_code_hint"></div>
+                            @if(hasRole('admin'))
+                                <button type="button" class="btn btn-sm btn-outline-primary" id="btn_add_pricing_code">
+                                    <i class="bi bi-plus-circle"></i> Добавить код
+                                </button>
+                            @endif
                         </div>
 
                         <div class="mb-3">
@@ -424,18 +470,15 @@ waitForJQuery(function() {
 
         // Инициализация автокомплитов
         initAutocomplete('tsn_number', 'tsn_number_dropdown', 'tsn_number_description', 'tsn_number_hint', 'tsn_number');
-        initAutocomplete('pricing_code', 'pricing_code_dropdown', 'pricing_code_description', 'pricing_code_hint', 'pricing_code');
 
         // Загрузка расшифровок при открытии страницы
         function loadInitialDescription(inputId, descriptionId, hintId, type) {
             const code = $('#' + inputId).val().trim();
-            //console.log('Load initial for:', inputId, 'code:', code);
             if (code.length > 0) {
                 $.get('{{ route('pricing_codes.get_description') }}', {
                     type: type,
                     code: code
                 }).done(function(data) {
-                    //console.log('Initial description for', inputId, ':', data);
                     if (data.description) {
                         $('#' + descriptionId).val(data.description);
                         $('#' + descriptionId).prop('readonly', true);
@@ -451,8 +494,209 @@ waitForJQuery(function() {
         }
 
         loadInitialDescription('tsn_number', 'tsn_number_description', 'tsn_number_hint', 'tsn_number');
-        loadInitialDescription('pricing_code', 'pricing_code_description', 'pricing_code_hint', 'pricing_code');
-        //console.log('Autocomplete init complete');
+
+        // Функционал множественных шифров расценки
+        function initPricingCodeAutocomplete($row) {
+            const $input = $row.find('.pricing-code-input');
+            const $dropdown = $row.find('.pricing-code-dropdown');
+            const $description = $row.find('.pricing-code-description-input');
+            const $hint = $row.find('.pricing-code-hint');
+            let currentFocus = -1;
+            let searchTimeout;
+
+            $input.on('input', function() {
+                const query = $(this).val().trim();
+                clearTimeout(searchTimeout);
+
+                if (query.length > 0) {
+                    searchTimeout = setTimeout(function() {
+                        $.get('{{ route('pricing_codes.search') }}', {
+                            type: 'pricing_code',
+                            query: query
+                        }).done(function(data) {
+                            showDropdownPricing($dropdown, data, query, $input, $description, $hint);
+                        });
+
+                        $.get('{{ route('pricing_codes.get_description') }}', {
+                            type: 'pricing_code',
+                            code: query
+                        }).done(function(data) {
+                            if (data.description) {
+                                $description.val(data.description);
+                                $description.prop('readonly', true);
+                                $hint.html('<i class="bi bi-check-circle text-success"></i> Код найден в справочнике');
+                            } else {
+                                $description.val('');
+                                $description.prop('readonly', false);
+                                $hint.html('<i class="bi bi-plus-circle text-primary"></i> Новый код - заполните расшифровку');
+                            }
+                        });
+                    }, 200);
+                } else {
+                    $dropdown.hide();
+                    $description.val('');
+                    $description.prop('readonly', false);
+                    $hint.text('');
+                }
+            });
+
+            $input.on('focus', function() {
+                const query = $(this).val().trim();
+                if (query.length > 0) {
+                    $.get('{{ route('pricing_codes.search') }}', {
+                        type: 'pricing_code',
+                        query: query
+                    }, function(data) {
+                        showDropdownPricing($dropdown, data, query, $input, $description, $hint);
+                    });
+                }
+            });
+
+            $input.on('keydown', function(e) {
+                const $items = $dropdown.find('.autocomplete-item');
+
+                if (e.keyCode === 40) {
+                    e.preventDefault();
+                    currentFocus++;
+                    if (currentFocus >= $items.length) currentFocus = 0;
+                    $items.removeClass('active');
+                    if (currentFocus >= 0) $items.eq(currentFocus).addClass('active');
+                } else if (e.keyCode === 38) {
+                    e.preventDefault();
+                    currentFocus--;
+                    if (currentFocus < 0) currentFocus = $items.length - 1;
+                    $items.removeClass('active');
+                    if (currentFocus >= 0) $items.eq(currentFocus).addClass('active');
+                } else if (e.keyCode === 13) {
+                    e.preventDefault();
+                    if (currentFocus > -1 && $items.length > 0) {
+                        $items.eq(currentFocus).trigger('click');
+                    }
+                } else if (e.keyCode === 27) {
+                    $dropdown.hide();
+                }
+            });
+
+            $(document).on('click', function(e) {
+                if (!$(e.target).closest($row).length) {
+                    $dropdown.hide();
+                }
+            });
+        }
+
+        function showDropdownPricing($dropdown, items, query, $input, $description, $hint) {
+            $dropdown.empty();
+
+            if (items.length === 0) {
+                $dropdown.hide();
+                return;
+            }
+
+            items.forEach(function(item) {
+                const $item = $('<div class="autocomplete-item"></div>');
+                const highlightedCode = highlightMatchPricing(item.code, query);
+                const highlightedDesc = item.description ? highlightMatchPricing(item.description, query) : '';
+
+                $item.html('<div class="code">' + highlightedCode + '</div>' +
+                           (item.description ? '<div class="description">' + highlightedDesc + '</div>' : ''));
+                $item.data('code', item.code);
+                $item.data('description', item.description || '');
+
+                $item.on('click', function() {
+                    $input.val($(this).data('code'));
+                    $description.val($(this).data('description'));
+                    $description.prop('readonly', true);
+                    $hint.html('<i class="bi bi-check-circle text-success"></i> Код найден в справочнике');
+                    $dropdown.hide();
+                });
+
+                $dropdown.append($item);
+            });
+
+            $dropdown.show();
+        }
+
+        function highlightMatchPricing(text, query) {
+            if (!text || !query) return text || '';
+            const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
+            return text.replace(regex, '<strong>$1</strong>');
+        }
+
+        function loadInitialPricingCodeDescription($row) {
+            const code = $row.find('.pricing-code-input').val().trim();
+            const $description = $row.find('.pricing-code-description-input');
+            const $hint = $row.find('.pricing-code-hint');
+
+            if (code.length > 0) {
+                $.get('{{ route('pricing_codes.get_description') }}', {
+                    type: 'pricing_code',
+                    code: code
+                }).done(function(data) {
+                    if (data.description) {
+                        $description.val(data.description);
+                        $description.prop('readonly', true);
+                        $hint.html('<i class="bi bi-check-circle text-success"></i> Код найден в справочнике');
+                    } else {
+                        $description.prop('readonly', false);
+                        $hint.html('<i class="bi bi-plus-circle text-primary"></i> Новый код - заполните расшифровку');
+                    }
+                });
+            }
+        }
+
+        // Инициализация существующих строк
+        $('.pricing-code-row').each(function() {
+            initPricingCodeAutocomplete($(this));
+            loadInitialPricingCodeDescription($(this));
+        });
+
+        // Добавление новой строки
+        $('#btn_add_pricing_code').on('click', function() {
+            const $container = $('#pricing_codes_container');
+            const newIndex = $container.find('.pricing-code-row').length;
+            const $newRow = $(`
+                <div class="pricing-code-row row mb-2" data-index="${newIndex}">
+                    <div class="col-md-5 position-relative">
+                        <input type="text"
+                               class="form-control pricing-code-input"
+                               name="pricing_codes[]"
+                               value=""
+                               autocomplete="off"
+                               placeholder="Код">
+                        <div class="autocomplete-dropdown pricing-code-dropdown"></div>
+                    </div>
+                    <div class="col-md-5">
+                        <input type="text"
+                               class="form-control pricing-code-description-input"
+                               name="pricing_codes_descriptions[]"
+                               placeholder="Расшифровка">
+                    </div>
+                    <div class="col-md-2">
+                        <button type="button" class="btn btn-outline-danger btn-remove-pricing-code" title="Удалить">
+                            <i class="bi bi-trash"></i>
+                        </button>
+                    </div>
+                    <div class="col-12"><div class="form-text pricing-code-hint"></div></div>
+                </div>
+            `);
+            $container.append($newRow);
+            initPricingCodeAutocomplete($newRow);
+            $newRow.find('.pricing-code-input').focus();
+        });
+
+        // Удаление строки
+        $(document).on('click', '.btn-remove-pricing-code', function() {
+            const $container = $('#pricing_codes_container');
+            if ($container.find('.pricing-code-row').length > 1) {
+                $(this).closest('.pricing-code-row').remove();
+            } else {
+                // Если осталась одна строка, просто очищаем её
+                const $row = $(this).closest('.pricing-code-row');
+                $row.find('.pricing-code-input').val('');
+                $row.find('.pricing-code-description-input').val('').prop('readonly', false);
+                $row.find('.pricing-code-hint').text('');
+            }
+        });
     @endif
     });
 });