Răsfoiți Sursa

Orders import

Alexander Musikhin 3 săptămâni în urmă
părinte
comite
0f28b19899

+ 3 - 0
.env.example

@@ -55,3 +55,6 @@ WORDS_IN_TABLE_CELL_LIMIT=15
 FCM_PROJECT_ID=
 FCM_CLIENT_EMAIL=
 FCM_PRIVATE_KEY=
+
+# id Artemenko Denis
+APP_DEFAULT_MAF_ORDER_USER_ID=1

+ 8 - 0
app/Helpers/DateHelper.php

@@ -93,4 +93,12 @@ class DateHelper
         return $date->format('Y-m-d');
     }
 
+    public static function excelDateToISODate(int $excelDate): string
+    {
+        $ts = ($excelDate - 25569) * 86400;
+        $date = new DateTime();
+        $date->setTimestamp($ts);
+        return $date->format('Y-m-d');
+    }
+
 }

+ 1 - 1
app/Http/Controllers/Controller.php

@@ -114,7 +114,7 @@ class Controller extends BaseController
         }
 
         // set order
-        $this->data['orderBy'] = (empty($request->order)) ? 'desc' : 'asc';
+        $this->data['orderBy'] = (empty($request->order)) ? 'asc' : 'desc';
     }
 
     /**

+ 5 - 2
app/Http/Controllers/FilterController.php

@@ -14,6 +14,7 @@ class FilterController extends Controller
         'products'      => 'products',
         'reclamations'  => 'reclamations_view',
         'maf_order'     => 'maf_orders_view',
+        'import'       => 'imports',
     ];
     public function getFilters(FilterRequest $request)
     {
@@ -27,10 +28,12 @@ class FilterController extends Controller
 
         if (Schema::hasColumn($dbTable, $column)) {
             $q = DB::table($dbTable)->select($column)->distinct();
-            if($table !== 'reclamations') {
+            if(!in_array($table, ['reclamations', 'import'])) {
                 $q->where('year' , year());
             }
-            $q->whereNull('deleted_at');
+            if (Schema::hasColumn($dbTable, 'deleted_at')) {
+                $q->whereNull('deleted_at');
+            }
             $result = $q->orderBy($column)->get()->pluck($column)->toArray();
         } else {
             $result = [];

+ 87 - 0
app/Http/Controllers/ImportController.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Jobs\Import\ImportJob;
+use App\Models\Import;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+
+class ImportController extends Controller
+{
+    protected array $data = [
+        'active'    => 'import',
+        'title'     => 'Импорт',
+        'id'        => 'import',
+        'header'    => [
+            'id'                        => 'ID',
+            'type'                      => 'Тип',
+            'status'                    => 'Статус',
+            'created_at'                => 'Создано',
+            'updated_at'                => 'Изменено',
+        ],
+        'searchFields' => [
+            'id',
+        ],
+    ];
+
+    /**
+     * Display a listing of the resource.
+     */
+    public function index(Request $request)
+    {
+        session(['gp_import' => $request->all()]);
+        $model = new Import;
+        // fill filters
+        $this->data['ranges'] = [];
+        $this->createFilters($model, 'type', 'status');
+        $this->createDateFilters($model, 'created_at', 'updated_at');
+
+        $q = $model::query();
+        $q->withoutGlobalScopes();
+
+        $this->acceptFilters($q, $request);
+        $this->acceptSearch($q, $request);
+        $this->setSortAndOrderBy($model, $request);
+
+        $q->orderBy($this->data['sortBy'], $this->data['orderBy']);
+        $this->data['imports'] = $q->paginate(session('per_page', config('pagination.per_page')))->withQueryString();
+
+        return view('import.index', $this->data);
+    }
+
+    public function store(Request $request)
+    {
+        // validate data
+        $request->validate([
+            'type'        => 'required|in:orders,reclamations',
+            'import_file' => 'required|file',
+        ]);
+
+        // load and save file
+        $path = Str::random(2) . '/' . Str::uuid() . '.' .$request->file('import_file')->getClientOriginalExtension();
+        Storage::disk('upload')->put($path, $request->file('import_file')->getContent());
+
+        $import = Import::query()->create([
+            'type' => $request->type,
+            'status' => 'new',
+            'filename' => $path,
+        ]);
+        $import->refresh();
+
+        ImportJob::dispatch($import, $request->user()->id);
+
+        Log::info('Import job ' . $request->type . ' created!');
+        return redirect()->route('import.index', session('gp_import'))->with(['success' => 'Задача импорта успешно создана!']);
+
+    }
+
+    public function show(Import $import)
+    {
+        $this->data['import'] = $import;
+        return view('import.show', $this->data);
+    }
+
+}

