Browse Source

+ при редатировании товара можно загружать изображения + если несколько
изображений найдено по артикулу - можно выбрать одно

Александр Мусихин 2 năm trước cách đây
mục cha
commit
d73c0db4d6

+ 26 - 9
app/Http/Controllers/ProductController.php

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
 use App\Http\Requests\SaveProductRequest;
 use App\Models\Product;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Redirect;
 use Illuminate\Support\Str;
 
 
@@ -48,7 +49,7 @@ class ProductController extends Controller
     }
 
     // todo вынести в job, чтобы работала в фоне
-    public function upload(Request $request)
+    public function upload_xls(Request $request)
     {
         $xls = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($request->file('file'));
         $xls->setReadDataOnly(true);
@@ -77,7 +78,7 @@ class ProductController extends Controller
                 $good[3] = preg_replace('!\s+!', ' ', $good[3]);
                 $tmp = explode(' ', $good[3], 2);
             }
-
+            $images = $this->find_images($tmp[0]);
             $data = [
 //                'article' => $tmp[0],
                 'series' => $series,
@@ -88,7 +89,7 @@ class ProductController extends Controller
                 'characteristics' => $good[4],
                 'tech_description' => $good[7],
                 'tech_description_short' => $good[8],
-                'image_path' => $this->find_image($tmp[0]),
+                'image_path' => (!empty($images)) ? $images[0] : $images,
             ];
 
             $a = Product::query()->updateOrCreate(['article' => $tmp[0]], $data);
@@ -104,32 +105,48 @@ class ProductController extends Controller
 
     private $allfiles; // remember files list
 
-    private function find_image($article)
+    private function find_images($article)
     {
         $path_to_files = './' . env('IMAGES_PATH', '---') . '/';
         if (!isset($this->allfiles) || empty($this->allfiles)) {
             $this->allfiles = scandir($path_to_files);
         }
+        $images = [];
         foreach ($this->allfiles as $filename) {
             if ((mb_strpos($filename, $article) === 0) || (
                     mb_strpos(Str::lower($filename), Str::slug($article)) === 0))
-                return $filename;
+                $images[] = $filename;
         }
-
-        return '';
+        return (!empty($images)) ? $images : '';
     }
 
     public function product(Request $request, $id)
     {
         $data['product'] = Product::query()->findOrFail($id);
+        $data['images'] = $this->find_images($data['product']->article);
         return view('products.product', $data);
     }
 
     public function save_product(SaveProductRequest $request)
     {
-        $p = Product::query()->where('id', $request->validated('id'))->update($request->validated());
-
+        Product::query()->where('id', $request->validated('id'))->update($request->validated());
         return redirect()->route('index');
     }
 
+    public function upload_image(Request $request){
+        $path = public_path() . '/' . env('IMAGES_PATH');
+//        dd($path);
+        $file = $request->file('filename');
+        $new_filename = $request->article . '.' . date('YmdHis') . '.' . $file->extension();
+        $file->move($path, $new_filename);
+        Product::query()->where('article', $request->article)->update(['image_path' => $new_filename]);
+        return redirect()->back();
+    }
+
+    public function update_image(Request $request, $id){
+        $validated = $request->validate(['image_path' => 'required|string']);
+        Product::query()->where('id', $id)->update($validated);
+        return redirect()->route('view_product', $id);
+    }
+
 }

+ 15 - 0
app/Http/Requests/SaveProductRequest.php

@@ -35,4 +35,19 @@ class SaveProductRequest extends FormRequest
             'tech_description_short' => 'required|string',
         ];
     }
+
+    public function messages()
+    {
+        return [
+            'series.required' => 'Не может быть пустым',
+            'name.required' => 'Не может быть пустым',
+            'name_for_form.required' => 'Не может быть пустым',
+            'product_group.required' => 'Не может быть пустым',
+            'price.required' => 'Не может быть пустым',
+            'characteristics.required' => 'Не может быть пустым',
+            'tech_description.required' => 'Не может быть пустым',
+            'tech_description_short.required' => 'Не может быть пустым',
+
+        ];
+    }
 }

+ 2 - 1
composer.json

@@ -11,7 +11,8 @@
         "laravel/sanctum": "^3.0",
         "laravel/tinker": "^2.7",
         "laravel/ui": "^4.2",
