Bladeren bron

Import product catalog from xls file

Alexander Musikhin 11 maanden geleden
bovenliggende
commit
69bc9853f4

+ 11 - 0
app/Helpers/Price.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Helpers;
+
+class Price
+{
+    public static function format(float $price): string
+    {
+        return number_format($price, 2, '.', ' ') . '₽';
+    }
+}

+ 7 - 3
app/Http/Controllers/ProductController.php

@@ -4,7 +4,7 @@ namespace App\Http\Controllers;
 
 use App\Jobs\Import\ImportCatalog;
 use App\Models\Product;
-use Exception;
+use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
@@ -17,9 +17,9 @@ class ProductController extends Controller
         'title'     => 'Каталог',
     ];
 
-    public function index()
+    public function index(Request $request)
     {
-        $this->data['products'] = Product::query()->paginate();
+        $this->data['products'] = Product::query()->paginate()->withQueryString();
         return view('catalog.index', $this->data);
     }
 
@@ -28,6 +28,10 @@ class ProductController extends Controller
 
     }
 
+    /**
+     * @param Request $request
+     * @return RedirectResponse
+     */
     public function import(Request $request)
     {
         // validate data

+ 1 - 0
app/Models/Product.php

@@ -11,6 +11,7 @@ class Product extends Model
 {
     use SoftDeletes;
     protected $fillable = [
+        'year',
         'name_tz',
         'type_tz',
         'nomenclature_number',

+ 2 - 1
app/Providers/AppServiceProvider.php

@@ -2,6 +2,7 @@
 
 namespace App\Providers;
 
+use Illuminate\Pagination\Paginator;
 use Illuminate\Support\ServiceProvider;
 
 class AppServiceProvider extends ServiceProvider
@@ -19,6 +20,6 @@ class AppServiceProvider extends ServiceProvider
      */
     public function boot(): void
     {
-        //
+        Paginator::useBootstrapFive();
     }
 }

+ 32 - 10
app/Services/ImportService.php

@@ -5,7 +5,7 @@ namespace App\Services;
 use App\Models\Product;
 use Exception;
 use Illuminate\Support\Facades\Storage;
-use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
 
 class ImportService
 {
@@ -30,23 +30,30 @@ class ImportService
 
 
     /**
+     * @param string $path
+     * @param int $year
+     * @return void
      * @throws Exception
      */
     public function handle(string $path, int $year): void
     {
         $path = Storage::disk('upload')->path($path);
-        $xls = IOFactory::createReaderForFile($path);
-        $xls->setReadDataOnly(true);
-        $sheet = $xls->load($path);
 
-        $records = $sheet->setActiveSheetIndex(0)->toArray();
+        $reader = new Xlsx();
+        $spreadsheet = $reader->load($path);
+        $sheet = $spreadsheet->getActiveSheet();
 
-        $data = [];
+        $rowIterator = $sheet->getRowIterator();
+
+        $headers = $this->rowToArray($rowIterator->current());
+
+
+
+        if($this->checkHeaders($headers)) {
+            foreach ($rowIterator as $row){
+                $record = $this->rowToArray($row);
+                if($record[0] === 'Фото') continue;
 
-        $err = [];
-        if($this->checkHeaders($records[0])) {
-            foreach ($records as $k => $record){
-                if($k === 0) continue;
                 Product::query()
                     ->updateOrCreate(['year' => $year, 'nomenclature_number' => $record[3]],
                     [
@@ -71,7 +78,22 @@ class ImportService
         }
     }
 
+    protected function rowToArray($row): array
+    {
+        $cellIterator = $row->getCellIterator();
+        $cellIterator->setIterateOnlyExistingCells(FALSE); // This loops through all cells, even if a cell value is not set.
+        $row_content = [];
+        foreach ($cellIterator as $cell) {
+            $row_content[] = $cell->getValue();
+        }
+
+        return $row_content;
+    }
 
+    /**
+     * @param array $headers
+     * @return bool
+     */
     protected function checkHeaders(array $headers): bool
     {
         return $this->getHeaders() == $headers;

+ 1 - 1
docker/nginx/conf.d/app.conf

@@ -36,7 +36,7 @@ server {
     location / {
       root   /var/frontend;
       index  index.php;
-      try_files $uri $uri/ /index.php;
+      try_files $uri $uri/ /index.php?$query_string;
     }
 
 #     location /api {

+ 2 - 0
resources/js/app.js

@@ -1 +1,3 @@
 import './bootstrap';
+
+import './custom.js'

+ 53 - 0
resources/js/custom.js

@@ -0,0 +1,53 @@
+$(document).ready(function () {
+    if ($('.main-alert').length) {
+        setTimeout(function () {
+            $('.main-alert').fadeTo(2000, 500).slideUp(500, function () {
+                $(".main-alert").slideUp(500);
+            })
+        }, 3000);
+    }
+
+    let user = localStorage.getItem('user');
+    if (user > 0) {
+        let socket = new WebSocket(localStorage.getItem('socketAddress'));
+        socket.onopen = function () {
+            console.log("[WS] Connected. Listen messages for user " + user);
+        };
+
+        socket.onmessage = function (event) {
+            let received = JSON.parse(event.data);
+            if (parseInt(received.data.user_id) === parseInt(user)) {
+                console.log(received);
+                console.log(`[WS] Received data action: ${received.data.action}. Message: ${received.data.message}`);
+                setTimeout(function () {
+                        if (received.data.payload.error) {
+                            $('.alerts').append('<div class="main-alert2 alert alert-danger" role="alert">' + received.data.message + '</div>');
+                        } else {
+                            $('.alerts').append('<div class="main-alert2 alert alert-success" role="alert">' + received.data.message + '</div>');
+                        }
+                        setTimeout(function () {
+                            $('.main-alert2').fadeTo(2000, 500).slideUp(500, function () {
+                                $(".main-alert2").slideUp(500);
+                            })
+                        }, 3000);
+                    }, 1000
+                );
+
+
+            }
+
+        };
+
+        socket.onclose = function (event) {
+            if (event.wasClean) {
+                console.log(`[WS] Closed clear, code=${event.code} reason=${event.reason}`);
+            } else {
+                console.log('[WS] Connection lost', event);
+            }
+        };
+        socket.onerror = function (error) {
+            console.log(`[error] ${error}`);
+        };
+
+    }
+});

+ 20 - 0
resources/sass/app.scss

@@ -27,3 +27,23 @@
     padding-right: 3rem;
   }
 }
+
+
+
+.tbl {
+  .row:nth-child(even) {
+    background-color: #f7f7f7;
+  }
+  .tbl-item {
+    cursor: pointer;
+  }
+
+  .tbl-item:hover {
+    background-color: #fff8ca;
+  }
+
+}
+
+.pagination .text-muted {
+  padding-right: 1rem;
+}

+ 87 - 39
resources/views/catalog/index.blade.php

@@ -1,48 +1,96 @@
 @extends('layouts.app')
 
 @section('content')
-    <form action="{{ route('catalog.import') }}" method="post" enctype="multipart/form-data">
-        @csrf
-        <input class="form-control mb-3" required type="number" name="year" min="2000" max="{{ (int)date('Y') + 1 }}" placeholder="2020" value="{{ date('Y') }}">
-        <input class="form-control mb-3" required type="file" name="import_file">
-        <button type="submit" class="btn btn-primary">Импорт</button>
-    </form>
-
-    <div class="px-md-3 px-2">
-        <div class="table">
-            <div class="row d-none d-md-flex header px-5 py-4 fw-bold">
-                <div class="col-xl-3">Фото</div>
-                <div class="col-xl-3">Наименование</div>
-                <div class="col-xl-3">фывфывфыв</div>
-                <div class="col-xl-3 text-end">Действия</div>
+
+    <div class="row mb-3">
+        <div class="col-6">
+            <h3>Каталог</h3>
+        </div>
+        <div class="col-6 text-end">
+            <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#importModal">
+                Импорт
+            </button>
+        </div>
+    </div>
+
+    <div class="tbl">
+        <div class="row d-none d-md-flex header px-2 py-3 fw-bold bg-primary-subtle">
+            <div class="col-xl-1">Год, ном. номер, артикул</div>
+            <div class="col-xl-2">Производитель, наименование ТЗ</div>
+            <div class="col-xl-3">Типы</div>
+            <div class="col-xl-1">Размеры, ед.изм.</div>
+            <div class="col-xl-1">Прайс</div>
+            <div class="col-xl-4">Примечания</div>
+        </div>
+
+        @foreach($products as $product)
+            <div class="row px-2 py-2 align-items-center tbl-item" onclick="location.href='{{ route('catalog.show', $product->id) }}'">
+                <div class="col-xl-1">
+                    {{ $product->year }}<br>
+                    {{ $product->nomenclature_number }}<br>
+                    {{ $product->article }}<br>
+                </div>
+                <div class="col-xl-2">
+                    {{ $product->manufacturer }}<br>
+                    {{ $product->name_tz }}<br>
+                </div>
+                <div class="col-xl-3">
+                    {{ $product->type_tz }}<br>
+                    {{ $product->type }}<br>
+                    {{ $product->manufacturer_name }}
+                </div>
+                <div class="col-xl-1">
+                    {{ $product->sizes }}<br>
+                    {{ $product->unit }}<br>
+                </div>
+
+                <div class="col-xl-1">
+                    {{ $product->price_status }}<br>
+                    {{ $product->product_price_txt }}<br>
+                    {{ $product->installation_price_txt }}<br>
+                    {{ $product->service_price_txt }}<br>
+                    {{ $product->total_price_txt }}
+                </div>
+                <div class="col-xl-4" title="{!! nl2br($product->note) !!}">
+                    {!! Str::words(nl2br($product->note), 15) !!}
+                </div>
+
             </div>
+        @endforeach
 
-{{--            @foreach($users as $user)--}}
-{{--                <div class="row px-md-5 px-2 py-4 align-items-center">--}}
-{{--                    <div class="col-xl-3 mb-3">--}}
-{{--                        <span class="ps-2">{{ $user->name }}</span>--}}
-{{--                    </div>--}}
-{{--                    <div class="col-xl-3">--}}
-{{--                        <span class="d-md-none fw-bold">E-mail: </span>{{ $user->email }}--}}
-{{--                    </div>--}}
-{{--                    <div class="col-xl-3">--}}
-{{--                        <span class="d-md-none fw-bold">Роль: </span>{{ roleName($user->role) }}--}}
-{{--                    </div>--}}
-{{--                    <div class="col-xl-3 text-end">--}}
-{{--                        <a href="{{ route('user.show', $user->id) }}"><i class="bi bi-pencil fs-5 text-primary"></i></a>--}}
-{{--                        <a href="#" onclick="if(confirm('Удалить пользователя {{ $user->name }}?')) { document.getElementById('delete-user-{{ $user->id }}').submit(); } ">--}}
-{{--                            <i class="bi bi-trash fs-5 text-danger"></i>--}}
-{{--                        </a>--}}
-{{--                        <form action="{{ route('user.destroy', $user->id) }}" method="post" class="visually-hidden d-none" id="delete-user-{{ $user->id }}">--}}
-{{--                            @csrf--}}
-{{--                            @method('DELETE')--}}
-{{--                        </form>--}}
-{{--                    </div>--}}
-{{--                </div>--}}
-{{--            @endforeach--}}
+    </div>
+    <div class="row pt-3 px-3">
+        <div class="col-12 pagination">
+            {{ $products->links() }}
         </div>
     </div>
-    @dump($products)
-    @dump($errors)
+
+
+
+
+
+    <!-- Модальное окно -->
+    <div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
+        <div class="modal-dialog">
+            <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('catalog.import') }}" method="post" enctype="multipart/form-data">
+                        @csrf
+                        @include('partials.input', ['title' => 'Год', 'name' => 'year', 'type' => 'number', 'min' => 2000, 'max' => (int) date('Y') + 1, 'value' => date('Y')])
+                        @include('partials.input', ['title' => 'XLSX файл', 'name' => 'import_file', 'type' => 'file'])
+                        @include('partials.submit', ['name' => 'Импорт'])
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    @if($errors->count())
+        @dump($errors)
+    @endif
 @endsection
 

+ 6 - 42
resources/views/layouts/app.blade.php

@@ -33,7 +33,7 @@
 
     <div id="app">
         <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
-            <div class="container">
+            <div class="container-fluid">
                 <a class="navbar-brand" href="{{ url('/') }}">
                     {{ config('app.name', 'Laravel') }}
                 </a>
@@ -88,7 +88,7 @@
         </nav>
 
         <main class="py-4">
-            <div class="container">
+            <div class="container-fluid">
                 @yield('content')
             </div>
         </main>
@@ -96,46 +96,10 @@
 
     @stack('scripts')
     <script type="module">
-        $(document).ready(function (){
-            if($('.main-alert').length){
-                setTimeout(function (){
-                    $('.main-alert').fadeTo(2000, 500).slideUp(500, function (){
-                        $(".main-alert").slideUp(500);
-                    })
-                }, 3000);
-            }
-
-            let user = {{ auth()->user()?->id ?? 0}}; // zero is not logged in user
-
-            if(user > 0) {
-                let socket = new WebSocket('{{ config('app.addr') . ':' . config('app.ws_port') }}');
-                socket.onopen = function() {
-                    console.log("[WS] Connected. Listen messages for user " + user);
-                };
-
-                socket.onmessage = function(event) {
-                    let received = JSON.parse(event.data);
-                    if(parseInt(received.data.user_id) === user) {
-                        console.log(received);
-                        console.log(`[WS] Received data action: ${received.data.action}. Message: ${received.data.message}`);
-                        alert(received.data.message);
-                    }
-
-                };
-
-                socket.onclose = function(event) {
-                    if (event.wasClean) {
-                        console.log(`[WS] Closed clear, code=${event.code} reason=${event.reason}`);
-                    } else {
-                        console.log('[WS] Connection lost', event);
-                    }
-                };
-                socket.onerror = function(error) {
-                    console.log(`[error] ${error}`);
-                };
-
-            }
-        });
+        let user = {{ auth()->user()?->id ?? 0}};
+        let socketAddress = '{{ config('app.addr') . ':' . config('app.ws_port') }}';
+        localStorage.setItem('user', user);
+        localStorage.setItem('socketAddress', socketAddress);
     </script>
 
 </body>

+ 1 - 1
resources/views/partials/submit.blade.php

@@ -1,6 +1,6 @@
 <div class="row">
     <div class="@if(!($right ?? null)) offset-md-{{ $offset ?? 4 }} col-md-8 @endif">
-        <button type="submit" class="btn btn-info sbmt text-white">{{ $name ?? 'Сохранить' }}</button>
+        <button type="submit" class="btn btn-primary sbmt text-white">{{ $name ?? 'Сохранить' }}</button>
     </div>
 </div>
 

+ 1 - 1
resources/views/users/profile.blade.php

@@ -11,7 +11,7 @@
 
             <div class="row pwd-change-link mb-3">
                 <div class="offset-md-4">
-                    <a onclick="$('.pwg-change').removeClass('d-none'); $('.pwd-change-link').addClass('d-none');" class="text-info" href="#">Сменить пароль</a>
+                    <a onclick="$('.pwg-change').removeClass('d-none'); $('.pwd-change-link').addClass('d-none');" class="text-secondary" href="#">Сменить пароль</a>
                 </div>
             </div>
 

+ 4 - 2
todo.md

@@ -1,7 +1,9 @@
 - [x] пользователи и роли
 - [x] управление пользователями (CRUD)
-- [ ] каталог товаров
-- [ ] импорт товаров
+- [x] каталог товаров
+- [ ] каталог товаров фильтры, поиск, сортировка
+- [ ] каталог товаров экспорт
+- [x] импорт товаров
 - [ ] складские остатки товаров
 - [ ] заказы (площадки) (сюда же входят клиенты, если нужна отдельная сущность)
 - [ ] заказы поставщику