+ 48 - 0
app/Jobs/Import/ImportJob.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Jobs\Import;
+
+use App\Events\SendWebSocketMessageEvent;
+use App\Models\Import;
+use App\Services\ImportOrdersService;
+use App\Services\ImportReclamationsService;
+use Exception;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Queue\Queueable;
+use Illuminate\Support\Facades\Log;
+
+class ImportJob implements ShouldQueue
+{
+    use Queueable;
+
+    /**
+     * Create a new job instance.
+     */
+    public function __construct(
+        private readonly Import $import,
+        private readonly int $userId,
+    )
+    {}
+
+    /**
+     * Execute the job.
+     */
+    public function handle(): void
+    {
+        try {
+            switch($this->import->type) {
+                case 'orders':
+                    (new ImportOrdersService($this->import))->handle();
+                    break;
+                case 'reclamations':
+                    (new ImportReclamationsService($this->import))->handle();
+                    break;
+            }
+            Log::info('Import ' . $this->import->type. ' job done!');
+            event(new SendWebSocketMessageEvent('Импорт завершён!', $this->userId, ['path' => $this->import->filename, 'type' => $this->import->type]));
+        } catch (Exception $e) {
+            Log::info('Import ' . $this->import->type. ' job failed! ERROR: ' . $e->getMessage());
+            event(new SendWebSocketMessageEvent('Ошибка импорта! ' . $e->getMessage(), $this->userId, ['error' => $e->getMessage()]));
+        }
+    }
+}

+ 25 - 0
app/Models/Import.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Import extends Model
+{
+    const DEFAULT_SORT_BY = 'created_at';
+
+    protected $fillable = [
+        'type',
+        'filename',
+        'status',
+        'result',
+    ];
+
+    public function log(string $message, string $level = 'INFO'): string
+    {
+        $d = date('Y-m-d H:i:s', strtotime('now'));
+        $string = "$d\t$level:\t$message\n";
+        $this->result = $this->result . $string;
+        return $string;
+    }
+}

+ 72 - 0
app/Services/ImportBaseService.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Import;
+use App\Models\Product;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
+use PhpOffice\PhpSpreadsheet\Worksheet\RowIterator;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+class ImportBaseService
+{
+    protected array $headers = [];
+    protected Worksheet $sheet;
+    protected RowIterator $rowIterator;
+
+    public function __construct(protected readonly Import $import)
+    {}
+
+    public function readFile(): void
+    {
+        $path = Storage::disk('upload')->path($this->import->filename);
+        $reader = new Xlsx();
+        $spreadsheet = $reader->load($path);
+        $this->sheet = $spreadsheet->getActiveSheet();
+        $this->rowIterator = $this->sheet->getRowIterator();
+    }
+
+    protected function rowToArray($row): array
+    {
+        $cellIterator = $row->getCellIterator();
+        $cellIterator->setIterateOnlyExistingCells(FALSE);
+        $row_content = [];
+        $keys = array_values($this->headers);
+        $i = 0;
+        foreach ($cellIterator as $cell) {
+            $row_content[$keys[$i++]] = $cell->getValue();
+        }
+
+        return $row_content;
+    }
+
+    /**
+     * @param array $headers
+     * @return bool
+     */
+    protected function checkHeaders(array $headers): bool
+    {
+        foreach ($headers as $k => $header) {
+            $headers[$k] = Str::before($header, "\n");
+        }
+
+        return $headers == array_keys($this->headers);
+    }
+
+    protected function findId(string $tableNameCol, string $value): string
+    {
+        list($table, $column) = explode('.', $tableNameCol);
+        $model = DB::table($table)
+            ->where($column, $value)
+            ->first();
+        if (!$model) {
+            echo $this->import->log("SKIP: no {$tableNameCol} {$value} found!", 'WARNING');
+        }
+        return $model?->id ?? '';
+    }
+
+
+}

