Quellcode durchsuchen

+ Сброс поиска
+ размер изображения в таблице
+ описание ограничил 23 слова, при наведении курсора показывается полное описание в title
+ изменена верстка
+ цена в шаблоне с копейками
+ после формирования файл автоматически скачивается, через секунду происходит переход на главную
+ выбор шаблона (файлы docx в папке storage/templates, подготовленные нужным образом)
+ шаблон с одним товаром на странице
+ импорт производится в фоне

Alexander Musikhin vor 2 Jahren
Ursprung
Commit
d079aa885d

+ 4 - 3
app/Http/Controllers/ExportController.php

@@ -50,10 +50,11 @@ class ExportController extends Controller
 
         // prepared vars - run job
 
-        $filename = 'export_' . date('YmdHis') . '.docx';
-        GenerateDocxJob::dispatch($vars_for_template, count($products), $filename);
 
-        Log::notice('Created job. Execution time: ' . microtime(true) - LARAVEL_START);
+        $filename = 'Наш_Двор_' . date('YmdHis') . '.docx';
+        GenerateDocxJob::dispatch($vars_for_template, count($products), $filename, $request->template);
+
+        Log::notice('Created export job. Execution time: ' . microtime(true) - LARAVEL_START);
 
         $data['filename'] = $filename;
         return redirect()->route('wait_export', $data);

+ 31 - 52
app/Http/Controllers/ProductController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers;
 
 use App\Http\Requests\SaveProductRequest;
+use App\Jobs\ImportXlsxJob;
 use App\Models\Product;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Redirect;
@@ -51,59 +52,28 @@ class ProductController extends Controller
         return view('products.index', $data);
     }
 
-    // todo вынести в job, чтобы работала в фоне
+
     public function upload_xls(Request $request)
     {
-        $xls = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($request->file('file'));
-        $xls->setReadDataOnly(true);
-        $sheet = $xls->load($request->file('file'));
-
-        // read xls to array
-        $goods = $sheet->setActiveSheetIndex(0)->toArray();
-        unset($sheet, $xls);
-
-        $series = '';
-        $i = 0;
-        $created = 0;
-        $updated = 0;
-        $no_image = [];
-        foreach ($goods as $good) {
-            // check first line and skip it
-            if ($good[0] === '№п/п' && $good['3'] === 'Наименование') continue;
-
-            // check the line is name of series
-            if ($good[0] == NULL && $good[1] == NULL && $good[2] == NULL && is_string($good[3])) {
-                $series = $good[3];
-                continue;
-            }
-            $tmp = explode("\n", $good[3]);
-            if (!isset($tmp[1])) {
-                $good[3] = preg_replace('!\s+!', ' ', $good[3]);
-                $tmp = explode(' ', $good[3], 2);
-            }
-            $images = $this->find_images($tmp[0]);
-            $data = [
-//                'article' => $tmp[0],
-                'series' => $series,
-                'name' => (isset($tmp[1])) ? $tmp[1] : 'error',
-                'name_for_form' => $good[2],
-                'product_group' => $good[1],
-                'price' => $good[5],
-                'characteristics' => $good[4],
-                'tech_description' => $good[7],
-                'tech_description_short' => $good[8],
-                'image_path' => (!empty($images)) ? $images[0] : $images,
-            ];
-
-            $a = Product::query()->updateOrCreate(['article' => $tmp[0]], $data);
-            if ($a->wasRecentlyCreated) $created++; else $updated++;
-            //echo $i++ . '. Серия: ' . $series . ', артикул: ' . $tmp[0] . '<br>';
-            $i++;
-            if ($data['image_path'] == '') $no_image[] = $tmp[0];
-        }
+        $file = $request->file('file');
+        $path = storage_path('import');
+        $filename = 'import_' . date('YmdHis') . '.' . $file->extension();
+        $file->move($path, $filename);
+
+        $info = ['count' => 0, 'updated' => 0, 'created' => 0, 'no_image' => []];
+        $f = fopen(storage_path('import/') . $filename . '.txt', 'w+');
+        fwrite($f, serialize($info));
+        fclose($f);
 
-        return view('products.import_result', ['count' => $i, 'updated' => $updated, 'created' => $created, 'no_image' => $no_image]);
 
+        ImportXlsxJob::dispatch(storage_path('import/' . $filename));
+        return redirect()->route('import_result', ['filename' => $filename]);
+    }
+
+    public function import_result(Request $request){
+        $filename = $request->filename . '.txt';
+        $data = unserialize(file_get_contents(storage_path('import/') . $filename));
+        return view('products.import_result', $data);
     }
 
     private $allfiles; // remember files list
