Browse Source

Update mobile UI and local environment config

Alexander Musikhin 3 weeks ago
parent
commit
5ba7a6629c

+ 1 - 1
.env.testing

@@ -6,7 +6,7 @@ COMPOSE_PROFILES=local
 APP_ADDR=http://127.0.0.1
 WEB_PORT=8090
 # external port of DB for connect by other soft
-DB_EXT_PORT=3306
+DB_EXT_PORT=3307
 WS_PORT=3090
 WS_ADDR=/ws
 WS_DEBUG_PORT=9229

+ 1 - 1
docker-compose.debug.yml

@@ -22,7 +22,7 @@ services:
     # Database service -------------------------------------------------------------------------------------------------
     db:
         ports:
-            - ${DB_EXT_PORT}:3306
+            - "127.0.0.1:${DB_EXT_PORT}:3306"
 
     # Websocket service ------------------------------------------------------------------------------------------------
     websocket:

+ 8 - 2
docker/nginx/conf.d/app.conf

@@ -6,10 +6,15 @@ server {
     access_log /dev/stderr;
     root /var/www/public;
     client_max_body_size 50m;
+    
+    resolver 127.0.0.11 valid=10s ipv6=off;
+    
+    set $upstream_app app;
+    
     location ~ \.php$ {
         try_files $uri =404;
         fastcgi_split_path_info ^(.+\.php)(/.+)$;
-        fastcgi_pass app:9000;
+        fastcgi_pass $upstream_app:9000;
         fastcgi_index index.php;
         include fastcgi_params;
         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
@@ -23,7 +28,8 @@ server {
     }
 
     location /ws {
-            proxy_pass http://websocket:9000;
+            set $upstream_ws websocket;
+            proxy_pass http://$upstream_ws:9000;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header Host $host;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

+ 269 - 0
resources/js/custom.js

@@ -1,4 +1,272 @@
 $(document).ready(function () {
+    function initMobileDatePicker() {
+        const mobileQuery = window.matchMedia('(max-width: 767.98px)');
+        const coarsePointer = window.matchMedia('(pointer: coarse)');
+
+        if (!mobileQuery.matches || !coarsePointer.matches) {
+            return;
+        }
+
+        const dateInputs = Array.from(document.querySelectorAll('input[type="date"]')).filter(function (input) {
+            return !input.disabled && !input.dataset.mobileDateEnhanced;
+        });
+
+        if (!dateInputs.length) {
+            return;
+        }
+
+        const monthFormatter = new Intl.DateTimeFormat('ru-RU', {
+            month: 'long',
+            year: 'numeric',
+        });
+        const displayFormatter = new Intl.DateTimeFormat('ru-RU');
+        const weekdayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
+
+        const overlay = document.createElement('div');
+        overlay.className = 'mobile-date-picker';
+        overlay.hidden = true;
+        overlay.innerHTML = '' +
+            '<div class="mobile-date-picker__dialog" role="dialog" aria-modal="true" aria-label="Выбор даты">' +
+                '<div class="mobile-date-picker__header">' +
+                    '<button type="button" class="btn btn-link mobile-date-picker__close" aria-label="Закрыть"><i class="bi bi-x-lg"></i></button>' +
+                    '<div class="mobile-date-picker__title"></div>' +
+                    '<button type="button" class="btn btn-link mobile-date-picker__today">Сегодня</button>' +
+                '</div>' +
+                '<div class="mobile-date-picker__month-nav">' +
+                    '<button type="button" class="btn btn-link mobile-date-picker__nav mobile-date-picker__nav--prev" aria-label="Предыдущий месяц"><i class="bi bi-chevron-left"></i></button>' +
+                    '<div class="mobile-date-picker__month-label"></div>' +
+                    '<button type="button" class="btn btn-link mobile-date-picker__nav mobile-date-picker__nav--next" aria-label="Следующий месяц"><i class="bi bi-chevron-right"></i></button>' +
+                '</div>' +
+                '<div class="mobile-date-picker__weekdays"></div>' +
+                '<div class="mobile-date-picker__grid"></div>' +
+                '<div class="mobile-date-picker__footer">' +
+                    '<button type="button" class="btn btn-outline-secondary mobile-date-picker__clear">Очистить</button>' +
+                    '<button type="button" class="btn btn-primary mobile-date-picker__apply">Готово</button>' +
+                '</div>' +
+            '</div>';
+        document.body.appendChild(overlay);
+
+        const titleEl = overlay.querySelector('.mobile-date-picker__title');
+        const monthLabelEl = overlay.querySelector('.mobile-date-picker__month-label');
+        const weekdaysEl = overlay.querySelector('.mobile-date-picker__weekdays');
+        const gridEl = overlay.querySelector('.mobile-date-picker__grid');
+        const closeBtn = overlay.querySelector('.mobile-date-picker__close');
+        const todayBtn = overlay.querySelector('.mobile-date-picker__today');
+        const clearBtn = overlay.querySelector('.mobile-date-picker__clear');
+        const applyBtn = overlay.querySelector('.mobile-date-picker__apply');
+        const prevBtn = overlay.querySelector('.mobile-date-picker__nav--prev');
+        const nextBtn = overlay.querySelector('.mobile-date-picker__nav--next');
+
+        weekdaysEl.innerHTML = weekdayLabels.map(function (day) {
+            return '<div class="mobile-date-picker__weekday">' + day + '</div>';
+        }).join('');
+
+        function parseIsoDate(value) {
+            if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
+                return null;
+            }
+
+            const parts = value.split('-').map(Number);
+            const date = new Date(parts[0], parts[1] - 1, parts[2]);
+
+            if (Number.isNaN(date.getTime())) {
+                return null;
+            }
+
+            return date;
+        }
+
+        function formatIsoDate(date) {
+            const year = date.getFullYear();
+            const month = String(date.getMonth() + 1).padStart(2, '0');
+            const day = String(date.getDate()).padStart(2, '0');
+
+            return year + '-' + month + '-' + day;
+        }
+
+        function formatDisplayDate(value) {
+            const date = parseIsoDate(value);
+
+            return date ? displayFormatter.format(date) : '';
+        }
+
+        function normalizeMonthLabel(date) {
+            const label = monthFormatter.format(date);
+
+            return label.charAt(0).toUpperCase() + label.slice(1);
+        }
+
+        function startOfMonth(date) {
+            return new Date(date.getFullYear(), date.getMonth(), 1);
+        }
+
+        function isSameDate(a, b) {
+            return a && b
+                && a.getFullYear() === b.getFullYear()
+                && a.getMonth() === b.getMonth()
+                && a.getDate() === b.getDate();
+        }
+
+        const state = {
+            activeInput: null,
+            hiddenInput: null,
+            displayInput: null,
+            selectedDate: null,
+            draftDate: null,
+            viewDate: startOfMonth(new Date()),
+        };
+
+        function syncDisplay(hiddenInput, displayInput) {
+            displayInput.value = formatDisplayDate(hiddenInput.value);
+            displayInput.classList.toggle('mobile-date-input--empty', !hiddenInput.value);
+        }
+
+        function closePicker() {
+            overlay.hidden = true;
+            document.body.classList.remove('mobile-date-picker-open');
+            state.activeInput = null;
+            state.hiddenInput = null;
+            state.displayInput = null;
+            state.selectedDate = null;
+            state.draftDate = null;
+        }
+
+        function renderCalendar() {
+            const today = new Date();
+            const monthStart = startOfMonth(state.viewDate);
+            const gridStart = new Date(monthStart);
+            const firstWeekday = (monthStart.getDay() + 6) % 7;
+
+            gridStart.setDate(monthStart.getDate() - firstWeekday);
+
+            monthLabelEl.textContent = normalizeMonthLabel(monthStart);
+            gridEl.innerHTML = '';
+
+            for (let index = 0; index < 42; index += 1) {
+                const day = new Date(gridStart);
+                day.setDate(gridStart.getDate() + index);
+
+                const button = document.createElement('button');
+                button.type = 'button';
+                button.className = 'mobile-date-picker__day';
+                button.textContent = String(day.getDate());
+                button.dataset.value = formatIsoDate(day);
+
+                if (day.getMonth() !== monthStart.getMonth()) {
+                    button.classList.add('is-outside');
+                }
+
+                if (isSameDate(day, today)) {
+                    button.classList.add('is-today');
+                }
+
+                if (state.draftDate && isSameDate(day, state.draftDate)) {
+                    button.classList.add('is-selected');
+                }
+
+                button.addEventListener('click', function () {
+                    state.draftDate = parseIsoDate(button.dataset.value);
+                    renderCalendar();
+                });
+
+                gridEl.appendChild(button);
+            }
+        }
+
+        function openPicker(hiddenInput, displayInput) {
+            state.activeInput = hiddenInput;
+            state.hiddenInput = hiddenInput;
+            state.displayInput = displayInput;
+            state.selectedDate = parseIsoDate(hiddenInput.value);
+            state.draftDate = state.selectedDate;
+            state.viewDate = startOfMonth(state.selectedDate || new Date());
+
+            titleEl.textContent = hiddenInput.dataset.mobileDateTitle || 'Выбор даты';
+            overlay.hidden = false;
+            document.body.classList.add('mobile-date-picker-open');
+            renderCalendar();
+        }
+
+        closeBtn.addEventListener('click', closePicker);
+
+        overlay.addEventListener('click', function (event) {
+            if (event.target === overlay) {
+                closePicker();
+            }
+        });
+
+        prevBtn.addEventListener('click', function () {
+            state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth() - 1, 1);
+            renderCalendar();
+        });
+
+        nextBtn.addEventListener('click', function () {
+            state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth() + 1, 1);
+            renderCalendar();
+        });
+
+        todayBtn.addEventListener('click', function () {
+            state.draftDate = startOfMonth(new Date());
+            state.draftDate = new Date(state.draftDate.getFullYear(), state.draftDate.getMonth(), new Date().getDate());
+            state.viewDate = startOfMonth(state.draftDate);
+            renderCalendar();
+        });
+
+        clearBtn.addEventListener('click', function () {
+            if (!state.hiddenInput || !state.displayInput) {
+                return;
+            }
+
+            state.hiddenInput.value = '';
+            state.hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
+            state.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
+            syncDisplay(state.hiddenInput, state.displayInput);
+            closePicker();
+        });
+
+        applyBtn.addEventListener('click', function () {
+            if (!state.hiddenInput || !state.displayInput || !state.draftDate) {
+                closePicker();
+                return;
+            }
+
+            state.hiddenInput.value = formatIsoDate(state.draftDate);
+            state.hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
+            state.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
+            syncDisplay(state.hiddenInput, state.displayInput);
+            closePicker();
+        });
+
+        document.addEventListener('keydown', function (event) {
+            if (event.key === 'Escape' && !overlay.hidden) {
+                closePicker();
+            }
+        });
+
+        dateInputs.forEach(function (input) {
+            input.dataset.mobileDateEnhanced = '1';
+            input.dataset.mobileDateTitle = input.closest('.row')?.querySelector('label[for="' + input.id + '"]')?.textContent.trim() || 'Выбор даты';
+
+            const displayInput = input.cloneNode(false);
+            displayInput.type = 'text';
+            displayInput.removeAttribute('name');
+            displayInput.value = '';
+            displayInput.readOnly = true;
+            displayInput.dataset.mobileDateDisplay = '1';
+            displayInput.classList.add('mobile-date-input');
+            displayInput.placeholder = 'Выберите дату';
+            input.type = 'hidden';
+            input.dataset.mobileDateHidden = '1';
+
+            input.parentNode.insertBefore(displayInput, input.nextSibling);
+            syncDisplay(input, displayInput);
+
+            displayInput.addEventListener('click', function () {
+                openPicker(input, displayInput);
+            });
+        });
+    }
+
     function cleanupStaleModalState() {
         if (!document.querySelector('.modal.show')) {
             document.querySelectorAll('.modal-backdrop').forEach(function (backdrop) {
@@ -98,6 +366,7 @@ $(document).ready(function () {
     }
 
     cleanupStaleModalState();
+    initMobileDatePicker();
     window.addEventListener('pageshow', cleanupStaleModalState);
 
     if ($('.main-alert').length) {

+ 14 - 9
resources/views/catalog/index.blade.php

@@ -2,22 +2,27 @@
 
 @section('content')
 
-    <div class="row mb-2 catalog">
-        <div class="col-md-4">
+    <div class="row mb-2 catalog page-header-row">
+        <div class="col-12 col-md-4 page-header-title">
             <h3>Каталог</h3>
         </div>
-        <div class="col-md-4 text-center">
+        <div class="col-auto col-md-4 text-md-center page-header-year">
             @include('partials.year-switcher')
         </div>
-        <div class="col-md-4 text-end">
+        <div class="col-auto col-md-4 text-md-end page-header-actions">
             @if(hasRole('admin'))
-                <button type="button" class="btn btn-sm mb-1 btn-primary" data-bs-toggle="modal" data-bs-target="#importModal">
-                    Импорт
+                <button type="button" class="btn btn-sm mb-1 btn-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#importModal" aria-label="Импорт">
+                    <i class="bi bi-upload page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Импорт</span>
                 </button>
-                <button type="button" class="btn btn-sm mb-1 btn-primary" data-bs-toggle="modal" data-bs-target="#exportModal">
-                    Экспорт
+                <button type="button" class="btn btn-sm mb-1 btn-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#exportModal" aria-label="Экспорт">
+                    <i class="bi bi-download page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Экспорт</span>
                 </button>
-                <a href="{{ route('catalog.create') }}" class="btn btn-sm mb-1 btn-primary">Добавить</a>
+                <a href="{{ route('catalog.create') }}" class="btn btn-sm mb-1 btn-primary page-action-btn" aria-label="Добавить">
+                    <i class="bi bi-plus-lg page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Добавить</span>
+                </a>
             @endif
         </div>
     </div>

+ 10 - 8
resources/views/maf_orders/index.blade.php

@@ -2,21 +2,23 @@
 
 @section('content')
 
-    <div class="row mb-2">
-        <div class="col-md-4">
+    <div class="row mb-2 page-header-row">
+        <div class="col-12 col-md-4 page-header-title">
             <h3>Заказы МАФ</h3>
         </div>
-        <div class="col-md-4 text-center">
+        <div class="col-auto col-md-4 text-md-center page-header-year">
             @include('partials.year-switcher')
         </div>
-        <div class="col-md-4 text-end">
+        <div class="col-auto col-md-4 text-md-end page-header-actions">
             @if(hasRole('admin,assistant_head'))
-                <button type="button" class="btn btn-sm btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#setOrderInStockModal">
-                    Весь заказ на складе
+                <button type="button" class="btn btn-sm btn-outline-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#setOrderInStockModal" aria-label="Весь заказ на складе">
+                    <i class="bi bi-box-seam page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Весь заказ на складе</span>
                 </button>
             @endif
-            <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
-                Добавить
+            <button type="button" class="btn btn-sm btn-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#addModal" aria-label="Добавить">
+                <i class="bi bi-plus-lg page-action-btn__icon"></i>
+                <span class="page-action-btn__label">Добавить</span>
             </button>
 
         </div>

+ 12 - 6
resources/views/orders/index.blade.php

@@ -1,19 +1,25 @@
 @extends('layouts.app')
 
 @section('content')
-    <div class="row mb-2">
-        <div class="col-md-4">
+    <div class="row mb-2 page-header-row">
+        <div class="col-12 col-md-4 page-header-title">
             <h3>Площадки</h3>
         </div>
-        <div class="col-md-4 text-center">
+        <div class="col-auto col-md-4 text-md-center page-header-year">
             @include('partials.year-switcher')
         </div>
-        <div class="col-md-4 text-end">
+        <div class="col-auto col-md-4 text-md-end page-header-actions">
             @if(hasRole('admin'))
-                <a href="{{ route('order.create') }}" class="btn btn-sm btn-primary">Создать</a>
+                <a href="{{ route('order.create') }}" class="btn btn-sm btn-primary page-action-btn" aria-label="Создать">
+                    <i class="bi bi-plus-lg page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Создать</span>
+                </a>
             @endif
             @if(hasRole('admin,manager'))
-                <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#exportOrdersModal">Экспорт</button>
+                <button type="button" class="btn btn-sm btn-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#exportOrdersModal" aria-label="Экспорт">
+                    <i class="bi bi-download page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Экспорт</span>
+                </button>
             @endif
         </div>
     </div>

+ 10 - 8
resources/views/products_sku/index.blade.php

@@ -2,20 +2,22 @@
 
 @section('content')
 
-    <div class="row mb-2">
-        <div class="col-md-4">
+    <div class="row mb-2 page-header-row">
+        <div class="col-12 col-md-4 page-header-title">
             <h3>МАФ</h3>
         </div>
-        <div class="col-md-4 text-center">
+        <div class="col-auto col-md-4 text-md-center page-header-year">
             @include('partials.year-switcher')
         </div>
-        <div class="col-md-4 text-end">
+        <div class="col-auto col-md-4 text-md-end page-header-actions">
             @if(hasRole('admin'))
-                <button type="button" class="btn btn-sm mb-1 btn-primary" data-bs-toggle="modal" data-bs-target="#importModal">
-                    Импорт
+                <button type="button" class="btn btn-sm mb-1 btn-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#importModal" aria-label="Импорт">
+                    <i class="bi bi-upload page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Импорт</span>
                 </button>
-                <button type="button" class="btn btn-sm mb-1 btn-primary" data-bs-toggle="modal" data-bs-target="#exportModal">
-                    Экспорт
+                <button type="button" class="btn btn-sm mb-1 btn-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#exportModal" aria-label="Экспорт">
+                    <i class="bi bi-download page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Экспорт</span>
                 </button>
             @endif
         </div>

+ 7 - 4
resources/views/reclamations/index.blade.php

@@ -2,13 +2,16 @@
 
 @section('content')
 
-    <div class="row mb-2">
-        <div class="col-md-6">
+    <div class="row mb-2 page-header-row">
+        <div class="col-12 col-md-6 page-header-title">
             <h3>Рекламации</h3>
         </div>
-        <div class="col-md-6 text-end">
+        <div class="col-auto ms-md-auto page-header-actions">
             @if(hasRole('admin,manager'))
-                <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#exportReclamationsModal">Экспорт</button>
+                <button type="button" class="btn btn-sm btn-primary page-action-btn" data-bs-toggle="modal" data-bs-target="#exportReclamationsModal" aria-label="Экспорт">
+                    <i class="bi bi-download page-action-btn__icon"></i>
+                    <span class="page-action-btn__label">Экспорт</span>
+                </button>
             @endif
 
         </div>

+ 3 - 3
resources/views/reports/index.blade.php

@@ -16,11 +16,11 @@
 @endphp
 
 @section('content')
-    <div class="row mb-2">
-        <div class="col-md-6">
+    <div class="row mb-2 page-header-row">
+        <div class="col-12 col-md-6 page-header-title">
             <h3>Отчеты</h3>
         </div>
-        <div class="col-md-6 text-center">
+        <div class="col-auto col-md-6 text-md-center page-header-year">
             @include('partials.year-switcher')
         </div>
     </div>