+ 264 - 0
app/Services/ImportOrdersService.php

@@ -0,0 +1,264 @@
+<?php
+
+namespace App\Services;
+
+use App\Helpers\DateHelper;
+use App\Models\Import;
+use App\Models\MafOrder;
+use App\Models\Order;
+use App\Models\Product;
+use App\Models\ProductSKU;
+use Exception;
+
+class ImportOrdersService extends ImportBaseService
+{
+    const HEADERS = [
+        'Округ'                         => 'districts.name',
+        'Район'                         => 'areas.name',
+        'Название площадки'             => 'orders.name',
+        'Адрес объекта'                 => 'orders.object_address',
+        'Тип объекта'                   => 'object_types.name',
+        'Артикул'                       => 'products.article',
+        'Номер номенклатуры'            => 'products.nomenclature_number',
+        'Габаритные размеры'            => 'products.sizes',
+        'RFID'                          => 'products_sku.rfid',
+        'Наименование производителя'    => 'products.manufacturer_name',
+        'Наименование по ТЗ'            => 'products.name_tz',
+        'Тип по тз'                     => 'products.type_tz',
+        'ТИП'                           => 'products.type',
+        'Цена товара'                   => 'products.product_price',
+        'Цена установки'                => 'products.installation_price',
+        'Итоговая цена'                 => 'products.total_price',
+        'Примечание'                    => 'products.note',
+        'Год установки'                 => 'orders.year',
+        'Номер заказа МАФ'              => 'maf_orders.order_number',
+        '№ Ведомости'                   => 'products_sku.statement_number',
+        'Дата ведомости'                => 'products_sku.statement_date',
+        '№УПД'                          => 'products_sku.upd_number',
+        'Статус'                        => 'order_statuses.name',
+        'Комментарий'                   => 'orders.comment',
+        'Менеджер'                      => 'users.name',
+        'Номер фабрики'                 => 'products_sku.factory_number',
+        'Дата пр-ва'                    => 'products_sku.manufacture_date',
+        'Срок эксплуатации (месяцев)'   => 'products.service_life',
+        '№ сертификата'                 => 'products.certificate_number',
+        'Дата выдачи'                   => 'products.certificate_date',
+        'Орган сертификации'            => 'products.certificate_issuer',
+        'ТИП (Декларация/Сертификат/Отказное)' => 'products.certificate_type',
+
+        // заголовки из файла для проверки и маппинга
+    ];
+
+
+    public function __construct(Import $import)
+    {
+        parent::__construct($import);
+        $this->headers = self::HEADERS;
+    }
+
+    public function handle(): bool
+    {
+        try {
+            $this->import->log('Reading file...');
+            $this->readFile();
+        } catch (Exception $exception){
+            $this->import->log($exception->getMessage(), 'ERROR');
+            $this->import->status = 'ERROR';
+            $this->import->save();
+            return false;
+        }
+
+        try {
+            $this->import->log('Checking headers...');
+            $headers = $this->rowToArray($this->rowIterator->current());
+            if (!$this->checkHeaders(array_values($headers))) {
+                throw new Exception("Invalid headers");
+            }
+        } catch (Exception $exception){
+            $this->import->log($exception->getMessage(), 'ERROR');
+            $this->import->status = 'ERROR';
+            $this->import->save();
+            return false;
+        }
+        $this->rowIterator->next();
+        $strNumber = 0;
+        $result = [
+            'productsCreated' => 0,
+            'ordersCreated' => 0,
+            'mafOrdersCreated' => 0,
+            'productsSkuCreated' => 0,
+        ];
+        foreach($this->rowIterator as $row){
+            $strNumber++;
+            $r = $this->rowToArray($row);
+            if($strNumber === 1) {
+                echo $this->import->log('Skip headers Row: ' . $strNumber);
+                continue;
+            }
+            echo $this->import->log("Row $strNumber: " . $r['orders.object_address']);
+            $year = (int) $r['orders.year'];
+
+            // округ
+            if(!($districtId = $this->findId('districts.shortname', $r['districts.name']))) {
+                continue;
+            }
+
+            // район
+            if(!($areaId = $this->findId('areas.name', $r['areas.name']))) {
+                continue;
+            }
+
+            // manager
+            if(!($userId = $this->findId('users.name', $r['users.name']))) {
+                continue;
+            }
+
+            // object_type
+            if(!($objectTypeId = $this->findId('object_types.name', $r['object_types.name']))) {
+                continue;
+            }
+
+            // order_statuses.name
+            if(!($orderStatusId = $this->findId('order_statuses.name', $r['order_statuses.name']))) {
+                continue;
+            }
+
+
+            // product
+            $product = Product::query()
+                ->where('year', $year)
+                ->where('nomenclature_number', $r['products.nomenclature_number'])
+                ->first();
+            if(!$product) {
+                $product = Product::query()
+                    ->create([
+                        'year' => $year,
+                        'article' => $r['products.article'],
+                        'nomenclature_number' => $r['products.nomenclature_number'],
+                        'manufacturer_name' => $r['products.manufacturer_name'],
+                        'name_tz' => $r['products.name_tz'],
+                        'type' => $r['products.type'],
+                        'type_tz' => $r['products.type_tz'],
+                        'product_price' => $r['products.product_price'],
+                        'installation_price' => $r['products.installation_price'],
+                        'total_price' => $r['products.total_price'],
+                        'note' => $r['products.note'],
+                        'service_life' => $r['products.service_life'],
+                        'certificate_number' => $r['products.certificate_number'],
+                        'certificate_date' => is_int($r['products.certificate_date']) ? DateHelper::excelDateToISODate($r['products.certificate_date']) : null,
+                        'certificate_issuer' => $r['products.certificate_issuer'],
+                        'certificate_type' => $r['products.certificate_type'],
+                        'sizes' => $r['products.sizes'],
+                        'unit' => 'шт.',
+                        'passport_name' => $r['products.manufacturer_name'],
+                        'statement_name' => $r['products.name_tz'],
+                        'manufacturer' => 'Наш двор',
+                    ]);
+                echo $this->import->log('Created product: ' . $product->name_tz);
+                $result['productsCreated'] += 1;
+            } else {
+                echo $this->import->log('Found product: ' . $product->name_tz);
+            }
+
+            // order
+            $order = Order::query()
+                ->where('year', $year)
+                ->where('object_address', $r['orders.object_address'])
+                ->first();
+            if(!$order) {
+                $order = Order::query()
+                    ->create([
+                        'year' => $year,
+                        'name' => $r['orders.object_address'],
+                        'user_id' => $userId,
+                        'district_id' => $districtId,
+                        'area_id' => $areaId,
+                        'object_address' => $r['orders.object_address'],
+                        'object_type_id' => $objectTypeId,
+                        'comment' => $r['orders.comment'],
+                        'order_status_id' => $orderStatusId,
+                        'tg_group_name' =>  '/' . $r['districts.name'] . ' ' . $r['orders.object_address'] . ' (' . $r['areas.name'] . ')' . ' - ' . $r['users.name'],
+                        'tg_group_link' => '',
+                    ]);
+                echo $this->import->log('Created order: ' . $order->object_address);
+                $result['ordersCreated'] += 1;
+            } else {
+                echo $this->import->log('Found order: ' . $order->object_address);
+            }
+
+            // maf order
+            if ($r['maf_orders.order_number'] != '') {
+                $mafOrder = MafOrder::query()
+                    ->where('year', $year)
+                    ->where('product_id', $product->id)
+                    ->where('order_number', $r['maf_orders.order_number'])
+                    ->first();
+                if(!$mafOrder) {
+                    $mafOrder = MafOrder::query()
+                        ->create([
+                            'year' => $year,
+                            'order_number' => $r['maf_orders.order_number'],
+                            'user_id' => config('app.default_maf_order_user_id'),
+                            'status' => 'на складе',
+                            'product_id' => $product->id,
+                            'quantity' => 1,
+                            'in_stock' => 0,
+                        ]);
+                    echo $this->import->log('Created maf order: ' . $mafOrder->order_number);
+                    $result['mafOrdersCreated'] += 1;
+                } else {
+                    echo $this->import->log('Found maf order: ' . $mafOrder->order_number);
+                    $mafOrder->quantity += 1;
+                    $mafOrder->save();
+                    echo $this->import->log('Incremented maf order: ' . $mafOrder->quantity);
+                }
+            } else {
+                $mafOrder = null;
+            }
+
+            // search rfid in products_sku
+            $manufacture_date = is_int($r['products_sku.manufacture_date']) ? DateHelper::excelDateToISODate($r['products_sku.manufacture_date']) : null;
+            $statement_date = is_int($r['products_sku.statement_date']) ? DateHelper::excelDateToISODate($r['products_sku.statement_date']) : null;
+
+            $productSKU = ProductSKU::query()
+                ->where('year', $year)
+                ->where('rfid', $r['products_sku.rfid'])
+                ->where('product_id', $product->id)
+                ->where('order_id', $order->id)
+                ->where('maf_order_id', $mafOrder?->id)
+                ->where('factory_number', $r['products_sku.factory_number'])
+                ->where('statement_number', $r['products_sku.statement_number'])
+                ->where('upd_number', $r['products_sku.upd_number'])
+                ->where('statement_date', $statement_date)
+                ->where('manufacture_date', $manufacture_date)
+                ->first();
+//            dd($productSKU->toRawSql());
+            if($productSKU) {
+                echo $this->import->log('Found product with rfid: ' . $productSKU->rfid . ' Skip.');
+                continue;
+            } else {
+                $productSKU = ProductSKU::query()->create([
+                    'year' => $year,
+                    'product_id' => $product->id,
+                    'order_id' => $order->id,
+                    'maf_order_id' => $mafOrder?->id,
+                    'status' => ($mafOrder?->id) ? 'отгружен' : 'требуется',
+                    'rfid' => $r['products_sku.rfid'],
+                    'factory_number' => $r['products_sku.factory_number'],
+                    'manufacture_date' => $manufacture_date,
+                    'statement_number' => $r['products_sku.statement_number'],
+                    'statement_date' => $statement_date,
+                    'upd_number' => $r['products_sku.upd_number'],
+                ]);
+                $result['productsSkuCreated'] += 1;
+                echo $this->import->log('Created product sku: ' . $productSKU->id);
+            }
+
+        }
+        echo $this->import->log(print_r($result, true));
+        $this->import->status = 'DONE';
+        $this->import->save();
+
+        return true;
+    }
+}