-        "phpoffice/phpspreadsheet": "^1.27"
+        "phpoffice/phpspreadsheet": "^1.27",
+        "phpoffice/phpword": "^1.0"
     },
     "require-dev": {
         "barryvdh/laravel-debugbar": "^3.7",

+ 176 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "459dea0eaee6d40278d8025cc1a15cd1",
+    "content-hash": "e8eada783b3c832be35318343a116427",
     "packages": [
         {
             "name": "brick/math",
@@ -958,6 +958,68 @@
             ],
             "time": "2022-10-26T14:07:24+00:00"
         },
+        {
+            "name": "laminas/laminas-escaper",
+            "version": "2.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laminas/laminas-escaper.git",
+                "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
+                "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-mbstring": "*",
+                "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0"
+            },
+            "conflict": {
+                "zendframework/zend-escaper": "*"
+            },
+            "require-dev": {
+                "infection/infection": "^0.26.6",
+                "laminas/laminas-coding-standard": "~2.4.0",
+                "maglnet/composer-require-checker": "^3.8.0",
+                "phpunit/phpunit": "^9.5.18",
+                "psalm/plugin-phpunit": "^0.17.0",
+                "vimeo/psalm": "^4.22.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Laminas\\Escaper\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs",
+            "homepage": "https://laminas.dev",
+            "keywords": [
+                "escaper",
+                "laminas"
+            ],
+            "support": {
+                "chat": "https://laminas.dev/chat",
+                "docs": "https://docs.laminas.dev/laminas-escaper/",
+                "forum": "https://discourse.laminas.dev",
+                "issues": "https://github.com/laminas/laminas-escaper/issues",
+                "rss": "https://github.com/laminas/laminas-escaper/releases.atom",
+                "source": "https://github.com/laminas/laminas-escaper"
+            },
+            "funding": [
+                {
+                    "url": "https://funding.communitybridge.org/projects/laminas-project",
+                    "type": "community_bridge"
+                }
+            ],
+            "time": "2022-10-10T10:11:09+00:00"
+        },
         {
             "name": "laravel/framework",
             "version": "v9.48.0",
@@ -2583,6 +2645,119 @@
             },
             "time": "2023-01-24T20:07:45+00:00"
         },