@@ -132,11 +102,8 @@ class ProductController extends Controller
 
     public function save_product(SaveProductRequest $request)
     {
-
-
         Product::query()->where('id', $request->validated('id'))->update($request->validated());
         return redirect($request->prev_url);
-//        return redirect()->route('index');
     }
 
     public function upload_image(Request $request){
@@ -158,7 +125,19 @@ class ProductController extends Controller
     public function select_export(Request $request){
         $data['ids'] = json_decode($request->ids, true);
         $data['products'] = Product::query()->whereIn('id', $data['ids'])->get();
+        $data['templates'] = $this->get_templates();
         return view('products.select_export', $data);
     }
 
+    private function get_templates(){
+        $files = scandir(storage_path('templates'));
+        $result = [];
+        foreach ($files as $f){
+            if(Str::endsWith($f, '.docx'))
+                $result[] = $f;
+        }
+        return$result;
+    }
+
+
 }

+ 4 - 4
app/Jobs/GenerateDocxJob.php

@@ -18,17 +18,18 @@ class GenerateDocxJob implements ShouldQueue
     protected $vars;
     protected $prod_count;
     protected $filename;
-
+    protected $template;
     /**
      * Create a new job instance.
      *
      * @return void
      */
-    public function __construct($vars, $count_products, $filename)
+    public function __construct($vars, $count_products, $filename, $template)
     {
         $this->vars =$vars;
         $this->prod_count = $count_products;
         $this->filename = $filename;
+        $this->template = $template;
     }
 
     /**
@@ -39,8 +40,7 @@ class GenerateDocxJob implements ShouldQueue
     public function handle()
     {
         $tmpfile = public_path('exported/docx/' . $this->filename . '.txt');
-
-        $my_template = new TemplateProcessor(storage_path('templates/template_maf.docx'));
+        $my_template = new TemplateProcessor(storage_path('templates/' . $this->template));
         $my_template->cloneBlock('product_block', $this->prod_count, true, true, $this->vars);
 
         $i = 0;

+ 114 - 0
app/Jobs/ImportXlsxJob.php

@@ -0,0 +1,114 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Product;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldBeUnique;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+
+class ImportXlsxJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+
+    protected $filename;
+
+    public function __construct($filename)
+    {
+        $this->filename = $filename;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $xls = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($this->filename);
+        $xls->setReadDataOnly(true);
+        $sheet = $xls->load($this->filename);
+        $tmpfile = $this->filename . '.txt';
+
+        // read xls to array
+        $goods = $sheet->setActiveSheetIndex(0)->toArray();
+        unset($sheet, $xls);
+
+        $series = '';
+        $i = $created = $updated = 0;
+        $no_image = [];
+
+        foreach ($goods as $good) {
+            // check first line and skip it
+            if ($good[0] === '№п/п' && $good['3'] === 'Наименование') continue;
+
+            // check the line is name of series
+            if ($good[0] == NULL && $good[1] == NULL && $good[2] == NULL && is_string($good[3])) {
+                $series = $good[3];
+                continue;
+            }
+
+            // get article from 3rd column
+            $tmp = explode("\n", $good[3]);
+            if (!isset($tmp[1])) {
+                $good[3] = preg_replace('!\s+!', ' ', $good[3]);
+                $tmp = explode(' ', $good[3], 2);
+            }
+            $images = $this->find_images($tmp[0]);
+            $data = [
+                'series' => $series,
+                'name' => (isset($tmp[1])) ? $tmp[1] : 'error',
+                'name_for_form' => $good[2],
+                'product_group' => $good[1],
+                'price' => $good[5],
+                'characteristics' => $good[4],
+                'tech_description' => $good[7],
+                'tech_description_short' => $good[8],
+                'image_path' => (!empty($images)) ? $images[0] : $images,
+            ];
+
+            $a = Product::query()->updateOrCreate(['article' => $tmp[0]], $data);
+            if ($a->wasRecentlyCreated) $created++; else $updated++;
+            //echo $i++ . '. Серия: ' . $series . ', артикул: ' . $tmp[0] . '<br>';
+            $i++;
+            if ($data['image_path'] == '') $no_image[] = $tmp[0];
+            $info = ['count' => $i, 'updated' => $updated, 'created' => $created, 'no_image' => $no_image];
+            $f = fopen($tmpfile, 'w+');
+            fwrite($f, serialize($info));
+            fclose($f);
+        }
+        $info['finished'] = true;
+        $f = fopen($tmpfile, 'w+');
+        fwrite($f, serialize($info));
+        fclose($f);
+
+    }
+
+    private $allfiles; // remember files list
+
+    private function find_images($article)
+    {
+        $path = base_path('www/') . env('IMAGES_PATH', '---') . '/';
+        if (!isset($this->allfiles) || empty($this->allfiles)) {
+            $this->allfiles = scandir($path);
+        }
+        $images = [];
+        foreach ($this->allfiles as $filename) {
+            if ((mb_strpos($filename, $article) === 0) || (
+                    mb_strpos(Str::lower($filename), Str::slug($article)) === 0))
+                $images[] = $filename;
+        }
+        return (!empty($images)) ? $images : '';
+    }
+}

+ 36 - 0
database/migrations/2019_08_19_000000_create_failed_jobs_table.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateFailedJobsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('failed_jobs', function (Blueprint $table) {
+            $table->id();
+            $table->string('uuid')->unique();
+            $table->text('connection');
+            $table->text('queue');
+            $table->longText('payload');
+            $table->longText('exception');
+            $table->timestamp('failed_at')->useCurrent();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('failed_jobs');
+    }
+}

+ 14 - 1
resources/views/products/import_result.blade.php

@@ -4,7 +4,12 @@
     <div class="container-fluid">
         <div class="row">
             <div class="col-12 text-center my-5">
-                <h1 class="">Результаты импорта</h1>
+                @isset($finished)
+                    <h1 class="">Импорт завершён</h1>
+                @else
+                    <h1 class="">Импортируем...</h1>
+                @endisset
+
                 <div class="fs-3 my-5">Обработано записей: {{ $count }}, из них создано: {{ $created }}, обновлено: {{ $updated }}.</div>
                 <div class="mb-5">
                     Не нашли изображение к следующим артикулам:<br> {{ implode(', ', $no_image) }}
@@ -16,4 +21,12 @@
         </div>
     </div>
 
+    @if(!isset($finished) )
+        <script>
+            setTimeout(function () {
+                window.location.reload();
+            }, 500);
+        </script>
+    @endif
+
 @endsection

+ 11 - 9
resources/views/products/index.blade.php

@@ -3,14 +3,14 @@
 @section('content')
     <div class="container-fluid">
         <div class="row align-items-center">
-            <div class="col-5">
+            <div class="col-xl-5 col-md-12">
                 <div class="">Всего найдено: {{ $products->total() }}</div>
                 {{ $products->links() }}
                 Выбрано: <span id="count_selected">0</span>
                 <button class="btn btn-sm btn-primary mx-3" onclick="select_export()">Экспорт</button>
                 <button class="btn btn-sm btn-primary" onclick="reset_selected()">Сбросить выбор</button>
             </div>
-            <div class="col-7">
+            <div class="col-xl-7 col-md-12">
                 <form class="" action="" method="get">
                     <div class="row my-2 justify-content-center">
                         <div class="col">
@@ -52,8 +52,9 @@
                                 </option>
                             </select>
                         </div>
-                        <div class="col-1">
+                        <div class="col-auto">
                             <button id="sb" class="btn btn-primary mt-4" type="submit">Поиск</button>
+                            <a href="{{ route('index') }}" class="btn btn-light mt-4 ms-1">Сброс</a>
                         </div>
                     </div>
                 </form>
@@ -69,11 +70,11 @@
                             <label><input type="checkbox" class="check_all" onchange="toggle(this)">&nbsp;Все</label>
                         </th>
 {{--                        <th>Группа <br> Наименование <br> Наименование под образец формы</th>--}}
-                        <th>Цена, руб.</th>
+                        <th>Цена,&nbsp;руб.</th>
                         <th>Характеристики</th>
-                        <th>Техническое описание</th>
-                        <th>Изображение</th>
-                        <th>Создан / Изменён</th>
+                        <th>Техническое&nbsp;описание</th>
+                        <th class="text-center ">Изображение</th>
+                        <th>Создан&nbsp;/&nbsp;Изменён</th>
                     </thead>
                     </tr>
                     <tbody>
@@ -83,6 +84,7 @@
                                 <label>
                                     <input type="checkbox" onchange="select_product({{ $product->id }}, this)" class="form-check-inline me-0 prd-chk" name="ids" value="{{ $product->id }}"><br>
                                     {{ $product->article }}<br>
+                                    {{ $product->series }}
 
                                 </label>
                             </td>
@@ -100,12 +102,12 @@
                                     {!! nl2br($product->characteristics) !!}
                                 </a>
                             </td>
-                            <td>{!! Str::words(nl2br($product->tech_description), 70) !!}</td>
+                            <td title="{{ $product->tech_description }}" style="font-size: small">{!! Str::words(nl2br($product->tech_description), 22) !!}</td>
                             <td class="text-center align-middle">
                                 @empty($product->image_path)
                                     Нет изображения
                                 @else
-                                    <img class="img-fluid"
+                                    <img class="img-fluid" style="max-height: 80px"
                                          src="{{ '/' . env('IMAGES_PATH', '/fill_images_path_in_env') . '/' . $product->image_path }}"
                                          alt="{{ $product->article }}">
                                 @endempty

+ 11 - 4
resources/views/products/select_export.blade.php

@@ -5,16 +5,23 @@
     <div class="container-fluid">
         <div class="row">
             <div class="col-12 text-center my-5">
-                Выбранные артикулы:
-                @foreach($products as $product)
-                    {{ $product->article }}
-                @endforeach
+                Выбранно артикулов: <span>{{ $products->count() }}</span>
+{{--                @foreach($products as $product)--}}
+{{--                    {{ $product->article }}--}}
+{{--                @endforeach--}}
             </div>
             <div class="col-12 text-center">
                 <form method="post" action="{{ route('export_docx') }}">
                     @csrf
                     <div class="row justify-content-center">
                         <div class="col-3 mb-3">
+                            <label class="form-label">Выберите шаблон</label>
+                            <select name="template">
+                                @foreach($templates as $template)
+                                    <option>{{ $template }}</option>
+                                @endforeach
+                            </select>
+
                             <label class="form-label">Выберите какое описание вставить в документ</label>
                             <select name="descr" class="form-select">
                                 <option value="1">Характеристики + краткое описание</option>

+ 8 - 1
resources/views/products/wait.blade.php

@@ -14,7 +14,7 @@
         </div>
         @if($percent == 100)
             <div class="col-12 my-5 fs-1 text-center">
-                Скачать <a href="/exported/docx/{{ $filename }}">{{ $filename }}</a>
+                Скачать <a id="link" href="/exported/docx/{{ $filename }}">{{ $filename }}</a>
             </div>
         @endif
 
@@ -26,6 +26,13 @@
                 window.location.reload();
             }, 1000);
         </script>
+    @else
+        <script>
+            document.getElementById('link').click();
+            setTimeout(function () {
+                window.location = '/';
+            }, 1000);
+        </script>
     @endif
 
 @endsection

+ 1 - 0
routes/web.php

@@ -28,6 +28,7 @@ Route::middleware('mgrauth')->group(function (){
     Route::post('/select_export', [ProductController::class, 'select_export'])->name('select_export');
     Route::post('/export_docx', [ExportController::class, 'export_docx'])->name('export_docx');
     Route::get('/wait_export/{filename}', [ExportController::class, 'wait_export'])->name('wait_export');
+    Route::get('/import_result/{filename}', [ProductController::class, 'import_result'])->name('import_result');
 });
 
 Route::get('logout', function (){

BIN
storage/import/import_20230204052108.xlsx


+ 1 - 0
storage/import/import_20230204052108.xlsx.txt

@@ -0,0 +1 @@
+a:5:{s:5:"count";i:938;s:7:"updated";i:938;s:7:"created";i:0;s:8:"no_image";a:5:{i:0;s:24:"Волейбольные";i:1;s:8:"Урна";i:2;s:23:"Урна уличная";i:3;s:16:"Мусорный";i:4;s:26:"ЕвроКонтейнер";}s:8:"finished";b:1;}

BIN
storage/import/import_20230204052147.xlsx


+ 1 - 0
storage/import/import_20230204052147.xlsx.txt

@@ -0,0 +1 @@
+a:5:{s:5:"count";i:938;s:7:"updated";i:932;s:7:"created";i:6;s:8:"no_image";a:0:{}s:8:"finished";b:1;}

BIN
storage/templates/template_maf.docx


BIN
storage/templates/template_maf_one_page.docx