+ 15 - 0
app/Services/ImportReclamationsService.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Import;
+
+class ImportReclamationsService extends ImportBaseService
+{
+
+    public function handle()
+    {
+
+    }
+
+}

+ 2 - 0
config/app.php

@@ -127,4 +127,6 @@ return [
         'store' => env('APP_MAINTENANCE_STORE', 'database'),
     ],
 
+    'default_maf_order_user_id' => env('APP_DEFAULT_MAF_ORDER_USER_ID', 1),
+
 ];

+ 31 - 0
database/migrations/2025_11_06_204349_create_imports_table.php

@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('imports', function (Blueprint $table) {
+            $table->id();
+            $table->string('type'); // orders, reclamations
+            $table->string('filename');
+            $table->string('status'); // new, in_progress, done
+            $table->mediumText('result')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('imports');
+    }
+};

+ 10 - 10
database/seeders/AreaSeeder.php

@@ -31,8 +31,8 @@ class AreaSeeder extends Seeder
         Area::updateOrCreate(['name' => 'Бескудниковский'], ['district_id' => '2']);
         Area::updateOrCreate(['name' => 'Войковский'], ['district_id' => '2']);
         Area::updateOrCreate(['name' => 'Головинский'], ['district_id' => '2']);
-        Area::updateOrCreate(['name' => 'Дегунино Восточное'], ['district_id' => '2']);
-        Area::updateOrCreate(['name' => 'Дегунино Западное'], ['district_id' => '2']);
+        Area::updateOrCreate(['name' => 'Восточное Дегунино'], ['district_id' => '2']);
+        Area::updateOrCreate(['name' => 'Западное Дегунино'], ['district_id' => '2']);
         Area::updateOrCreate(['name' => 'Дмитровский'], ['district_id' => '2']);
         Area::updateOrCreate(['name' => 'Коптево'], ['district_id' => '2']);
         Area::updateOrCreate(['name' => 'Левобережный'], ['district_id' => '2']);
