Просмотр исходного кода

added reclamation payment archive export

Alexander Musikhin 3 недель назад
Родитель
Сommit
3cb9797071

+ 32 - 0
app/Http/Controllers/ReclamationController.php

@@ -6,7 +6,9 @@ use App\Http\Requests\CreateReclamationRequest;
 use App\Http\Requests\StoreReclamationDetailsRequest;
 use App\Http\Requests\StoreReclamationRequest;
 use App\Http\Requests\StoreReclamationSparePartsRequest;
+use App\Jobs\ExportReclamationsJob;
 use App\Jobs\GenerateFilesPack;
+use App\Jobs\GenerateReclamationPaymentPack;
 use App\Jobs\GenerateReclamationPack;
 use App\Models\File;
 use App\Models\Order;
@@ -81,6 +83,29 @@ class ReclamationController extends Controller
         return view('reclamations.index', $this->data);
     }
 
+    public function export(Request $request)
+    {
+        $gp = session('gp_reclamations') ?? [];
+        $filterRequest = new Request($gp);
+
+        $model = new ReclamationView();
+        $this->createFilters($model, 'user_name', 'status_name');
+        $this->createDateFilters($model, 'create_date', 'finish_date');
+
+        $q = $model::query();
+        $this->acceptFilters($q, $filterRequest);
+        $this->acceptSearch($q, $filterRequest);
+        $this->setSortAndOrderBy($model, $filterRequest);
+        $q->orderBy($this->data['sortBy'], $this->data['orderBy']);
+
+        $reclamationIds = $q->pluck('id')->toArray();
+
+        ExportReclamationsJob::dispatch($reclamationIds, $request->user()->id);
+
+        return redirect()->route('reclamations.index', session('gp_reclamations'))
+            ->with(['success' => 'Задача экспорта рекламаций создана!']);
+    }
+
     public function create(CreateReclamationRequest $request, Order $order)
     {
         $reclamation = Reclamation::query()->create([
@@ -416,6 +441,13 @@ class ReclamationController extends Controller
             ->with(['success' => 'Задача генерации документов создана!']);
     }
 
+    public function generateReclamationPaymentPack(Request $request, Reclamation $reclamation)
+    {
+        GenerateReclamationPaymentPack::dispatch($reclamation, auth()->user()->id);
+        return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
+            ->with(['success' => 'Задача генерации пакета документов на оплату создана!']);
+    }
+
     public function generatePhotosBeforePack(Request $request, Reclamation $reclamation)
     {
         GenerateFilesPack::dispatch($reclamation, $reclamation->photos_before, auth()->user()->id, 'Фото проблемы');

+ 33 - 0
app/Jobs/ExportReclamationsJob.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Events\SendWebSocketMessageEvent;
+use App\Services\ExportReclamationsService;
+use Exception;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Queue\Queueable;
+use Illuminate\Support\Facades\Log;
+
+class ExportReclamationsJob implements ShouldQueue
+{
+    use Queueable;
+
+    public function __construct(
+        private readonly array $reclamationIds,
+        private readonly int $userId,
+    )
+    {}
+
+    public function handle(): void
+    {
+        try {
+            $link = (new ExportReclamationsService())->handle($this->reclamationIds, $this->userId);
+            Log::info('Export reclamations finished!');
+            event(new SendWebSocketMessageEvent('Экспорт рекламаций готов!', $this->userId, ['success' => true, 'link' => $link]));
+        } catch (Exception $e) {
+            Log::error('Export reclamations failed! ' . $e->getMessage());
+            event(new SendWebSocketMessageEvent('Ошибка экспорта рекламаций! ', $this->userId, ['error' => $e->getMessage()]));
+        }
+    }
+}

+ 33 - 0
app/Jobs/GenerateReclamationPaymentPack.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Events\SendWebSocketMessageEvent;
+use App\Models\Reclamation;
+use App\Services\GenerateDocumentsService;
+use Exception;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Queue\Queueable;
+use Illuminate\Support\Facades\Log;
+
+class GenerateReclamationPaymentPack implements ShouldQueue
+{
+    use Queueable;
+
+    public function __construct(
+        private readonly Reclamation $reclamation,
+        private readonly int $userId
+    ) {}
+
+    public function handle(): void
+    {
+        try {
+            $link = (new GenerateDocumentsService())->generateReclamationPaymentPack($this->reclamation, $this->userId);
+            Log::info('GenerateReclamationPaymentPack finished!');
+            event(new SendWebSocketMessageEvent('Пакет документов на оплату готов!', $this->userId, ['success' => true, 'link' => $link]));
+        } catch (Exception $e) {
+            Log::error('GenerateReclamationPaymentPack failed! ' . $e->getMessage());
+            event(new SendWebSocketMessageEvent('Ошибка создания пакета документов на оплату! ', $this->userId, ['error' => $e->getMessage()]));
+        }
+    }
+}

+ 352 - 0
app/Services/ExportReclamationsService.php

@@ -0,0 +1,352 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\File;
+use App\Models\PricingCode;
+use App\Models\Product;
+use App\Models\Reclamation;
+use App\Models\SparePart;
+use App\Helpers\DateHelper;
+use Illuminate\Support\Facades\Storage;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+class ExportReclamationsService
+{
+    private const TEMPLATE = './templates/ReclamationsExport.xlsx';
+    private const BASE_PRICING_CODE = '14.20-38-1';
+
+    public function handle(array $reclamationIds, int $userId): string
+    {
+        $reader = IOFactory::createReader('Xlsx');
+        $spreadsheet = $reader->load(self::TEMPLATE);
+        $sheet = $spreadsheet->getActiveSheet();
+        $templateLastRow = $sheet->getHighestRow();
+        $templateStyle = $sheet->getStyle('A2:X2');
+
+        $reclamations = Reclamation::query()
+            ->whereIn('id', $reclamationIds)
+            ->with([
+                'order.district',
+                'order.area',
+                'skus.product' => fn($q) => $q->withoutGlobalScopes(),
+                'spareParts.pricingCodes',
+            ])
+            ->get()
+            ->keyBy('id');
+
+        $row = 2;
+        foreach ($reclamationIds as $reclamationId) {
+            $reclamation = $reclamations->get($reclamationId);
+            if (!$reclamation) {
+                continue;
+            }
+
+            $base = $this->buildBaseRow($reclamation);
+
+            $groupRows = $this->buildGroupRows($reclamation, $base);
+            foreach ($groupRows as $group) {
+                $count = max(count($group), 1);
+                for ($i = 0; $i < $count; $i++) {
+                    $data = $group[$i] ?? $base;
+                    $sheet->duplicateStyle($templateStyle, 'A' . $row . ':X' . $row);
+                    $this->writeRow($sheet, $row, $data);
+                    $row++;
+                }
+            }
+        }
+
+        if ($row <= $templateLastRow) {
+            $sheet->removeRow($row, $templateLastRow - $row + 1);
+        }
+
+        $this->applyBorders($sheet, 1, $row - 1);
+        $this->applyDefaultStyles($sheet, 2, $row - 1);
+
+        $fileName = fileName('Рекламации ' . date('Y-m-d H-i-s') . '.xlsx');
+        $fd = 'export/reclamations';
+        Storage::disk('public')->makeDirectory($fd);
+        $fp = storage_path('app/public/' . $fd . '/') . $fileName;
+        Storage::disk('public')->delete($fd . '/' . $fileName);
+
+        (new Xlsx($spreadsheet))->save($fp);
+
+        $link = url('/storage/' . $fd . '/' . $fileName);
+
+        File::query()->create([
+            'link' => $link,
+            'path' => $fp,
+            'user_id' => $userId,
+            'original_name' => $fileName,
+            'mime_type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        ]);
+
+        return $link;
+    }
+
+    private function buildBaseRow(Reclamation $reclamation): array
+    {
+        $order = $reclamation->order;
+
+        return [
+            'A' => null, // № папки
+            'B' => $order?->district?->shortname ?? $order?->district_name ?? null, // Округ
+            'C' => $order?->area?->name ?? $order?->area_name ?? null, // Район
+            'D' => $order?->object_address ?? null, // Адрес
+            'E' => null, // Артикул
+            'F' => null, // Тип
+            'G' => null, // Наименование МАФ по паспорту
+            'H' => $reclamation->whats_done ?? null, // Что сделали
+            'I' => $reclamation->create_date ?? null, // Дата заявки
+            'J' => $order?->year ?? null, // Год поставки МАФ
+            'K' => $reclamation->reason ?? null, // Тип обращения
+            'L' => null, // Применяемая деталь
+            'M' => null, // Кол-во
+            'N' => null, // Цена по КС
+            'O' => null, // Лимит по МАФ
+            'P' => null, // Цена детали
+            'Q' => null, // Подтверждение цены
+            'R' => null, // Шифр 1
+            'S' => null, // Наименование 1
+            'T' => null, // Шифр 2
+            'U' => null, // Наименование 2
+            'V' => null, // Шифр 3
+            'W' => null, // Наименование 3
+            'X' => $reclamation->comment ?? null, // Комментарий
+        ];
+    }
+
+    private function buildGroupRows(Reclamation $reclamation, array $base): array
+    {
+        $skus = $reclamation->skus;
+        $spareParts = $reclamation->spareParts;
+        $details = $reclamation->details;
+
+        $groups = [];
+
+        if ($skus->isEmpty()) {
+            $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
+            $codes = [
+                [self::BASE_PRICING_CODE, $baseDescription],
+            ];
+            $groups[] = [$this->applyPricingCodes($this->buildRowForMaf($base, null), $codes)];
+            return $groups;
+        }
+
+        foreach ($skus as $idx => $sku) {
+            $mafRow = $this->buildRowForMaf($base, $sku);
+            if ($idx > 0) {
+                $this->blankColumns($mafRow, ['A', 'B', 'C', 'D']);
+            }
+            $detailRows = $this->buildDetailRows($mafRow, $spareParts, $details);
+            $groups[] = $detailRows;
+        }
+
+        return $groups;
+    }
+
+    private function buildRowForMaf(array $base, $sku): array
+    {
+        if (!$sku || !$sku->product) {
+            return $base;
+        }
+
+        $product = $sku->product;
+        $year = $sku->year ?? $product->year;
+
+        $passportName = null;
+        if ($product->article && $year) {
+            $productForYear = Product::withoutGlobalScopes()
+                ->where('year', $year)
+                ->where('article', $product->article)
+                ->first();
+            $passportName = $productForYear?->passport_name;
+        }
+
+        $row = $base;
+        $row['E'] = $product->article;
+        $row['F'] = $product->article;
+        $row['G'] = $passportName;
+
+        return $row;
+    }
+
+    private function buildDetailRows(array $mafRow, $spareParts, $details): array
+    {
+        $rows = [];
+
+        $items = [];
+        foreach ($spareParts as $sp) {
+            $items[] = ['type' => 'spare', 'data' => $sp];
+        }
+        foreach ($details as $detail) {
+            $items[] = ['type' => 'detail', 'data' => $detail];
+        }
+
+        if (empty($items)) {
+            $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
+            $codes = [
+                [self::BASE_PRICING_CODE, $baseDescription],
+            ];
+            $rows[] = $this->applyPricingCodes($mafRow, $codes);
+            return $rows;
+        }
+
+        foreach ($items as $index => $item) {
+            $row = $mafRow;
+            if ($index > 0) {
+                $this->blankColumns($row, ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'X']);
+            }
+            if ($item['type'] === 'spare') {
+                $rows[] = $this->buildRowsForSparePart($row, $item['data']);
+            } else {
+                $rows[] = $this->buildRowForDetail($row, $item['data']);
+            }
+        }
+
+        return $rows;
+    }
+
+    private function buildRowsForSparePart(array $mafRow, SparePart $sp): array
+    {
+        $row = $mafRow;
+        $row['L'] = $sp->article;
+        $row['M'] = $sp->pivot?->quantity ?? null;
+        $row['P'] = $sp->expertise_price;
+        $row['Q'] = $sp->tsn_number;
+
+        $codes = [];
+        $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
+        $codes[] = [self::BASE_PRICING_CODE, $baseDescription];
+
+        foreach ($sp->pricingCodes as $pc) {
+            if ($pc->type !== PricingCode::TYPE_PRICING_CODE) {
+                continue;
+            }
+            if ($pc->code === self::BASE_PRICING_CODE) {
+                continue;
+            }
+            $codes[] = [$pc->code, $pc->description];
+        }
+
+        return $this->applyPricingCodes($row, $codes);
+    }
+
+    private function buildRowForDetail(array $mafRow, $detail): array
+    {
+        $row = $mafRow;
+        $row['L'] = $detail->name;
+        $row['M'] = $detail->quantity;
+        $row['P'] = null;
+        $row['Q'] = null;
+
+        $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
+        $codes = [
+            [self::BASE_PRICING_CODE, $baseDescription],
+        ];
+
+        return $this->applyPricingCodes($row, $codes);
+    }
+
+    private function applyPricingCodes(array $row, array $codes): array
+    {
+        $slots = [
+            ['R', 'S'],
+            ['T', 'U'],
+            ['V', 'W'],
+        ];
+
+        for ($i = 0; $i < count($slots); $i++) {
+            $row[$slots[$i][0]] = null;
+            $row[$slots[$i][1]] = null;
+        }
+
+        $idx = 0;
+        foreach ($codes as $code) {
+            if ($idx >= count($slots)) {
+                break;
+            }
+            $row[$slots[$idx][0]] = $code[0] ?? null;
+            $row[$slots[$idx][1]] = $code[1] ?? null;
+            $idx++;
+        }
+
+        return $row;
+    }
+
+    private function blankColumns(array &$row, array $cols): void
+    {
+        foreach ($cols as $col) {
+            $row[$col] = null;
+        }
+    }
+
+    private function writeRow(Worksheet $sheet, int $row, array $data): void
+    {
+        $rowRange = 'A' . $row . ':X' . $row;
+        $sheet->getStyle($rowRange)->getAlignment()->setWrapText(true);
+        $sheet->getStyle($rowRange)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+        $sheet->getStyle($rowRange)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
+
+        foreach ($data as $col => $value) {
+            if ($col === 'I') {
+                if (!empty($value) && DateHelper::isDate($value)) {
+                    $excelDate = DateHelper::ISODateToExcelDate($value);
+                    $sheet->setCellValue('I' . $row, $excelDate);
+                    $sheet->getStyle('I' . $row)->getNumberFormat()->setFormatCode('m/d/yyyy');
+                } else {
+                    $sheet->setCellValueExplicit('I' . $row, (string)$value, DataType::TYPE_STRING);
+                    $sheet->getStyle('I' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT);
+                }
+                continue;
+            }
+
+            if ($col === 'P') {
+                if(isset($value) && is_numeric($value)) {
+                    $sheet->setCellValue('P' . $row, (float)$value);
+                }
+                $sheet->getStyle('P' . $row)->getNumberFormat()->setFormatCode('#,##0.00\\₽');
+                continue;
+            }
+
+            if ($value === null || $value === '') {
+                $sheet->setCellValue($col . $row, null);
+                continue;
+            }
+
+            $sheet->setCellValueExplicit($col . $row, (string)$value, DataType::TYPE_STRING);
+            $sheet->getStyle($col . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT);
+        }
+    }
+
+    private function applyBorders(Worksheet $sheet, int $fromRow, int $toRow): void
+    {
+        $highestCol = $sheet->getHighestColumn();
+        $range = 'A' . $fromRow . ':' . $highestCol . $toRow;
+        $sheet->getStyle($range)
+            ->getBorders()
+            ->getAllBorders()
+            ->setBorderStyle(Border::BORDER_THIN)
+            ->setColor(new Color('777777'));
+    }
+
+    private function applyDefaultStyles(Worksheet $sheet, int $fromRow, int $toRow): void
+    {
+        if ($toRow < $fromRow) {
+            return;
+        }
+
+        $highestCol = $sheet->getHighestColumn();
+        $range = 'A' . $fromRow . ':' . $highestCol . $toRow;
+        $sheet->getStyle($range)->getAlignment()->setWrapText(true);
+        $sheet->getStyle($range)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+        $sheet->getStyle($range)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
+    }
+}

+ 110 - 0
app/Services/GenerateDocumentsService.php

@@ -376,6 +376,54 @@ class GenerateDocumentsService
         return $fileModel?->link ?? '';
     }
 
+    /**
+     * @throws Exception
+     */
+    public function generateReclamationPaymentPack(Reclamation $reclamation, int $userId): string
+    {
+        $reclamation->loadMissing([
+            'order.statements',
+            'documents',
+            'acts',
+            'photos_before',
+            'photos_after',
+        ]);
+
+        $tmpRoot = 'reclamations/' . $reclamation->id . '/tmp';
+        $baseDir = $tmpRoot . '/Пакет документов на оплату - ' . fileName($reclamation->order->object_address);
+        $beforeDir = $baseDir . '/Фотографии проблемы';
+        $afterDir = $baseDir . '/Фотографии после устранения';
+
+        Storage::disk('public')->makeDirectory($beforeDir);
+        Storage::disk('public')->makeDirectory($afterDir);
+
+        $this->copyPhotosWithEmptyFallback($reclamation->photos_before, $beforeDir);
+        $this->copyPhotosWithEmptyFallback($reclamation->photos_after, $afterDir);
+
+        foreach ($reclamation->documents as $document) {
+            if ($this->shouldSkipDocumentForPaymentPack($document)) {
+                continue;
+            }
+            $this->copyFileToDir($document->path, $baseDir, $document->original_name);
+        }
+
+        foreach ($reclamation->acts as $act) {
+            $this->copyFileToDir($act->path, $baseDir, $act->original_name);
+        }
+
+        foreach ($reclamation->order?->statements ?? [] as $statement) {
+            $this->copyFileToDir($statement->path, $baseDir, $statement->original_name);
+        }
+
+        $archiveName = 'Пакет документов на оплату - ' . fileName($reclamation->order->object_address) . '.zip';
+        $fileModel = (new FileService())->createZipArchive($tmpRoot, $archiveName, $userId);
+
+        Storage::disk('public')->deleteDirectory($tmpRoot);
+        $reclamation->documents()->syncWithoutDetaching($fileModel);
+
+        return $fileModel?->link ?? '';
+    }
+
     /**
      * @throws Exception
      */
@@ -534,6 +582,68 @@ class GenerateDocumentsService
         return $fileModel;
     }
 
+    private function copyPhotosWithEmptyFallback(Collection $photos, string $targetDir): void
+    {
+        if ($photos->isEmpty()) {
+            Storage::disk('public')->put(
+                $targetDir . '/empty.txt',
+                'Данный файл создан для возможности создания пустой папки в архиве'
+            );
+            return;
+        }
+
+        foreach ($photos as $photo) {
+            $this->copyFileToDir($photo->path, $targetDir, $photo->original_name);
+        }
+    }
+
+    private function shouldSkipDocumentForPaymentPack($file): bool
+    {
+        if (!$file) {
+            return true;
+        }
+
+        if ($file->mime_type === 'application/zip') {
+            return true;
+        }
+
+        if (Str::endsWith((string)$file->original_name, '.zip')) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function copyFileToDir(string $fromPath, string $targetDir, string $originalName): void
+    {
+        if (!Storage::disk('public')->exists($fromPath)) {
+            return;
+        }
+
+        $safeName = $this->uniqueFileName($targetDir, $originalName);
+        $to = $targetDir . '/' . $safeName;
+
+        if (!Storage::disk('public')->exists($to)) {
+            Storage::disk('public')->copy($fromPath, $to);
+        }
+    }
+
+    private function uniqueFileName(string $dir, string $name): string
+    {
+        $base = pathinfo($name, PATHINFO_FILENAME);
+        $ext = pathinfo($name, PATHINFO_EXTENSION);
+        $extPart = $ext ? '.' . $ext : '';
+
+        $candidate = $base . $extPart;
+        $counter = 1;
+        while (Storage::disk('public')->exists($dir . '/' . $candidate)) {
+            $candidate = $base . ' (' . $counter . ')' . $extPart;
+            $counter++;
+        }
+
+        return $candidate;
+    }
+
     public function generateTtnPack(Ttn $ttn, int $userId): string
     {
         $skus = ProductSKU::query()->whereIn('id', json_decode($ttn->skus))->get();

+ 4 - 2
resources/views/reclamations/edit.blade.php

@@ -14,10 +14,12 @@
                 @if(hasRole('admin,manager'))
                     <a href="{{ route('order.generate-reclamation-pack', $reclamation) }}"
                        class="btn btn-primary btn-sm">Пакет документов рекламации</a>
+                    <a href="{{ route('reclamation.generate-reclamation-payment-pack', $reclamation) }}"
+                       class="btn btn-primary btn-sm">Пакет документов на оплату</a>
                 @endif
                 @if(hasRole('admin'))
                     <a href="#" onclick="if(confirm('Удалить рекламацию?')) $('form#destroy').submit();"
-                       class="btn btn-sm mb-1 btn-danger">Удалить</a>
+                       class="btn btn-sm btn-danger">Удалить</a>
                     <form action="{{ route('reclamations.delete', $reclamation) }}" method="post" class="d-none" id="destroy">
                         @csrf
                         @method('DELETE')
@@ -799,4 +801,4 @@
             $(this).closest('.spare-part-row').remove();
         });
     </script>
-@endpush
+@endpush

+ 8 - 1
resources/views/reclamations/index.blade.php

@@ -7,11 +7,18 @@
             <h3>Рекламации</h3>
         </div>
         <div class="col-6 text-end">
-
+            @if(hasRole('admin'))
+                <a href="#" class="btn btn-sm btn-primary" onclick="$('#export-reclamations').submit()">Экспорт</a>
+            @endif
 
         </div>
     </div>
 
+    @if(hasRole('admin'))
+        <form class="d-none" method="post" action="{{ route('reclamations.export') }}" id="export-reclamations">
+            @csrf
+        </form>
+    @endif
 
     @include('partials.table', [
         'id'        => $id,

+ 3 - 1
routes/web.php

@@ -60,7 +60,7 @@ Route::middleware('auth:web')->group(function () {
             Route::get('create', [UserController::class, 'create'])->name('user.create');
             Route::get('{user}', [UserController::class, 'show'])->name('user.show');
             Route::post('', [UserController::class, 'store'])->name('user.store');
-            Route::put('{user}', [UserController::class, 'update'])->name('user.update');
+//            Route::put('{user}', [UserController::class, 'update'])->name('user.update');
             Route::delete('{user}', [UserController::class, 'destroy'])->name('user.destroy');
             Route::post('undelete/{user}', [UserController::class, 'undelete'])->name('user.undelete');
         });
@@ -158,6 +158,7 @@ Route::middleware('auth:web')->group(function () {
         Route::get('order/generate-installation-pack/{order}', [OrderController::class, 'generateInstallationPack'])->name('order.generate-installation-pack');
         Route::get('order/generate-handover-pack/{order}', [OrderController::class, 'generateHandoverPack'])->name('order.generate-handover-pack');
         Route::get('reclamation/generate-reclamation-pack/{reclamation}', [ReclamationController::class, 'generateReclamationPack'])->name('order.generate-reclamation-pack');
+        Route::get('reclamation/generate-reclamation-payment-pack/{reclamation}', [ReclamationController::class, 'generateReclamationPaymentPack'])->name('reclamation.generate-reclamation-payment-pack');
         Route::get('reclamation/generate-photos-before-pack/{reclamation}', [ReclamationController::class, 'generatePhotosBeforePack'])->name('reclamation.generate-photos-before-pack');
         Route::get('reclamation/generate-photos-after-pack/{reclamation}', [ReclamationController::class, 'generatePhotosAfterPack'])->name('reclamation.generate-photos-after-pack');
 
@@ -196,6 +197,7 @@ Route::middleware('auth:web')->group(function () {
         Route::delete('catalog/{product}', [ProductController::class, 'delete'])->name('catalog.delete');
 
         Route::post('catalog-export', [ProductController::class, 'export'])->name('catalog.export');
+        Route::post('reclamations/export', [ReclamationController::class, 'export'])->name('reclamations.export');
 
         Route::post('mafs-import', [ProductSKUController::class, 'importMaf'])->name('mafs.import');
         Route::post('mafs-export', [ProductSKUController::class, 'exportMaf'])->name('mafs.export');

BIN
templates/ReclamationsExport.xlsx