+        {
+            "name": "phpoffice/phpword",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPOffice/PHPWord.git",
+                "reference": "8521612b39edeec9055d3688ad555342a40857dd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/8521612b39edeec9055d3688ad555342a40857dd",
+                "reference": "8521612b39edeec9055d3688ad555342a40857dd",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-xml": "*",
+                "laminas/laminas-escaper": ">=2.6",
+                "php": "^7.1|^8.0"
+            },
+            "require-dev": {
+                "dompdf/dompdf": "^2.0",
+                "ext-gd": "*",
+                "ext-libxml": "*",
+                "ext-zip": "*",
+                "mpdf/mpdf": "^8.1",
+                "php-coveralls/php-coveralls": "^2.5",
+                "phpmd/phpmd": "^2.13",
+                "phpunit/phpunit": ">=7.0",
+                "symfony/process": "^4.4",
+                "tecnickcom/tcpdf": "^6.5"
+            },
+            "suggest": {
+                "dompdf/dompdf": "Allows writing PDF",
+                "ext-gd2": "Allows adding images",
+                "ext-xmlwriter": "Allows writing OOXML and ODF",
+                "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
+                "ext-zip": "Allows writing OOXML and ODF"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-develop": "0.19-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PhpOffice\\PhpWord\\": "src/PhpWord"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-3.0"
+            ],
+            "authors": [
+                {
+                    "name": "Mark Baker"
+                },
+                {
+                    "name": "Gabriel Bull",
+                    "email": "me@gabrielbull.com",
+                    "homepage": "http://gabrielbull.com/"
+                },
+                {
+                    "name": "Franck Lefevre",
+                    "homepage": "https://rootslabs.net/blog/"
+                },
+                {
+                    "name": "Ivan Lanin",
+                    "homepage": "http://ivan.lanin.org"
+                },
+                {
+                    "name": "Roman Syroeshko",
+                    "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
+                },
+                {
+                    "name": "Antoine de Troostembergh"
+                }
+            ],
+            "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)",
+            "homepage": "https://phpword.readthedocs.io/",
+            "keywords": [
+                "ISO IEC 29500",
+                "OOXML",
+                "Office Open XML",
+                "OpenDocument",
+                "OpenXML",
+                "PhpOffice",
+                "PhpWord",
+                "Rich Text Format",
+                "WordprocessingML",
+                "doc",
+                "docx",
+                "html",
+                "odf",
+                "odt",
+                "office",
+                "pdf",
+                "php",
+                "reader",
+                "rtf",
+                "template",
+                "template processor",
+                "word",
+                "writer"
+            ],
+            "support": {
+                "issues": "https://github.com/PHPOffice/PHPWord/issues",
+                "source": "https://github.com/PHPOffice/PHPWord/tree/1.0.0"
+            },
+            "time": "2022-11-15T20:24:50+00:00"
+        },
         {
             "name": "phpoption/phpoption",
             "version": "1.9.0",

+ 4 - 0
resources/sass/_custom.scss

@@ -8,3 +8,7 @@
 .prod-tr:has(td:hover) {
     background-color: rgba(220,224,255,0.85);
 }
+
+.notice {
+    color: #bfbfbf;
+}

+ 1 - 1
resources/views/layouts/app.blade.php

@@ -79,7 +79,7 @@
         </main>
     </div>
 
-    <form action="{{ route('upload') }}" method="post" enctype="multipart/form-data" class="visually-hidden"
+    <form action="{{ route('upload_xls') }}" method="post" enctype="multipart/form-data" class="visually-hidden"
           onchange="this.submit()">
         @csrf
         <input class="form-control" type="file" id="import-form" name="file">

+ 20 - 2
resources/views/products/index.blade.php

@@ -62,7 +62,9 @@
                 <table class="table">
                     <thead>
                     <tr class="align-middle">
-                        <th>Артикул, Серия</th>
+                        <th class="text-center">Артикул, Серия<br>
+                            <label><input type="checkbox" onchange="toggle(this)">&nbsp;Все</label>
+                        </th>
 
                         <th>Группа <br> Наименование <br> Наименование под образец формы</th>
                         <th>Цена</th>
@@ -77,7 +79,7 @@
                         <tr class="align-middle prod-tr">
                             <td class="text-center">
                                 <label>
-                                    <input type="checkbox" class="form-check-inline me-0" name="prd_{{ $product->id }}"><br>
+                                    <input type="checkbox" class="form-check-inline me-0 prd-chk" name="ids" value="{{ $product->id }}"><br>
                                     {{ $product->article }}<br>
                                     {{ $product->series }}
                                 </label>
@@ -114,4 +116,20 @@
         </div>
     </div>
 
+
+    <script>
+        function toggle(source) {
+            let checkboxes = document.querySelectorAll('.prd-chk');
+            for(let i =0; i < checkboxes.length; i++){
+                checkboxes[i].checked = source.checked;
+            }
+        }
+
+
+
+
+
+
+    </script>
+
 @endsection

+ 115 - 20
resources/views/products/product.blade.php

@@ -16,47 +16,96 @@
                         </div>
                         <div class="col-6">
                             <label for="series" class="form-label">Серия</label>
-                            <input class="form-control" name="series" id="series" value="{{ $product->series }}">
+                            <input class="form-control @error('series') border-danger @enderror"
+                                   name="series" id="series" value="{{ old('series', $product->series) }}">
+                            @error('series')
+                            <div class="form-text text-danger" id="titleError">
+                                {{ $message }}
+                            </div>
+                            @enderror
                         </div>
                     </div>
 
                     <div class="row mt-3">
                         <div class="col-6">
                             <label for="name" class="form-label">Наименовение</label>
-                            <input class="form-control" name="name" id="name" value="{{ $product->name }}">
+                            <input class="form-control @error('name') border-danger @enderror"
+                                   name="name" id="name" value="{{ old('name', $product->name) }}">
+                            @error('name')
+                            <div class="form-text text-danger" id="titleError">
+                                {{ $message }}
+                            </div>
+                            @enderror
                         </div>
                         <div class="col-6">
                             <label for="name_for_form" class="form-label">Наименование под образец формы</label>
-                            <input class="form-control" name="name_for_form" id="name_for_form"
-                                   value="{{ $product->name_for_form }}">
+                            <input class="form-control @error('name_for_form') border-danger @enderror"
+                                   name="name_for_form" id="name_for_form"
+                                   value="{{ old('name_for_form', $product->name_for_form) }}">
+                            @error('name_for_form')
+                            <div class="form-text text-danger" id="titleError">
+                                {{ $message }}
+                            </div>
+                            @enderror
                         </div>
                     </div>
 
                     <div class="row mt-3">
                         <div class="col-6">
                             <label for="product_group" class="form-label">Группа</label>
-                            <input class="form-control" name="product_group" id="product_group"
-                                   value="{{ $product->product_group }}">
+                            <input class="form-control @error('product_group') border-danger @enderror"
+                                   name="product_group" id="product_group"
+                                   value="{{ old('product_group', $product->product_group) }}">
+                            @error('product_group')
+                            <div class="form-text text-danger" id="titleError">
+                                {{ $message }}
+                            </div>
+                            @enderror
                         </div>
                         <div class="col-6">
                             <label for="price" class="form-label">Цена</label>
-                            <input type="number" class="form-control" name="price" id="price"
-                                   value="{{ $product->price }}">
+                            <input type="number" class="form-control @error('price') border-danger @enderror"
+                                   name="price" id="price"
+                                   value="{{ old('price', $product->price) }}">
+                            @error('price')
+                            <div class="form-text text-danger" id="titleError">
+                                {{ $message }}
+                            </div>
+                            @enderror
                         </div>
                     </div>
 
-                    <label for="characteristics" class="form-label">Характеристики</label>
-                    <textarea class="form-control" name="characteristics" rows="4"
-                              id="characteristics">{{ $product->characteristics }}</textarea>
+                    <label for="characteristics" class="form-label mt-3">Характеристики</label>
+                    <textarea class="form-control @error('characteristics') border-danger @enderror"
+                              name="characteristics" rows="4"
+                              id="characteristics">{{ old('characteristics', $product->characteristics) }}</textarea>
+                    @error('characteristics')
+                    <div class="form-text text-danger" id="titleError">
+                        {{ $message }}
+                    </div>
+                    @enderror
 
-                    <label for="tech_description" class="form-label">Техническое описание</label>
-                    <textarea class="form-control" name="tech_description" rows="6"
-                              id="tech_description">{{ $product->tech_description }}</textarea>
+                    <label for="tech_description" class="form-label mt-3">Техническое описание</label>
+                    <textarea class="form-control @error('tech_description') border-danger @enderror"
+                              name="tech_description" rows="5"
+                              id="tech_description">{{ old('tech_description', $product->tech_description) }}</textarea>
+                    @error('tech_description')
+                    <div class="form-text text-danger" id="titleError">
+                        {{ $message }}
+                    </div>
+                    @enderror
 
-                    <label for="tech_description_short" class="form-label">Техническое описание сокращенное без
+                    <label for="tech_description_short" class="form-label mt-3">Техническое описание сокращенное без
                         артикула</label>
-                    <textarea class="form-control" name="tech_description_short" rows="6"
-                              id="tech_description_short">{{ $product->tech_description_short }}</textarea>
+                    <textarea class="form-control  @error('tech_description_short') border-danger @enderror"
+                              name="tech_description_short" rows="5"
+                              id="tech_description_short">{{
+                                old('tech_description_short', $product->tech_description_short) }}</textarea>
+                    @error('tech_description_short')
+                    <div class="form-text text-danger" id="titleError">
+                        {{ $message }}
+                    </div>
+                    @enderror
 
                     <div class="col-12 text-center mt-3">
                         <button type="submit" class="btn btn-primary">Сохранить</button>
@@ -65,11 +114,57 @@
             </div>
 
             <div class="col-sm-12 col-md-6">
-                <img src="{{ '/' . env('IMAGES_PATH', '/fill_images_path_in_env') . '/' . $product->image_path }}"
-                     alt="{{ $product->article }}" class="img-fluid">
-            </div>
+                <div class="col-12 text-end">
+                    <button class="btn btn-primary" onclick="document.getElementById('upload-image-form').click()">
+                        Загрузить
+                    </button>
+                    <br>
+                    <span class="notice">Изображение сохраниться в папку с изображениями товаров: stroyprofit.com/{{ env('IMAGES_PATH', 'fill-path') }}/<br>
+                    Имя файла будет сформировано уникальное, в начале имени до точки будет артикул товара.</span>
+
+
+                    <form action="{{ route('upload_image') }}" method="post" enctype="multipart/form-data"
+                          class="visually-hidden" onchange="this.submit()">
+                        @csrf
+                        <input type="hidden" name="article" value="{{ $product->article }}">
+                        <input type="file" name="filename" id="upload-image-form" accept=".jpg">
+                    </form>
+                </div>
+
+
+                @empty($product->image_path)
+                    <div class="text-center my-5 fs-3">Нет изображения</div>
+                @else
+                    <div class="col-12 text-center" >
+                        <img src="{{ '/' . env('IMAGES_PATH', '/fill_images_path_in_env') . '/' . $product->image_path }}"
+                             alt="{{ $product->article }}" class="img-fluid" style="max-height: 40vh;">
+                    </div>
+
+                @endempty
+                @if(count($images) > 1)
+                    <div class="col-12 mt-3">
+                        <div class="fs-5 mb-4">Найдены изображения с таким артикулом:</div>
+                        <div class="row justify-content-center align-items-end">
+                            @foreach($images as $image)
+                                <div class="h-100 col-md-3 col-sm-2 text-center">
+                                    <img class="img-thumbnail"
+                                         src="{{ '/' . env('IMAGES_PATH', 'zzz') . '/' . $image }}" alt="{{ $image }}">
+
+
+                                    <form action="{{ route('update_image', $product->id) }}" method="post">
+                                        @csrf
+                                        <input type="hidden" name="image_path" value="{{ $image }}">
+                                        <button type="submit" class="btn btn-primary my-3">Выбрать</button>
+                                    </form>
 
+                                </div>
+                            @endforeach
+                        </div>
+                    </div>
+                @endif
 
+
+            </div>
         </div>
     </div>
 

+ 3 - 4
routes/web.php

@@ -18,10 +18,9 @@ use Illuminate\Support\Facades\Route;
 
 Route::get('/', [ProductController::class, 'index'])->name('index');
 Route::get('/product/{id}', [ProductController::class, 'product'])->name('view_product');
-Route::post('/upload_xls', [ProductController::class, 'upload'])->name('upload');
+Route::post('/upload_xls', [ProductController::class, 'upload_xls'])->name('upload_xls');
+Route::post('/upload_image', [ProductController::class, 'upload_image'])->name('upload_image');
 Route::post('/save_product', [ProductController::class, 'save_product'])->name('save_product');
+Route::post('/update_image/{id}', [ProductController::class, 'update_image'])->name('update_image');
 
 
-//Auth::routes();
-
-Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');

+ 17 - 5
todo.txt

@@ -2,16 +2,25 @@
 + постраницчный вывод таблицы товаров, количество товаров на странице
 + фильры: общий поиск, серия, диапазон цен
 + редактирование товара (все поля, кроме изображения и артикула)
++ при редатировании товара можно загружать изображения
++ если несколько изображений найдено по артикулу - можно выбрать одно
+
+
+
+
+
+todo выбор товаров с запоминаннием при переходах между страницами
+todo формирование документа ворд из выбранных товаров
+todo Авторизация в системе без разделения ролей на базе «манагера»
 
 
 
 
-todo Авторизация в системе без разделения ролей на базе «манагера».
 
 Примерное ТЗ.
 todo Авторизация в системе без разделения ролей на базе «манагера».
-+Каталог товаров по данной таблице xlsx.
-+Возможность обновлять и загружать новые товары из этой (или ее части) таблицы xlsx.(по аналогии с «манагером»). Товары должны быть разделены по сериям.
++ Каталог товаров по данной таблице xlsx.
++ озможность обновлять и загружать новые товары из этой (или ее части) таблицы xlsx.(по аналогии с «манагером»). Товары должны быть разделены по сериям.
 
 * Все картинки загружаются на сайт в одну папку. Название файла картинки обязательно содержит артикул изделия, может состоять из любых символов и быть любой длины, но при этом в названии может присутствовать разделитель «точка», до которого происходит идентификация изделия.
 *** upd картинки берутся с сайта стройпрофит, если она есть в наличии - подтягиватеся, если нет, то сообщение об этом при импорте
@@ -23,8 +32,11 @@ todo Должна присутствовать возможность сформ
 - как для одного изделия, так и для группы изделий (как из одной серии, так и из разных) ;
 - в отдельный файл для каждого изделия из выборки и в общий файл из выборки.
 
-+Просмотр каталога постранично, поиск по каталогу, возможно фильтры (например по серии, цене и тп).
++ Просмотр каталога постранично, поиск по каталогу, возможно фильтры (например по серии, цене и тп).
 
 Редактирование товара, возможность подгрузить фото.  Фото загружаю на сайт я(админ), файл на сайт загружаю я, иначе велик риск уничтожения всей информации.
 Выгрузка в xlsx.
-+Дизайн - стандартный шаблон.
++ Дизайн - стандартный шаблон.
+
+
+todo нужно каким-то образом запоминать ранее выбранные изображения у товаров, либо при загрузке предоставлять выбор