@@ -53,8 +53,8 @@ class AreaSeeder extends Seeder
         Area::updateOrCreate(['name' => 'Лосиноостровский'], ['district_id' => '3']);
         Area::updateOrCreate(['name' => 'Марфино'], ['district_id' => '3']);
         Area::updateOrCreate(['name' => 'Марьина роща'], ['district_id' => '3']);
-        Area::updateOrCreate(['name' => 'Медведково Северное'], ['district_id' => '3']);
-        Area::updateOrCreate(['name' => 'Медведково Южное'], ['district_id' => '3']);
+        Area::updateOrCreate(['name' => 'Северное Медведково'], ['district_id' => '3']);
+        Area::updateOrCreate(['name' => 'Южное Медведково'], ['district_id' => '3']);
         Area::updateOrCreate(['name' => 'Останкинский'], ['district_id' => '3']);
         Area::updateOrCreate(['name' => 'Отрадное'], ['district_id' => '3']);
         Area::updateOrCreate(['name' => 'Ростокино'], ['district_id' => '3']);
@@ -68,9 +68,9 @@ class AreaSeeder extends Seeder
         Area::updateOrCreate(['name' => 'Восточный'], ['district_id' => '4']);
         Area::updateOrCreate(['name' => 'Гольяново'], ['district_id' => '4']);
         Area::updateOrCreate(['name' => 'Ивановское'], ['district_id' => '4']);
-        Area::updateOrCreate(['name' => 'Измайлово Восточное'], ['district_id' => '4']);
+        Area::updateOrCreate(['name' => 'Восточное Измайлово'], ['district_id' => '4']);
         Area::updateOrCreate(['name' => 'Измайлово'], ['district_id' => '4']);
-        Area::updateOrCreate(['name' => 'Измайлово Северное'], ['district_id' => '4']);
+        Area::updateOrCreate(['name' => 'Северное Измайлово'], ['district_id' => '4']);
         Area::updateOrCreate(['name' => 'Косино-Ухтомский'], ['district_id' => '4']);
         Area::updateOrCreate(['name' => 'Метрогородок'], ['district_id' => '4']);
         Area::updateOrCreate(['name' => 'Новогиреево'], ['district_id' => '4']);
@@ -114,8 +114,8 @@ class AreaSeeder extends Seeder
 
         // ЮЗАО
         Area::updateOrCreate(['name' => 'Академический'], ['district_id' => '7']);
-        Area::updateOrCreate(['name' => 'Бутово Северное'], ['district_id' => '7']);
-        Area::updateOrCreate(['name' => 'Бутово Южное'], ['district_id' => '7']);
+        Area::updateOrCreate(['name' => 'Северное Бутово'], ['district_id' => '7']);
+        Area::updateOrCreate(['name' => 'Южное Бутово'], ['district_id' => '7']);
         Area::updateOrCreate(['name' => 'Гагаринский'], ['district_id' => '7']);
         Area::updateOrCreate(['name' => 'Зюзино'], ['district_id' => '7']);
         Area::updateOrCreate(['name' => 'Коньково'], ['district_id' => '7']);
@@ -146,8 +146,8 @@ class AreaSeeder extends Seeder
         Area::updateOrCreate(['name' => 'Митино'], ['district_id' => '9']);
         Area::updateOrCreate(['name' => 'Покровское-Стрешнево'], ['district_id' => '9']);
         Area::updateOrCreate(['name' => 'Строгино'], ['district_id' => '9']);
-        Area::updateOrCreate(['name' => 'Тушино Северное'], ['district_id' => '9']);
-        Area::updateOrCreate(['name' => 'Тушино Южное'], ['district_id' => '9']);
+        Area::updateOrCreate(['name' => 'Северное Тушино'], ['district_id' => '9']);
+        Area::updateOrCreate(['name' => 'Южное Тушино'], ['district_id' => '9']);
         Area::updateOrCreate(['name' => 'Хорошёво-Мневники'], ['district_id' => '9']);
         Area::updateOrCreate(['name' => 'Щукино'], ['district_id' => '9']);
 

+ 1 - 1
resources/views/catalog/index.blade.php

@@ -40,7 +40,7 @@
         <div class="modal-dialog modal-fullscreen-sm-down">
             <div class="modal-content">
                 <div class="modal-header">
-                    <h1 class="modal-title fs-5" id="exampleModalLabel">Выберите год и файл для импорта</h1>
+                    <h1 class="modal-title fs-5" id="exampleModalLabel">Выберите файл для импорта</h1>
                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
                 </div>
                 <div class="modal-body">

+ 56 - 0
resources/views/import/index.blade.php

@@ -0,0 +1,56 @@
+@extends('layouts.app')
+
+@section('content')
+    <div class="row mb-3">
+        <div class="col-6">
+            <h3>Импорт</h3>
+        </div>
+        @if(hasRole('admin'))
+            <div class="col-6 text-end">
+                <button type="button" class="btn btn-sm mb-1 btn-primary" data-bs-toggle="modal" data-bs-target="#importModal">
+                    Импорт
+                </button>
+            </div>
+        @endif
+    </div>
+
+
+    @include('partials.table', [
+        'id'        => $id,
+        'header'    => $header,
+        'strings'   => $imports,
+        'routeName' => 'import.show',
+    ])
+
+    <div class="row pt-3 px-3">
+        <div class="col-12 pagination">
+            {{ $imports->links() }}
+        </div>
+    </div>
+
+
+    <!-- Модальное окно импорта-->
+    <div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
+        <div class="modal-dialog modal-fullscreen-sm-down">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h1 class="modal-title fs-5" id="exampleModalLabel">Выберите файл для импорта</h1>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
+                </div>
+                <div class="modal-body">
+                    <form action="{{ route('import.create') }}" method="post" enctype="multipart/form-data">
+                        @csrf
+                        @include('partials.select', ['title' => 'Вкладка', 'name' => 'type', 'options' => ['orders' => 'Площадки', 'reclamations' => 'Рекламации']])
+                        @include('partials.input', ['title' => 'XLSX файл', 'name' => 'import_file', 'type' => 'file', 'required' => true])
+                        @include('partials.submit', ['name' => 'Импорт'])
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    @if($errors->any())
+        @dump($errors)
+    @endif
+
+@endsection

+ 43 - 0
resources/views/import/show.blade.php

@@ -0,0 +1,43 @@
+@php
+    use App\Models\Order;
+@endphp
+
+@extends('layouts.app')
+
+@section('content')
+
+    <div class="px-3">
+        <div class="row mb-2">
+            <div class="col-md-6">
+                <h3>
+                    Импорт {{ $import->id }}
+                </h3>
+            </div>
+            <div class="col-md-6 text-end">
+
+                <a href="{{ $previous_url ?? route('import.index', session('gp_import')) }}"
+                   class="btn btn-sm mb-1 btn-outline-secondary">Назад</a>
+
+            </div>
+        </div>
+        <div class="row">
+            <div class="col-xl-12 border-end">
+
+                @include('partials.input',  ['name' => 'type', 'title' => 'Тип', 'value' => $import->type, 'disabled' => true])
+                @include('partials.input',  ['name' => 'status', 'title' => 'Статус', 'value' => $import->status, 'disabled' => true])
+                @include('partials.input',  ['name' => 'filename', 'title' => 'Файл', 'value' => $import->filename, 'disabled' => true])
+                @include('partials.input',  ['name' => 'created_at', 'title' => 'Создан', 'value' => $import->created_at, 'disabled' => true])
+                @include('partials.input',  ['name' => 'updated_at', 'title' => 'Изменен', 'value' => $import->updated_at, 'disabled' => true])
+
+                @include('partials.textarea', ['name' => 'result', 'title' => 'Результат', 'value' => $import->result , 'disabled' => true, 'size' => 30])
+
+
+            </div>
+        </div>
+    </div>
+
+    @if($errors->any())
+        @dump($errors)
+    @endif
+
+@endsection

+ 1 - 0
resources/views/layouts/menu.blade.php

@@ -29,6 +29,7 @@
             <ul class="dropdown-menu dropdown-menu-end">
                 <li class="dropdown-item"><a class="nav-link" href="{{ route('contract.index') }}">Договоры</a></li>
                 <li class="dropdown-item"><a class="nav-link" href="{{ route('user.index') }}">Пользователи</a></li>
+                <li class="dropdown-item"><a class="nav-link" href="{{ route('import.index') }}">Импорт</a></li>
             </ul>
         </li>
 

+ 1 - 1
resources/views/orders/show.blade.php

@@ -253,7 +253,7 @@
                                             @if($p->maf_order?->order_number)
                                                 <i class="bi bi-check-all text-success fw-bold"></i>
                                             @else
-                                                @if($needs[$p->product_id]['sku']-- > 0)
+                                                @if(isset($needs[$p->product_id]) && ($needs[$p->product_id]['sku']-- > 0))
                                                     <i class="bi bi-check text-success fw-bold"></i>
                                                 @else
                                                     <i class="bi bi-x text-danger fw-bold"></i>

+ 7 - 0
routes/web.php

@@ -3,6 +3,7 @@
 use App\Http\Controllers\AreaController;
 use App\Http\Controllers\ContractController;
 use App\Http\Controllers\FilterController;
+use App\Http\Controllers\ImportController;
 use App\Http\Controllers\MafOrderController;
 use App\Http\Controllers\OrderController;
 use App\Http\Controllers\ProductController;
@@ -52,6 +53,12 @@ Route::middleware('auth:web')->group(function () {
             Route::delete('{user}', [UserController::class, 'destroy'])->name('user.destroy');
             Route::post('undelete/{user}', [UserController::class, 'undelete'])->name('user.undelete');
         });
+
+        Route::prefix('import')->group(function (){
+            Route::get('', [ImportController::class, 'index'])->name('import.index');
+            Route::get('{import}', [ImportController::class, 'show'])->name('import.show');
+            Route::post('', [ImportController::class, 'store'])->name('import.create');
+        });
     });
 
     // profile