6 Commits dfed188583 ... 931f18458a

Author SHA1 Message Date
  Alexander Musikhin 931f18458a refactored docs 2 months ago
  Alexander Musikhin 16cf697bf7 refactored spare parts 2 months ago
  Alexander Musikhin 84faaef1d8 refactored menu 2 months ago
  Alexander Musikhin 873ad859a1 added template for common catalog 2 months ago
  Alexander Musikhin 3ab5ac075e refactoring documentation 2 months ago
  Alexander Musikhin e93d90b803 refactoring documentation 3 months ago

+ 1 - 1
.gitignore

@@ -1,5 +1,5 @@
 /docker/database
-/.DS_Store
+*.DS_Store*
 /.phpunit.cache
 /node_modules
 /public/build

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

@@ -9,7 +9,7 @@ use Illuminate\Http\Request;
 class PricingCodeController extends Controller
 {
     protected array $data = [
-        'active' => 'spare_parts',
+        'active' => 'pricing_codes',
         'title' => 'Справочник расшифровок',
         'id' => 'pricing_codes',
         'header' => [

+ 2 - 0
app/Http/Controllers/SparePartController.php

@@ -128,6 +128,8 @@ class SparePartController extends Controller
 
     public function help()
     {
+        $this->data['active'] = 'spare_parts_help';
+        $this->data['title'] = 'Справка по модулю «Запчасти»';
         $markdownPath = base_path('docs/spare-parts.md');
         $markdown = File::exists($markdownPath) ? File::get($markdownPath) : '# Справка не найдена';
         $this->data['helpContent'] = Str::markdown($markdown, [

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

@@ -8,7 +8,7 @@ use Illuminate\Http\Request;
 class SparePartInventoryController extends Controller
 {
     protected array $data = [
-        'active' => 'spare_parts',
+        'active' => 'spare_part_inventory',
         'title' => 'Контроль наличия запчастей',
         'id' => 'spare_part_inventory',
     ];

+ 5 - 5
app/Http/Controllers/SparePartOrderController.php

@@ -15,8 +15,8 @@ use Illuminate\Validation\ValidationException;
 class SparePartOrderController extends Controller
 {
     protected array $data = [
-        'active' => 'spare_parts',
-        'title' => 'Заказы деталей',
+        'active' => 'spare_part_orders',
+        'title' => 'Заказы запчастей',
         'id' => 'spare_part_orders',
         'header' => [
             'id' => 'ID',
@@ -149,7 +149,7 @@ class SparePartOrderController extends Controller
             $nav,
             route('spare_part_orders.index', session('gp_spare_part_orders'))
         );
-        return redirect()->to($backUrl)->with(['success' => 'Заказ детали успешно создан!']);
+        return redirect()->to($backUrl)->with(['success' => 'Заказ запчасти успешно создан!']);
     }
 
     public function update(StoreSparePartOrderRequest $request, SparePartOrder $sparePartOrder): RedirectResponse
@@ -162,7 +162,7 @@ class SparePartOrderController extends Controller
             $nav,
             route('spare_part_orders.index', session('gp_spare_part_orders'))
         );
-        return redirect()->to($backUrl)->with(['success' => 'Заказ детали успешно обновлён!']);
+        return redirect()->to($backUrl)->with(['success' => 'Заказ запчасти успешно обновлён!']);
     }
 
     public function destroy(Request $request, SparePartOrder $sparePartOrder): RedirectResponse
@@ -176,7 +176,7 @@ class SparePartOrderController extends Controller
         $sparePartOrder->delete();
 
         return redirect()->route('spare_part_orders.index', session('gp_spare_part_orders'))
-            ->with(['success' => 'Заказ детали успешно удалён!']);
+            ->with(['success' => 'Заказ запчасти успешно удалён!']);
     }
 
     /**

+ 20 - 0
app/Http/Controllers/UnderDevelopmentController.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Contracts\View\View;
+use Illuminate\Http\Request;
+
+class UnderDevelopmentController extends Controller
+{
+    public function __invoke(Request $request): View
+    {
+        $title = (string) $request->route('title', 'Раздел');
+
+        return view('development.index', [
+            'active' => (string) $request->route('active', 'development'),
+            'title' => $title,
+            'sectionTitle' => $title,
+        ]);
+    }
+}

+ 8 - 1
config/access_routes.php

@@ -3,6 +3,9 @@
 return [
     'exact' => [
         'area.ajax-get-areas-by-district' => 'areas.ajax.view',
+        'calculations.index' => true,
+        'common-catalog.index' => true,
+        'documents.index' => true,
         'getFilters' => 'filters.view',
         'notifications.index' => true,
         'notifications.read-all' => true,
@@ -11,6 +14,10 @@ return [
         'product.search' => 'catalog.search',
         'pricing_codes.get_description' => 'pricing_codes.search',
         'pricing_codes.search' => 'pricing_codes.search',
+        'schedule.orders' => true,
+        'schedule.deliveries' => true,
+        'stock.index' => true,
+        'stock.orders' => true,
         'user.profile' => 'profile.view',
         'profile.store' => 'profile.update',
         'profile.delete' => 'profile.delete',
@@ -137,7 +144,7 @@ return [
             'generate-reclamation-pack' => 'reclamations.documents.generate',
         ],
         'pricing_codes.' => [
-            'index' => 'pricing_codes.view',
+            'index' => ['pricing_codes.view', 'spare_parts.view'],
             'store' => 'pricing_codes.create',
             'update' => 'pricing_codes.update',
             'destroy' => 'pricing_codes.delete',

+ 10 - 9
docs/refactor/menu.md

@@ -91,12 +91,14 @@ Manager
 
 Статус: **Частично**.
 
-Текущий раздел рекламаций есть, но в новом меню должен иметь внутренние вкладки/подпункты `Все` и `ДКР`.
+Текущий раздел рекламаций есть, но в новом интерфейсе должен иметь отдельные вкладки `Все` и `ДКР`. Вкладка `ДКР` обязательна для отделения рекламаций, по которым формируется документация для оплаты.
 
 | Пункт | Статус | Текущий маршрут | Комментарий |
 |---|---|---|---|
-| Все | Есть | `reclamations.index` | Текущий общий список рекламаций. |
-| ДКР | Частично | `reclamations.index` | Текущий список с предустановленным фильтром по источнику `ДКР`. |
+| Все | Есть | `reclamations.index` | Общий список всех рекламаций. |
+| ДКР | Частично | `reclamations.index` | Отдельная вкладка текущего списка с обязательным фильтром по типу `dkr` (`ДКР`); только здесь доступны рекламации для формирования платежных документов. |
+
+Рекламации из `График заказов` создаются из карточки заказа после выбора одного или нескольких МАФ. Открывается текущая форма создания, назначается тип `other` (`Прочее`) из справочника `reclamation_types`; такие рекламации не выводятся во вкладке `ДКР`, и для них недоступна документация для оплаты.
 
 ### Запчасти
 
@@ -143,11 +145,11 @@ Manager
 
 | Пункт | Статус | Текущий маршрут | Комментарий |
 |---|---|---|---|
-| Позиции | Нужно реализовать | - | Новый общий справочник. Нужны колонки по шаблону калькулятора, импорт/экспорт. |
+| Позиции | Нужно реализовать | - | Новый общий справочник. Состав и порядок колонок берутся из `docs/refactor/Шаблон Каталог ощий.xlsx`, нужны импорт/экспорт. |
 | Карточка позиции | Нужно реализовать | - | Центральная карточка общей позиции/МАФ. |
 | Фото МАФ | Нужно реализовать | - | Фото позиции общего каталога. |
 | Документы | Нужно реализовать | - | Сопутствующие документы позиции через текущий файловый механизм. |
-| Техническое описание | Уточнить / встроить | - | Вероятно поглощается общим каталогом как выгрузка/представление позиции в нужном формате. |
+| Техническое описание | Нужно реализовать внутри карточки | - | Встроено в общий каталог как выгрузка/представление позиции в нужном формате. |
 | Калькуляция | Нужно реализовать | - | API-запрос по артикулу и отображение возвращенной калькуляции, если она есть. |
 
 ### Документация
@@ -158,9 +160,9 @@ Manager
 
 ### Технич. описание
 
-Статус: **Уточнить / вероятно встроить в каталог**.
+Статус: **Встроить в каталог**.
 
-По новым вводным это, вероятно, не отдельный модуль, а часть `Каталог общий`: по сути те же данные позиции, но с выгрузками в определенном формате. Есть готовый Laravel-модуль, из которого можно взять экспорт.
+Это не отдельный пункт меню и не самостоятельный модуль. Функциональность технического описания должна быть доступна из карточки позиции `Каталог общий`: по сути это те же данные позиции, но с выгрузками в определенном формате. Есть готовый Laravel-модуль, из которого нужно взять экспорт.
 
 ### Калькуляции
 
@@ -199,5 +201,4 @@ Manager
 
 ## Открытые вопросы
 
-1. Оставлять ли `Технич. описание` отдельным пунктом меню или полностью встроить в `Каталог общий`?
-2. Какой точный контракт внешнего API калькуляций?
+1. Какой точный контракт внешнего API калькуляций?

+ 150 - 63
docs/refactor/plan.md

@@ -36,10 +36,13 @@
 - [x] Реорганизацию меню делаем сразу общей для всей платформы Manager 2.0.
 - [x] Заглушки новых модулей показываем всем авторизованным пользователям.
 - [x] На новые модули выбранный год не влияет.
-- [x] `Технич. описание`, вероятно, будет поглощено модулем `Каталог общий`; готовый Laravel-модуль используем как источник экспорта.
+- [x] `Технич. описание` встроено в модуль `Каталог общий`; готовый Laravel-модуль используем как источник экспорта.
 - [x] `Калькуляции` работают через внешний API: запрос по артикулу, возврат калькуляции при наличии.
-- [x] `Рекламации -> ДКР` реализуется как фильтр текущего списка.
-- [x] Источник рекламации хранится отдельным полем: при создании из площадки — `ДКР`, иначе `Прочее`.
+- [x] `Рекламации -> ДКР` реализуется как отдельная вкладка на основе текущего списка с обязательным фильтром по типу `dkr`.
+- [x] Тип рекламации хранится через обязательную связь со справочником `reclamation_types`; начальные типы `ДКР` и `Прочее` создаются сидером.
+- [x] В интерфейсе рекламаций обязательны отдельные вкладки `Все` и `ДКР`.
+- [x] Из графика заказов рекламация создается по выбранным МАФ через текущую форму и получает тип `other` (`Прочее`).
+- [x] Документация для оплаты доступна только для рекламаций типа `ДКР`.
 - [x] Маршруты `Каталог общий` используют префикс `common-catalog/...`.
 - [x] Таблицы и права `Каталог общий` используют техническое имя `common-catalog`.
 - [x] Техническое имя модуля `Склад наличие`: `stock`.
@@ -62,40 +65,113 @@
 - [x] Написать отдельное ТЗ для модуля `Калькуляции`: [tz-calculations.md](tz-calculations.md).
 - [x] Написать отдельное ТЗ для модуля `Администратор`: [tz-admin.md](tz-admin.md).
 
-## Этап 1. Реорганизация меню
-
-- [ ] Проверить текущие permissions для всех существующих пунктов меню.
-- [ ] Для новых пунктов-заглушек открыть доступ всем авторизованным пользователям.
-- [ ] Добавить общий механизм страницы `В разработке`.
-- [ ] Добавить маршруты для новых разделов-заглушек.
-- [ ] Перестроить `resources/views/layouts/menu.blade.php` под новую древовидную структуру.
-- [ ] Сгруппировать существующие пункты ДКР внутрь верхнего меню `ДКР`.
-- [ ] Оставить `Каталог` внутри меню `ДКР`.
-- [ ] Добавить отдельный верхний пункт `Каталог общий`.
-- [ ] Сгруппировать `График монтажей` внутрь верхнего меню `Графики`.
-- [ ] Добавить заглушки `График заказов` и `График доставок`.
-- [ ] Разложить `Рекламации` на `Все` и `ДКР`.
-- [ ] Оставить `Запчасти` отдельной верхней группой с текущими вкладками.
-- [ ] Добавить заглушки `Склад наличие -> Наличие` и `Склад наличие -> Заказы`.
-- [ ] Добавить заглушки `Документация`, `Каталог общий`, `Технич. описание`, `Калькуляции`.
-- [ ] Проверить, что выбранный год не скрывает и не фильтрует новые модули.
-- [ ] Переименовать группу `Администрирование` в `Администратор`.
-- [ ] Проверить отображение меню для основных ролей.
-
-## Этап 2. Каталог общий
+## Статус готовности этапов
+
+| Этап | Статус | Почему |
+|---|---|---|
+| 1. Реорганизация меню и заглушки | Реализован и проверен | Новое меню и заглушки приняты по результатам пользовательской проверки. |
+| 2. Адаптация существующих разделов после переноса | Реализован, ожидает пользовательской проверки | Рабочие разделы проверены, навигация запчастей и доступ к справочнику уточнены, регрессионные тесты проходят. |
+| 3. Рекламации: вкладки и тип | Готов к реализации | Решены вкладки `Все`/`ДКР`, справочник типов, начальные значения и запрет платежных документов для `Прочее`. |
+| 4. Общий каталог: ядро | Готов к реализации | Есть маршрутный префикс, техническое имя, шаблон Excel, состав колонок и предварительные типы данных. |
+| 5. Документация | Готов к реализации | Решены дерево папок, наследование прав, версии и хранение файлов. |
+| 6. Склад наличие: ядро | Готов после ядра общего каталога | Логика берется из `Запчасти`, но позиции должны браться из `Каталог общий`. PDF-экспорт остается отдельным открытым подпунктом. |
+| 7. Техническое описание | Готово после ядра общего каталога и анализа готового Laravel-модуля | Функция встроена в карточку общего каталога, но нужно получить модуль и форматы выгрузки. |
+| 8. Калькуляции | Не готово к полной реализации | Нужен контракт внешнего API. Можно сделать только место/кнопку в карточке общего каталога. |
+| 9. Графики заказов и доставок | Не готово к полной реализации | Нужны формат обмена с 1С, поля, статусы и правила доставок/отгрузок. |
+| 10. Рекламации из графика заказов | Готово только после графика заказов | Сценарий согласован, но точка входа зависит от карточки заказа в графике. |
+
+## Этап 1. Реорганизация меню и заглушки
+
+Статус: **реализован и проверен пользователем**.
+
+- [x] Проверить текущие permissions для всех существующих пунктов меню.
+- [x] Для новых пунктов-заглушек открыть доступ всем авторизованным пользователям.
+- [x] Добавить общий механизм страницы `В разработке`.
+- [x] Добавить маршруты для новых разделов-заглушек.
+- [x] Перестроить `resources/views/layouts/menu.blade.php` под новую древовидную структуру.
+- [x] Сгруппировать существующие пункты ДКР внутрь верхнего меню `ДКР`.
+- [x] Оставить `Каталог` внутри меню `ДКР`.
+- [x] Добавить отдельный верхний пункт `Каталог общий`.
+- [x] Сгруппировать `График монтажей` внутрь верхнего меню `Графики`.
+- [x] Добавить заглушки `График заказов` и `График доставок`.
+- [x] Разложить `Рекламации` на `Все` и `ДКР`.
+- [x] Оставить `Запчасти` отдельной верхней группой с текущими вкладками.
+- [x] Добавить заглушки `Склад наличие -> Наличие` и `Склад наличие -> Заказы`.
+- [x] Добавить заглушки `Документация`, `Каталог общий`, `Калькуляции`.
+- [x] Проверить, что выбранный год не скрывает и не фильтрует новые модули.
+- [x] Переименовать группу `Администрирование` в `Администратор`.
+- [x] Проверить отображение меню для основных ролей.
+
+## Этап 2. Адаптация существующих разделов после переноса
+
+Статус: **реализован, ожидает пользовательской проверки**.
+
+Задача этапа — проверить существующие рабочие разделы после изменения меню и внести точечные правки без изменения бизнес-логики.
+
+- [x] Проверить, что разделы ДКР открываются из группы `ДКР`.
+- [x] Проверить, что каталог ДКР остался внутри группы `ДКР`.
+- [x] Проверить, что текущий `График монтажей` открывается из группы `Графики`.
+- [x] Проверить, что раздел `Запчасти` работает в новой структуре меню без изменения бизнес-логики.
+- [x] Проверить вкладки `Каталог`, `Заказы запчастей`, `Контроль наличия`, `Справочник расшифровок`, `Справка`.
+- [x] Проверить, что `Запчасти` используются как общий модуль, включая ДКР.
+- [x] Проверить, что меню `Администратор` содержит текущие пункты администрирования.
+- [x] Проверить, что `Подрядчики` всегда отображаются внутри группы `Администратор`.
+- [x] Проверить доступы для пользователей, ролей и настроек.
+- [x] Проверить необходимость новых прав: на этом этапе новые права не потребовались.
+
+## Этап 3. Рекламации: вкладки и тип
+
+Статус: **готов к реализации для текущих рекламаций**.
+
+Создание рекламации из графика заказов вынесено в отдельный этап, потому что зависит от реализации графика заказов.
+
+- [ ] Реализовать отдельные вкладки `Все` и `ДКР`.
+- [ ] Создать справочник `reclamation_types` и заполнить его типами `ДКР` / `Прочее` через сидер.
+- [ ] Добавить обязательную связь `reclamations.reclamation_type_id` и перенести существующие записи на тип `ДКР`.
+- [ ] Реализовать вкладку `ДКР` на текущем списке с обязательным фильтром по типу `dkr`.
+- [ ] Проверить создание рекламаций по площадкам ДКР.
+- [ ] Запретить формирование документации для оплаты для рекламаций типа `Прочее`.
+- [ ] Проверить формирование запросов на запчасти из рекламации.
+- [ ] Сохранить текущую рабочую логику рекламаций.
+
+## Этап 4. Каталог общий: ядро
+
+Статус: **готов к реализации ядра**.
+
+В этот этап не входят внешняя API-интеграция калькуляций и перенос экспорта техописаний. Для них нужны отдельные вводные.
 
 - [ ] Зафиксировать, что текущий `catalog.*` относится к каталогу ДКР.
 - [ ] Спроектировать маршруты с префиксом `common-catalog/...`, таблицы и права с техническим именем `common-catalog`.
 - [ ] Сверить текущую модель каталога ДКР с требованиями общего каталога как источник паттернов, без смешивания данных.
-- [ ] Определить недостающие поля по шаблону калькулятора.
+- [x] Зафиксировать состав и порядок колонок общего каталога.
+- [x] Зафиксировать файл `docs/refactor/Шаблон Каталог ощий.xlsx` как источник колонок общего каталога.
+- [x] Зафиксировать предварительные типы данных по шаблону общего каталога.
+- [ ] Спроектировать обязательность и валидацию утвержденных полей.
 - [ ] Использовать текущий файловый механизм для хранения фото и документов позиции.
 - [ ] Доработать импорт/экспорт позиций.
 - [ ] Доработать карточку позиции.
-- [ ] Определить встраивание `Технич. описание` в карточку общего каталога.
-- [ ] Добавить интеграцию с `Калькуляции` через внешний API по артикулу.
+- [x] Зафиксировать встраивание `Технич. описание` в карточку общего каталога.
+- [ ] Добавить в карточку место под будущую кнопку/блок `Калькуляции`.
 - [ ] Проверить связи с графиками, рекламациями и складом наличия.
 
-## Этап 3. Склад наличие
+## Этап 5. Документация
+
+Статус: **готов к реализации**.
+
+- [ ] Реализовать дерево папок.
+- [ ] Разрешить создание корневых папок только админу.
+- [ ] Реализовать права чтения и записи на папки для всех, ролей и отдельных пользователей.
+- [ ] Реализовать наследование прав от родительской папки при создании подпапок.
+- [ ] Реализовать создание, просмотр, редактирование и удаление документов.
+- [ ] Реализовать версионность документов.
+- [ ] Реализовать полное удаление отдельных версий.
+- [ ] Использовать текущий механизм хранения файлов.
+
+## Этап 6. Склад наличие: ядро
+
+Статус: **готов после этапа 4**.
+
+Ядро склада можно реализовывать после появления позиций общего каталога. PDF-экспорт по шаблону вынесен отдельным подпунктом, потому что точный шаблон еще не согласован.
 
 - [ ] Разобрать текущую реализацию `Запчасти` как эталон.
 - [ ] Зафиксировать соответствие сущностей `Запчасти` -> `Склад наличие`.
@@ -108,55 +184,62 @@
 - [ ] Реализовать отмену брони.
 - [ ] Реализовать историю движений.
 - [ ] Реализовать импорт заказов/остатков.
-- [ ] Реализовать экспорт наличия в PDF по шаблону.
 - [ ] Настроить права: `Наличие` всем, `Заказы` админу и роли `assistant_head`.
 - [ ] Покрыть критичную логику расчетов тестами.
+- [ ] После согласования шаблона реализовать экспорт наличия в PDF.
+
+## Этап 7. Техническое описание в общем каталоге
+
+Статус: **готово после этапа 4 и анализа готового Laravel-модуля**.
+
+- [ ] Получить и изучить готовый Laravel-модуль техописаний.
+- [ ] Составить карту данных модуля техописаний.
+- [ ] Составить карту экспортов и форматов выгрузки.
+- [ ] Сопоставить данные техописаний с полями `Каталог общий`.
+- [ ] Решить, какие данные хранятся прямо в карточке позиции, а какие требуют связанных таблиц.
+- [ ] Встроить просмотр/редактирование техописания в карточку общего каталога.
+- [ ] Перенести или адаптировать экспорт из готового Laravel-модуля.
+
+## Этап 8. Калькуляции
+
+Статус: **не готово к полной реализации**.
+
+До получения контракта API можно реализовать только место под кнопку/блок в карточке общего каталога.
+
+- [ ] Получить подробное описание внешнего API калькуляций.
+- [ ] Согласовать endpoint, HTTP-метод, авторизацию, параметры запроса и формат ответа.
+- [ ] Реализовать запрос по артикулу позиции общего каталога.
+- [ ] Реализовать отображение найденной калькуляции.
+- [ ] Реализовать состояние, когда калькуляция по артикулу не найдена.
+- [ ] Реализовать обработку ошибок API.
 
-## Этап 4. Графики
+## Этап 9. Графики заказов и доставок
+
+Статус: **не готово к полной реализации**.
+
+Текущий `График монтажей` переносится на этапе 2. Новые графики требуют уточнения обмена с 1С, полей, статусов и правил доставок.
 
 - [ ] Описать источник данных для `График заказов`.
 - [ ] Спроектировать загрузку данных через 1С.
 - [ ] Реализовать или перенести `График заказов`.
+- [ ] Добавить карточку заказа с выбором одного или нескольких МАФ.
+- [ ] Добавить создание рекламации из заказа через текущую форму.
 - [ ] Реализовать `График доставок` в виде недельного списка.
 - [ ] Добавить ручное планирование занятости водителей.
 - [ ] Связать доставки с адресами из графика заказов.
-- [ ] Перенести текущий `График монтажей` в новую группу без потери логики.
 - [ ] Проверить связи монтажей с ДКР-площадками и графиком заказов.
 
-## Этап 5. Рекламации
-
-- [ ] Разделить представление на `Все` и `ДКР`.
-- [ ] Реализовать `ДКР` как фильтр текущего списка.
-- [ ] Добавить отдельное поле источника рекламации: `ДКР` / `Прочее`.
-- [ ] Проверить создание рекламаций по площадкам ДКР.
-- [ ] Проверить связь рекламаций с графиком заказов.
-- [ ] Проверить формирование запросов на запчасти из рекламации.
-- [ ] Сохранить текущую рабочую логику рекламаций.
-
-## Этап 6. Запчасти
-
-- [ ] Перенести раздел в новую структуру меню без изменения бизнес-логики.
-- [ ] Проверить вкладки `Каталог`, `Заказы запчастей`, `Контроль наличия`, `Справочник расшифровок`, `Справка`.
-- [ ] Проверить, что `Запчасти` используются как общий модуль, включая ДКР.
-- [ ] Использовать реализацию раздела как эталон для `Склад наличие`.
-- [ ] После внедрения склада проверить, что логика запчастей не изменилась.
-
-## Этап 7. Информационные разделы
-
-- [ ] Реализовать модуль `Документация`: папки и файлы.
-- [ ] Изучить готовый Laravel-модуль техописаний.
-- [ ] Перенести/адаптировать экспорт техописаний в карточку общего каталога.
-- [ ] Подготовить заглушку для `Калькуляции`.
-- [ ] После получения подробного описания API калькуляций реализовать запрос из карточки позиции по артикулу.
+## Этап 10. Рекламации из графика заказов
 
-## Этап 8. Администратор
+Статус: **готово после этапа 9**.
 
-- [ ] Переименовать меню `Администрирование` в `Администратор`.
-- [ ] Сохранить текущие пункты администрирования.
-- [ ] Проверить доступы для пользователей, ролей и настроек.
-- [ ] При необходимости добавить права для новых разделов Manager 2.0.
+- [ ] Реализовать создание рекламации из графика заказов по выбранным МАФ с типом `other` (`Прочее`).
+- [ ] В карточке заказа дать выбор одного или нескольких МАФ.
+- [ ] Открывать текущую форму создания рекламации с предзаполненными данными.
+- [ ] Проверить, что рекламация попадает во вкладку `Все` и не попадает во вкладку `ДКР`.
+- [ ] Проверить, что платежные документы для такой рекламации недоступны.
 
-## Этап 9. Проверка
+## Этап 11. Проверка
 
 - [ ] Запустить `make test` внутри контейнеров.
 - [ ] Проверить меню в браузере под админом.
@@ -170,5 +253,9 @@
 
 ## Открытые вопросы
 
-- [ ] Оставлять ли `Технич. описание` отдельным пунктом меню или полностью встроить в `Каталог общий`?
 - [ ] Какой точный контракт внешнего API калькуляций?
+- [ ] Где находится готовый Laravel-модуль техописаний и какие форматы выгрузки из него переносим?
+- [ ] Какой точный PDF-шаблон нужен для экспорта `Склад наличие`?
+- [ ] Какой формат обмена с 1С для `График заказов`?
+- [ ] Какие поля, статусы и правила ручного редактирования нужны для `График заказов`?
+- [ ] Какие поля, статусы, правила отгрузок и связи с заказами нужны для `График доставок`?

+ 8 - 8
docs/refactor/tz-admin.md

@@ -129,14 +129,14 @@
 
 ## 8. Этапы реализации
 
-- [ ] Переименовать группу меню `Администрирование` в `Администратор`.
-- [ ] Сохранить все текущие подпункты.
-- [ ] Перенести `Подрядчики` внутрь группы `Администратор` без отдельного верхнего пункта.
-- [ ] Проверить отображение для администратора.
-- [ ] Проверить отображение для пользователя с частичными административными правами.
-- [ ] Проверить, что пользователь только с `contractors.view` видит группу `Администратор` и пункт `Подрядчики` внутри нее.
-- [ ] Проверить маршруты всех подпунктов.
-- [ ] Проверить, что текущие права работают без изменений.
+- [x] Переименовать группу меню `Администрирование` в `Администратор`.
+- [x] Сохранить все текущие подпункты.
+- [x] Перенести `Подрядчики` внутрь группы `Администратор` без отдельного верхнего пункта.
+- [x] Проверить отображение для администратора.
+- [x] Проверить отображение для пользователя с частичными административными правами.
+- [x] Проверить, что пользователь только с `contractors.view` видит группу `Администратор` и пункт `Подрядчики` внутри нее.
+- [x] Проверить маршруты всех подпунктов.
+- [x] Проверить, что текущие права работают без изменений.
 
 ## 9. Критерии приемки
 

+ 67 - 17
docs/refactor/tz-catalog-common.md

@@ -5,6 +5,7 @@
 - [План реорганизации](plan.md)
 - [Карта нового меню](menu.md)
 - [ТЗ: Модуль ДКР](tz-dkr.md)
+- [Шаблон каталога](Шаблон%20Каталог%20ощий.xlsx)
 
 ## 1. Назначение
 
@@ -107,11 +108,62 @@
 - экспорт;
 - импорт, если он будет предусмотрен для общего каталога.
 
+Источник полей: файл [Шаблон Каталог ощий.xlsx](Шаблон%20Каталог%20ощий.xlsx), лист 1, строка 1. В строке 3 шаблона есть пример заполнения.
+
 Колонки:
 
-- взять текущие колонки каталога ДКР как ориентир для анализа;
-- дополнительно сверить с шаблоном калькулятора;
-- недостающие поля вынести в отдельный список доработок после анализа шаблона.
+1. `ID`;
+2. `Внешний вид`;
+3. `Артикул`;
+4. `Наименование по калькулятору`;
+5. `Вид`;
+6. `Габаритные размеры`;
+7. `Высота падения`;
+8. `Дополнительные сведения`;
+9. `Единица измерения габаритов`;
+10. `Вес, кг.`;
+11. `Объем, м3`;
+12. `Места`;
+13. `Состав`;
+14. `Возрастная группа`;
+15. `Макс.кол-во пользователей`;
+16. `Ед.`;
+17. `Серия`;
+18. `ТМ`;
+19. `Примечание`.
+
+Требования к колонкам:
+
+- указанный перечень и порядок являются целевым форматом общего каталога;
+- при импорте принимать заголовок `Единица измерения габаритов` в формате шаблона с переносом строки между словами `измерения` и `габаритов`;
+- поле `Артикул` хранить строкой, чтобы сохранять ведущие нули;
+- `Внешний вид` отображает фото позиции;
+- состав полей общего каталога не зависит от состава полей каталога ДКР;
+- обязательность заполнения и правила валидации полей уточняются при проектировании модели данных.
+
+Предварительная модель полей по шаблону:
+
+| Колонка | Поле | Тип данных | Примечание |
+|---|---|---|---|
+| A | `ID` | integer | Внутренний идентификатор/номер строки каталога. |
+| B | `Внешний вид` | image/file | Фото позиции; в шаблоне есть встроенное изображение. |
+| C | `Артикул` | string | Сохранять ведущие нули, пример: `0254`. |
+| D | `Наименование по калькулятору` | text | Может содержать переносы строк. |
+| E | `Вид` | string | Категория/вид позиции. |
+| F | `Габаритные размеры` | text | Может содержать текст и единицы измерения. |
+| G | `Высота падения` | decimal | Числовое значение, допускается `0`. |
+| H | `Дополнительные сведения` | text nullable | Дополнительное описание. |
+| I | `Единица измерения габаритов` | string | В шаблоне заголовок разбит переносом строки. |
+| J | `Вес, кг.` | decimal | Вес в килограммах. |
+| K | `Объем, м3` | decimal | Объем в кубических метрах. |
+| L | `Места` | integer | Количество мест. |
+| M | `Состав` | text nullable | Состав комплекта/позиции. |
+| N | `Возрастная группа` | string | Пример: `0+`. |
+| O | `Макс.кол-во пользователей` | integer | Максимальное количество пользователей. |
+| P | `Ед.` | string | Единица учета, пример: `шт.`. |
+| Q | `Серия` | string | Серия позиции. |
+| R | `ТМ` | string | Торговая марка. |
+| S | `Примечание` | text nullable | Свободное примечание. |
 
 ## 7. Карточка позиции
 
@@ -122,7 +174,7 @@
 - просмотр и редактирование полей позиции общего каталога;
 - загрузка фото МАФ/позиции;
 - хранение сопутствующих документов;
-- блок/выгрузка технического описания, если модуль техописаний будет поглощен каталогом;
+- блок/выгрузка технического описания, встроенные в карточку общего каталога;
 - переход в `Калькуляции`;
 - отображение связанных МАФ/SKU, если это потребуется для новой логики;
 - сохранение привычных действий карточки каталога ДКР только как UI-паттерна, без смешивания данных.
@@ -155,14 +207,11 @@
 
 - спроектировать импорт и экспорт общего каталога;
 - использовать текущий импорт/экспорт каталога ДКР как технический референс, если это ускорит реализацию;
-- привести состав колонок к шаблону калькулятора после анализа шаблона;
+- использовать утвержденные колонки общего каталога в заданном порядке;
+- использовать [Шаблон Каталог ощий.xlsx](Шаблон%20Каталог%20ощий.xlsx) как целевой шаблон импорта/экспорта общего каталога;
 - при импорте обновлять существующие позиции по стабильному ключу;
 - ошибки импорта должны быть понятны пользователю.
 
-Открытая доработка:
-
-- отдельно сверить шаблон калькулятора и текущие поля каталога ДКР как референс.
-
 ## 10. Связи с другими модулями
 
 | Модуль | Связь | Требование |
@@ -171,7 +220,7 @@
 | Склад наличие | Использует картинку, артикул, наименование, характеристики, ед. изм. | Склад должен брать справочные поля из общего каталога. |
 | Графики | Используют данные по позициям | При разработке графиков опираться на общий каталог. |
 | Рекламации | Используют данные по позициям | Сохранить/добавить связь рекламаций с позициями общего каталога. |
-| Технич. описание | Вероятно является частью общего каталога | Использовать готовый Laravel-модуль как источник экспорта/формата. |
+| Технич. описание | Является частью общего каталога | Использовать готовый Laravel-модуль как источник экспорта/формата. |
 | Калькуляции | Запрашиваются из карточки позиции через внешний API | Запрос по артикулу, отображение возвращенной калькуляции при наличии. |
 
 ## 11. Что не входит в первый этап
@@ -182,7 +231,7 @@
 - полная переработка модели `Product`;
 - внедрение новых прав доступа;
 - разработка модуля `Склад наличие`;
-- отдельный модуль `Технич. описание`, если он будет поглощен каталогом;
+- отдельный модуль `Технич. описание`;
 - разработка интеграции с калькуляциями до получения подробного описания API;
 - изменение бизнес-логики ДКР.
 
@@ -197,11 +246,13 @@
 - [ ] Спроектировать карточку позиции общего каталога.
 - [ ] Проверить загрузку фото позиции.
 - [ ] Спроектировать загрузку документов позиции через текущий файловый механизм.
-- [ ] Составить список текущих полей каталога ДКР как источник для анализа.
-- [ ] Сверить поля с шаблоном калькулятора.
-- [ ] Описать недостающие поля для доработки.
+- [x] Зафиксировать состав и порядок колонок общего каталога.
+- [x] Зафиксировать шаблон общего каталога как источник колонок.
+- [x] Зафиксировать предварительные типы данных по шаблону общего каталога.
+- [ ] Спроектировать обязательность и валидацию утвержденных полей.
+- [ ] Реализовать утвержденные поля в списке и карточке позиции.
 - [ ] Подготовить расширение блока документов до набора файлов, если нужно.
-- [ ] Определить, включается ли `Технич. описание` внутрь карточки общего каталога.
+- [x] Зафиксировать, что `Технич. описание` включается внутрь карточки общего каталога.
 - [ ] Добавить в карточку место под API-запрос `Калькуляции`.
 - [ ] Проверить связи с модулями, которым нужен общий каталог.
 - [ ] Проверить, что заглушка `Каталог общий` видна всем авторизованным пользователям.
@@ -213,12 +264,11 @@
 - Текущие маршруты `catalog.index` и `catalog.show` сохранены как маршруты каталога ДКР.
 - Заглушка `Каталог общий` доступна всем авторизованным пользователям.
 - После реализации общий каталог не смешивает данные с каталогом ДКР без отдельной миграции/синхронизации.
+- Список, импорт и экспорт общего каталога используют утвержденный состав и порядок колонок из файла `docs/refactor/Шаблон Каталог ощий.xlsx`.
 - Карточка позиции общего каталога поддерживает фото, документы, техописание/выгрузку и калькуляцию по артикулу.
 - ДКР продолжает использовать свой каталог без поломки существующих связей.
 - Реорганизация меню выполнена как часть общей структуры Manager 2.0.
 
 ## 14. Открытые вопросы
 
-- Какие точные колонки должны быть добавлены по шаблону калькулятора?
-- Как именно включить готовый Laravel-модуль техописаний в общий каталог и какие экспорты оттуда переносить?
 - Какой точный формат API калькуляций: endpoint, параметры, авторизация, структура ответа?

+ 9 - 9
docs/refactor/tz-dkr.md

@@ -208,15 +208,15 @@
 
 ## 14. Этапы реализации
 
-- [ ] Проверить текущие маршруты и permissions разделов ДКР.
-- [ ] Добавить верхний пункт меню `ДКР`.
-- [ ] Перенести пункты `Площадки`, `МАФ`, `Каталог`, `Отчеты`, `Ответственные`, `Заказы МАФ` внутрь `ДКР`.
-- [ ] Проверить, что `catalog.index` доступен как каталог ДКР.
-- [ ] Проверить активное состояние меню для каждого подраздела ДКР.
-- [ ] Проверить отображение меню для ролей с разным набором прав.
-- [ ] Проверить, что существующие страницы ДКР открываются по прежним маршрутам.
-- [ ] Проверить связи площадок с рекламациями.
-- [ ] Проверить связи площадок с графиком монтажей.
+- [x] Проверить текущие маршруты и permissions разделов ДКР.
+- [x] Добавить верхний пункт меню `ДКР`.
+- [x] Перенести пункты `Площадки`, `МАФ`, `Каталог`, `Отчеты`, `Ответственные`, `Заказы МАФ` внутрь `ДКР`.
+- [x] Проверить, что `catalog.index` доступен как каталог ДКР.
+- [x] Проверить активное состояние меню для каждого подраздела ДКР.
+- [x] Проверить отображение меню для ролей с разным набором прав.
+- [x] Проверить, что существующие страницы ДКР открываются по прежним маршрутам.
+- [x] Проверить связи площадок с рекламациями.
+- [x] Проверить связи площадок с графиком монтажей.
 
 ## 15. Критерии приемки
 

+ 126 - 32
docs/refactor/tz-reclamations.md

@@ -44,7 +44,7 @@
 - связь с запчастями и резервами;
 - создание записи в графике монтажей из рекламации.
 
-На первом этапе бизнес-логика не меняется, выполняется только реорганизация меню и подготовка разделения `Все` / `ДКР`.
+На первом этапе сохраняется текущая бизнес-логика рекламаций и добавляется явное разделение интерфейса на вкладки `Все` / `ДКР`.
 
 ## 3. Место в меню
 
@@ -59,10 +59,10 @@
 Требования:
 
 - добавить верхний пункт меню `Рекламации`;
-- внутри разместить пункты `Все` и `ДКР`;
+- внутри раздела реализовать отдельные вкладки `Все` и `ДКР`;
 - текущий маршрут `reclamations.index` оставить без переименования;
 - текущий маршрут `reclamations.show` оставить без переименования;
-- для пункта `ДКР` использовать фильтр текущего списка;
+- вкладку `ДКР` реализовать на текущем списке с обязательным фильтром по типу рекламации `ДКР`;
 - реорганизация меню выполняется в общем этапе Manager 2.0.
 
 ## 4. Права доступа
@@ -81,7 +81,58 @@
 - пользователи с текущим доступом к рекламациям должны сохранить доступ;
 - верхний пункт `Рекламации` показывать, если доступен хотя бы один дочерний пункт.
 
-## 5. Подраздел `Все`
+## 5. Справочник типов рекламаций
+
+Тип рекламации должен храниться не строковым полем, а ссылкой на отдельный системный справочник.
+
+### 5.1. Таблица типов
+
+Создать таблицу `reclamation_types` со следующими полями:
+
+| Поле | Тип | Требование |
+|---|---|---|
+| `id` | `bigint` | Первичный ключ. |
+| `code` | `string` | Уникальный стабильный код типа. |
+| `name` | `string` | Отображаемое название типа. |
+| `created_at`, `updated_at` | `timestamps` | Служебные даты. |
+
+Начальные записи справочника:
+
+| `code` | `name` |
+|---|---|
+| `dkr` | `ДКР` |
+| `other` | `Прочее` |
+
+Требования:
+
+- создать модель `ReclamationType`;
+- определить в модели константы кодов `CODE_DKR = 'dkr'` и `CODE_OTHER = 'other'`;
+- создать сидер `ReclamationTypeSeeder`;
+- сидер должен идемпотентно создавать или обновлять типы по полю `code`;
+- зарегистрировать `ReclamationTypeSeeder` в общем процессе заполнения БД;
+- системные коды `dkr` и `other` использовать в бизнес-логике вместо сравнения отображаемых названий;
+- на текущем этапе отдельный интерфейс управления справочником не требуется;
+- удаление используемого типа рекламации должно быть запрещено внешним ключом (`restrictOnDelete`).
+
+### 5.2. Связь с рекламацией
+
+В таблицу `reclamations` добавить обязательное поле `reclamation_type_id`:
+
+- внешний ключ на `reclamation_types.id`;
+- индекс для фильтрации списков;
+- связь `Reclamation::reclamationType()`;
+- обратная связь `ReclamationType::reclamations()`;
+- существующим рекламациям при переносе данных назначить тип с кодом `dkr`;
+- после заполнения существующих записей поле не должно допускать `null`.
+
+Тип назначается сервером по сценарию создания:
+
+- рекламация, созданная из площадки ДКР, получает тип `dkr`;
+- рекламация, созданная не из площадки, включая график заказов, получает тип `other`;
+- клиентская форма не должна позволять подменить автоматически определенный тип;
+- если в будущем потребуется ручной выбор типа, это оформляется отдельным изменением требований и прав доступа.
+
+## 6. Подраздел `Все`
 
 Назначение:
 
@@ -94,13 +145,15 @@
 - экспорт;
 - переход в карточку;
 - отображение статусов;
-- отображение рекламаций по ДКР и прочих рекламаций.
+- отображение рекламаций по ДКР и прочих рекламаций;
+- вкладка содержит все рекламации независимо от типа;
+- наличие рекламации во вкладке `Все` не дает права формировать платежные документы, если тип не `ДКР`.
 
 Текущая основа:
 
 - `reclamations.index`.
 
-## 6. Подраздел `ДКР`
+## 7. Подраздел `ДКР`
 
 Назначение:
 
@@ -110,21 +163,25 @@
 Функционал:
 
 - отображение только рекламаций типа `ДКР`;
-- реализация через текущий список рекламаций с предустановленным фильтром по источнику;
+- отдельная вкладка интерфейса на основе текущего списка с обязательным фильтром по типу рекламации;
 - создание рекламации с привязкой к площадке ДКР;
 - сохранение текущей карточки рекламации;
 - связь с графиком монтажей;
-- формирование запросов на запчасти.
+- формирование запросов на запчасти;
+- формирование документации для оплаты только по рекламациям типа `ДКР`.
 
 Принятое решение:
 
-- отдельный URL для списка `ДКР` не нужен;
-- используется текущий список с фильтром по источнику рекламации;
-- для определения источника рекламации добавляется отдельное поле;
-- при создании рекламации из площадки источник устанавливается `ДКР`;
-- при создании рекламации не из площадки источник устанавливается `Прочее`.
+- отдельный URL для списка `ДКР` не обязателен;
+- в интерфейсе обязательно отдельное разделение на вкладки `Все` и `ДКР`;
+- вкладка `ДКР` использует текущий список с обязательным фильтром по `reclamation_type_id` типа с кодом `dkr`;
+- тип рекламации определяется через справочник `reclamation_types`;
+- при создании рекламации из площадки назначается тип `dkr` (`ДКР`);
+- при создании рекламации не из площадки, включая создание из графика заказов, назначается тип `other` (`Прочее`);
+- рекламации типа `Прочее` не выводятся во вкладке `ДКР`;
+- формирование документации для оплаты разрешено только для рекламаций типа `ДКР`.
 
-## 7. Карточка рекламации
+## 8. Карточка рекламации
 
 Карточка должна сохранить текущий функционал:
 
@@ -144,55 +201,92 @@
 Изменения на первом этапе:
 
 - не менять структуру карточки;
-- добавить/использовать отображение источника рекламации `ДКР` / `Прочее`;
+- отображать тип рекламации из справочника `ДКР` / `Прочее`;
+- для типа `Прочее` скрыть и запретить серверной проверкой формирование документации для оплаты;
+- для типа `ДКР` сохранить текущую возможность формирования документации для оплаты;
 - сохранить текущие маршруты и формы.
 
-## 8. Связи
+## 9. Создание из графика заказов
+
+Сценарий:
+
+1. Пользователь открывает заказ в `График заказов`.
+2. Пользователь выбирает один или несколько МАФ заказа.
+3. Пользователь нажимает `Добавить рекламацию`.
+4. Открывается текущая форма создания рекламации.
+5. В форму передаются заказ и выбранные МАФ.
+6. Пользователь заполняет форму по текущему процессу и сохраняет рекламацию.
+
+Требования:
+
+- использовать существующую форму создания рекламации;
+- поддержать выбор одного или нескольких МАФ;
+- привязать созданную рекламацию к заказу и выбранным МАФ;
+- назначить созданной рекламации тип `other` (`Прочее`);
+- созданная рекламация отображается во вкладке `Все`;
+- созданная рекламация не отображается во вкладке `ДКР`;
+- для нее недоступно формирование документации для оплаты;
+- остальные функции карточки рекламации работают по текущим правилам.
+
+## 10. Связи
 
 | Модуль | Связь | Требование |
 |---|---|---|
 | ДКР / Площадки | Используется при создании рекламации типа `ДКР` | Сохранить текущую связь. |
 | Каталог общий | Данные по позициям | Использовать общий каталог как источник номенклатуры. |
-| График заказов | Используется при создании рекламации | Реализовать после появления графика заказов. |
+| График заказов | Из заказа создается рекламация по одному или нескольким МАФ | Открывать текущую форму создания с предзаполненным заказом и выбранными МАФ; назначать тип `other` (`Прочее`). |
 | График монтажей | Рекламация может попасть в монтажный график | Сохранить текущую связь. |
 | Запчасти | Из рекламации формируется запрос на запчасть | Сохранить текущую связь и резервы. |
 
-## 9. Что не входит в первый этап
+## 11. Что не входит в первый этап
 
 - изменение текущих маршрутов `reclamations.*`;
-- переработка модели `Reclamation`;
+- переработка модели `Reclamation` сверх добавления типа рекламации и связей с заказом/выбранными МАФ;
 - изменение логики резервов запчастей;
-- изменение генерации документов;
+- изменение состава и шаблонов документации для оплаты;
 - разработка графика заказов;
 - внедрение новых прав доступа.
 
-## 10. Этапы реализации
+## 12. Этапы реализации
 
 - [ ] Проверить текущие маршруты `reclamations.*`.
 - [ ] Проверить текущие permissions рекламаций.
 - [ ] Добавить верхнюю группу меню `Рекламации`.
-- [ ] Добавить пункт `Все`, ведущий на текущий список.
-- [ ] Добавить пункт `ДКР` как отдельный URL или фильтр после решения.
+- [ ] Добавить отдельные вкладки `Все` и `ДКР`.
+- [ ] Создать таблицу `reclamation_types` и модель `ReclamationType`.
+- [ ] Создать и зарегистрировать `ReclamationTypeSeeder` с типами `dkr` (`ДКР`) и `other` (`Прочее`).
+- [ ] Добавить `reclamations.reclamation_type_id`, внешний ключ, индекс и связи моделей.
+- [ ] Назначить существующим рекламациям тип `dkr` и сделать поле типа обязательным.
+- [ ] Реализовать вкладку `ДКР` на текущем списке с обязательным фильтром по типу `dkr`.
 - [ ] Проверить открытие карточки рекламации.
 - [ ] Проверить создание рекламации по площадке ДКР.
+- [ ] Реализовать создание рекламации из графика заказов с выбором одного или нескольких МАФ.
+- [ ] Передавать заказ и выбранные МАФ в текущую форму создания рекламации.
+- [ ] Автоматически назначать тип рекламации в зависимости от сценария создания.
+- [ ] Запретить формирование документации для оплаты для типа `Прочее`.
 - [ ] Проверить связь с графиком монтажей.
 - [ ] Проверить блок запчастей и резервов.
 - [ ] Проверить экспорт рекламаций.
 
-## 11. Критерии приемки
+## 13. Критерии приемки
 
 - В меню есть группа `Рекламации`.
-- Внутри есть пункты `Все` и `ДКР`.
+- Внутри есть отдельные вкладки `Все` и `ДКР`.
 - `Все` открывает текущий общий список рекламаций.
 - Текущие маршруты рекламаций сохранены.
 - Пользователи с прежним доступом сохраняют доступ.
 - Рекламации типа `ДКР` связаны с площадками ДКР.
-- При создании рекламации из площадки источник автоматически устанавливается `ДКР`.
-- При создании рекламации не из площадки источник устанавливается `Прочее`.
-- Пункт `Рекламации -> ДКР` открывает текущий список с фильтром по источнику `ДКР`.
+- Типы рекламаций хранятся в отдельной таблице `reclamation_types`.
+- Сидер создает типы `dkr` (`ДКР`) и `other` (`Прочее`) без дублирования при повторном запуске.
+- Каждая рекламация имеет обязательную ссылку `reclamation_type_id` на справочник типов.
+- Существующие рекламации после миграции имеют тип `ДКР`.
+- При создании рекламации из площадки автоматически назначается тип `dkr` (`ДКР`).
+- При создании рекламации не из площадки назначается тип `other` (`Прочее`).
+- Вкладка `ДКР` открывает текущий список с обязательным фильтром по типу `dkr`.
+- Рекламации из графика заказов не попадают во вкладку `ДКР`.
+- Из графика заказов можно выбрать один или несколько МАФ и открыть текущую форму создания рекламации.
+- Созданная из графика заказов рекламация привязана к заказу и выбранным МАФ.
+- Документация для оплаты формируется только для рекламаций типа `ДКР`.
+- Для рекламаций типа `Прочее` действие формирования платежных документов отсутствует и блокируется на сервере.
 - Из рекламации по-прежнему можно работать с запчастями.
 - Перенос рекламации в график монтажей работает как раньше.
-
-## 12. Открытые вопросы
-
-- Как будет использоваться связь с будущим `График заказов` при создании рекламации?

+ 37 - 14
docs/refactor/tz-schedules.md

@@ -104,7 +104,21 @@
 - фильтры по дате, заказу, позиции, статусу;
 - импорт/обновление данных из 1С;
 - история последней загрузки;
-- обработка ошибок загрузки.
+- обработка ошибок загрузки;
+- переход в карточку заказа;
+- выбор одного или нескольких МАФ заказа;
+- действие `Добавить рекламацию` для выбранных МАФ;
+- открытие существующей формы создания рекламации с передачей заказа и выбранных МАФ.
+
+Требования к созданию рекламации:
+
+- рекламация создается из карточки заказа, а не напрямую из строки списка;
+- пользователь должен выбрать минимум один МАФ;
+- допускается выбор нескольких МАФ;
+- после нажатия `Добавить рекламацию` открывается текущая форма создания рекламации;
+- рекламации назначается тип `other` (`Прочее`) из справочника типов;
+- рекламация отображается в общей вкладке рекламаций и не отображается во вкладке `ДКР`;
+- для такой рекламации не формируется документация для оплаты.
 
 Открытые вопросы:
 
@@ -196,6 +210,7 @@
 | Каталог общий | График заказов | Позиции заказа и справочные данные по номенклатуре. |
 | График заказов | График доставок | Адреса и заказы для планирования доставки. |
 | График заказов | График монтажей | Связь производственного графика с монтажом. |
+| График заказов | Рекламации | Создание рекламации из заказа по выбранным МАФ через текущую форму. |
 | ДКР / Площадки | График монтажей | Площадки для монтажных работ. |
 | Рекламации | График монтажей | Работы по рекламациям. |
 
@@ -203,7 +218,8 @@
 
 - связи не должны ломать автономность ДКР;
 - текущий график монтажей должен работать до реализации графика заказов;
-- новые графики должны опираться на `Каталог общий`, а не на каталог внутри ДКР.
+- новые графики должны опираться на `Каталог общий`, а не на каталог внутри ДКР;
+- рекламация из графика заказов должна получать тип `other` (`Прочее`) из справочника типов.
 
 ## 9. Данные и модели
 
@@ -248,20 +264,24 @@
 
 ## 11. Этапы реализации
 
-- [ ] Проверить текущие маршруты `schedule.*`.
-- [ ] Проверить текущие permissions графика монтажей.
-- [ ] Добавить верхнюю группу меню `Графики`.
-- [ ] Перенести текущий пункт `График монтажей` внутрь `Графики`.
-- [ ] Оставить маршрут `schedule.index` без переименования.
-- [ ] Добавить страницу-заглушку `График заказов`.
-- [ ] Добавить страницу-заглушку `График доставок`.
-- [ ] Проверить активное состояние меню для `Графики`.
-- [ ] Проверить, что текущий график монтажей открывается и работает.
-- [ ] Проверить создание записи графика из площадки/заказа.
-- [ ] Проверить создание записи графика из рекламации.
-- [ ] Проверить экспорт графика монтажей.
+- [x] Проверить текущие маршруты `schedule.*`.
+- [x] Проверить текущие permissions графика монтажей.
+- [x] Добавить верхнюю группу меню `Графики`.
+- [x] Перенести текущий пункт `График монтажей` внутрь `Графики`.
+- [x] Оставить маршрут `schedule.index` без переименования.
+- [x] Добавить страницу-заглушку `График заказов`.
+- [x] Добавить страницу-заглушку `График доставок`.
+- [x] Проверить активное состояние меню для `Графики`.
+- [x] Проверить, что текущий график монтажей открывается и работает.
+- [x] Проверить создание записи графика из площадки/заказа.
+- [x] Проверить создание записи графика из рекламации.
+- [x] Проверить экспорт графика монтажей.
 - [ ] Составить отдельный список полей для будущего графика заказов.
 - [ ] Составить отдельный список полей для будущего графика доставок.
+- [ ] Реализовать карточку заказа с выбором одного или нескольких МАФ.
+- [ ] Добавить действие `Добавить рекламацию`.
+- [ ] Открывать текущую форму рекламации с переданными заказом и выбранными МАФ.
+- [ ] Назначать созданной рекламации тип `other` (`Прочее`).
 
 ## 12. Критерии приемки
 
@@ -273,6 +293,9 @@
 - Пользователи с текущим доступом к графику монтажей сохраняют доступ.
 - График монтажей продолжает работать с площадками ДКР.
 - График монтажей продолжает работать с рекламациями.
+- Из карточки заказа можно выбрать один или несколько МАФ и открыть форму создания рекламации.
+- Созданная рекламация связана с заказом и выбранными МАФ.
+- Созданная рекламация имеет тип `other` (`Прочее`), не попадает во вкладку `ДКР` и не позволяет формировать документацию для оплаты.
 
 ## 13. Открытые вопросы
 

+ 13 - 13
docs/refactor/tz-spare-parts.md

@@ -171,19 +171,19 @@
 
 ## 12. Этапы реализации
 
-- [ ] Проверить текущие маршруты `spare_parts.*`.
-- [ ] Проверить текущие маршруты `spare_part_orders.*`.
-- [ ] Проверить текущие маршруты `spare_part_inventory.index`.
-- [ ] Проверить текущие маршруты `pricing_codes.*`.
-- [ ] Перенести меню `Запчасти` в новую структуру без изменения маршрутов.
-- [ ] Переименовать пункт `Заказы деталей` в `Заказы запчастей`.
-- [ ] Открыть пункт `Справочник расшифровок` всем пользователям с доступом к модулю `Запчасти`.
-- [ ] Проверить каталог запчастей.
-- [ ] Проверить заказы запчастей.
-- [ ] Проверить контроль наличия.
-- [ ] Проверить справочник расшифровок.
-- [ ] Проверить связь с рекламациями.
-- [ ] Зафиксировать логику модуля как основу для `Склад наличие`.
+- [x] Проверить текущие маршруты `spare_parts.*`.
+- [x] Проверить текущие маршруты `spare_part_orders.*`.
+- [x] Проверить текущие маршруты `spare_part_inventory.index`.
+- [x] Проверить текущие маршруты `pricing_codes.*`.
+- [x] Перенести меню `Запчасти` в новую структуру без изменения маршрутов.
+- [x] Переименовать пункт `Заказы деталей` в `Заказы запчастей`.
+- [x] Открыть пункт `Справочник расшифровок` всем пользователям с доступом к модулю `Запчасти`.
+- [x] Проверить каталог запчастей.
+- [x] Проверить заказы запчастей.
+- [x] Проверить контроль наличия.
+- [x] Проверить справочник расшифровок.
+- [x] Проверить связь с рекламациями.
+- [x] Зафиксировать логику модуля как основу для `Склад наличие`.
 
 ## 13. Критерии приемки
 

+ 6 - 11
docs/refactor/tz-technical-description.md

@@ -11,11 +11,11 @@
 
 `Технич. описание` описывает технические данные позиции общего каталога и выгрузки этих данных в заданном формате.
 
-По новым вводным это, скорее всего, не отдельный самостоятельный модуль, а часть модуля `Каталог общий`: по сути это те же данные позиции, только представленные и выгружаемые в определенном формате.
+По новым вводным это не отдельный самостоятельный модуль, а часть модуля `Каталог общий`: по сути это те же данные позиции, только представленные и выгружаемые в определенном формате.
 
 ## 2. Статус
 
-Статус: **кандидат на поглощение модулем `Каталог общий`**.
+Статус: **встроить в модуль `Каталог общий`**.
 
 Есть готовый модуль на Laravel. Его нужно изучить и использовать как источник:
 
@@ -26,9 +26,7 @@
 
 ## 3. Место в меню
 
-До окончательного решения пункт `Технич. описание` можно оставить в целевой карте меню как заглушку для совместимости с исходным ТЗ.
-
-Предпочтительная целевая модель:
+Отдельный пункт меню `Технич. описание` не нужен. Целевая модель:
 
 ```text
 Каталог общий
@@ -46,15 +44,13 @@
 
 ## 4. Права доступа
 
-На этапе заглушки:
-
-- пункт/заглушку показывать всем авторизованным пользователям.
+Отдельная заглушка для техописания не требуется, потому что функциональность встраивается в карточку общего каталога.
 
 Целевая модель:
 
 - просмотр техописания наследует доступ к позиции общего каталога;
 - экспорт техописания наследует доступ к позиции общего каталога;
-- редактирование данных техописания наследует права редактирования позиции общего каталога, если техописание будет встроено в карточку позиции.
+- редактирование данных техописания наследует права редактирования позиции общего каталога.
 
 ## 5. Функциональные требования
 
@@ -82,7 +78,7 @@
 - [ ] Решить, какие данные хранятся прямо в карточке позиции, а какие требуют связанных таблиц.
 - [ ] Встроить просмотр/редактирование техописания в карточку общего каталога.
 - [ ] Перенести или адаптировать экспорт из готового Laravel-модуля.
-- [ ] Если отдельный пункт меню больше не нужен, убрать его из финальной навигации.
+- [x] Не добавлять отдельный пункт меню `Технич. описание` в финальную навигацию.
 
 ## 8. Критерии приемки
 
@@ -96,4 +92,3 @@
 
 - Где находится готовый Laravel-модуль техописаний и какие его части можно переносить напрямую?
 - Какие точные форматы выгрузки нужны?
-- Оставлять ли отдельный пункт меню `Технич. описание` или полностью перенести функциональность внутрь `Каталог общий`?

BIN
docs/refactor/Шаблон Каталог ощий.xlsx


+ 14 - 0
resources/views/development/index.blade.php

@@ -0,0 +1,14 @@
+@extends('layouts.app')
+
+@section('content')
+    <div class="row justify-content-center">
+        <div class="col-12 col-lg-8 col-xl-6">
+            <div class="card">
+                <div class="card-body text-center py-5">
+                    <h1 class="h4 mb-3">{{ $sectionTitle }}</h1>
+                    <p class="text-muted mb-0">В разработке</p>
+                </div>
+            </div>
+        </div>
+    </div>
+@endsection

+ 129 - 58
resources/views/layouts/menu.blade.php

@@ -1,57 +1,130 @@
 @if(auth()->check())
-    @if(hasPermission('orders.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'orders') active @endif" href="{{ route('order.index', session('gp_orders')) }}">Площадки</a></li>
-    @endif
+    @php
+        $dkrActive = in_array($active ?? '', [
+            'orders',
+            'product_sku',
+            'catalog',
+            'reports',
+            'responsibles',
+            'maf_order',
+        ], true);
 
-    @if(hasPermission('maf.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'product_sku') active @endif"
-                                href="{{ route('product_sku.index', session('gp_product_sku')) }}">МАФ</a></li>
-    @endif
-    @if(hasPermission('catalog.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'catalog') active @endif"
-                                href="{{ route('catalog.index', session('gp_products')) }}">Каталог</a></li>
-    @endif
-    @if(hasPermission('reports.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'reports') active @endif"
-                                href="{{ route('reports.index', session('gp_reports')) }}">Отчёты</a></li>
-    @endif
-    @if(hasPermission('responsibles.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'responsibles') active @endif" href="{{ route('responsible.index', session('gp_responsibles')) }}">Ответственные</a></li>
+        $schedulesActive = in_array($active ?? '', [
+            'schedule_orders',
+            'schedule_deliveries',
+            'schedule',
+        ], true);
+
+        $reclamationsActive = ($active ?? '') === 'reclamations';
+
+        $sparePartsActive = in_array($active ?? '', [
+            'spare_parts',
+            'spare_part_orders',
+            'spare_part_inventory',
+            'pricing_codes',
+            'spare_parts_help',
+        ], true);
+
+        $stockActive = in_array($active ?? '', ['stock', 'stock_orders'], true);
+
+        $adminActive = in_array($active ?? '', [
+            'contractors',
+            'contracts',
+            'users',
+            'roles',
+            'admin_settings',
+            'admin_districts',
+            'admin_areas',
+            'notification_logs',
+            'import',
+            'year-data',
+            'clear-data',
+        ], true);
+    @endphp
+
+    @if(hasAnyPermission([
+        'orders.view',
+        'maf.view',
+        'catalog.view',
+        'reports.view',
+        'responsibles.view',
+        'maf_orders.view',
+    ]))
+        <li class="nav-item dropdown">
+            <a class="nav-link dropdown-toggle @if($dkrActive) active @endif" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                ДКР
+            </a>
+            <ul class="dropdown-menu">
+                @if(hasPermission('orders.view'))<li><a class="dropdown-item @if(($active ?? '') === 'orders') active @endif" href="{{ route('order.index', session('gp_orders')) }}">Площадки</a></li>@endif
+                @if(hasPermission('maf.view'))<li><a class="dropdown-item @if(($active ?? '') === 'product_sku') active @endif" href="{{ route('product_sku.index', session('gp_product_sku')) }}">МАФ</a></li>@endif
+                @if(hasPermission('catalog.view'))<li><a class="dropdown-item @if(($active ?? '') === 'catalog') active @endif" href="{{ route('catalog.index', session('gp_products')) }}">Каталог</a></li>@endif
+                @if(hasPermission('reports.view'))<li><a class="dropdown-item @if(($active ?? '') === 'reports') active @endif" href="{{ route('reports.index', session('gp_reports')) }}">Отчёты</a></li>@endif
+                @if(hasPermission('responsibles.view'))<li><a class="dropdown-item @if(($active ?? '') === 'responsibles') active @endif" href="{{ route('responsible.index', session('gp_responsibles')) }}">Ответственные</a></li>@endif
+                @if(hasPermission('maf_orders.view'))<li><a class="dropdown-item @if(($active ?? '') === 'maf_order') active @endif" href="{{ route('maf_order.index', session('gp_maf_order')) }}">Заказы МАФ</a></li>@endif
+            </ul>
+        </li>
     @endif
+
+    <li class="nav-item dropdown">
+        <a class="nav-link dropdown-toggle @if($schedulesActive) active @endif" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+            Графики
+        </a>
+        <ul class="dropdown-menu">
+            <li><a class="dropdown-item @if(($active ?? '') === 'schedule_orders') active @endif" href="{{ route('schedule.orders') }}">График заказов</a></li>
+            <li><a class="dropdown-item @if(($active ?? '') === 'schedule_deliveries') active @endif" href="{{ route('schedule.deliveries') }}">График доставок</a></li>
+            @if(hasPermission('schedule.view'))<li><a class="dropdown-item @if(($active ?? '') === 'schedule') active @endif" href="{{ route('schedule.index', session('gp_schedule')) }}">График монтажей</a></li>@endif
+        </ul>
+    </li>
+
     @if(hasPermission('reclamations.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'reclamations') active @endif"
-                                href="{{ route('reclamations.index', session('gp_reclamations')) }}">Рекламации</a></li>
+        <li class="nav-item dropdown">
+            <a class="nav-link dropdown-toggle @if($reclamationsActive) active @endif" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                Рекламации
+            </a>
+            <ul class="dropdown-menu">
+                <li><a class="dropdown-item @if(($active ?? '') === 'reclamations' && request('tab') !== 'dkr') active @endif" href="{{ route('reclamations.index', session('gp_reclamations')) }}">Все</a></li>
+                <li><a class="dropdown-item @if(($active ?? '') === 'reclamations' && request('tab') === 'dkr') active @endif" href="{{ route('reclamations.index', array_merge((array) session('gp_reclamations', []), ['tab' => 'dkr'])) }}">ДКР</a></li>
+            </ul>
+        </li>
     @endif
 
     @if(hasPermission('spare_parts.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'spare_parts') active @endif"
-                                href="{{ route('spare_parts.index', session('gp_spare_parts', [])) }}">Запчасти</a></li>
+        <li class="nav-item dropdown">
+            <a class="nav-link dropdown-toggle @if($sparePartsActive) active @endif" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                Запчасти
+            </a>
+            <ul class="dropdown-menu">
+                <li><a class="dropdown-item @if(($active ?? '') === 'spare_parts') active @endif" href="{{ route('spare_parts.index', session('gp_spare_parts', [])) }}">Каталог</a></li>
+                @if(hasPermission('spare_part_orders.view'))<li><a class="dropdown-item @if(($active ?? '') === 'spare_part_orders') active @endif" href="{{ route('spare_part_orders.index') }}">Заказы запчастей</a></li>@endif
+                @if(hasPermission('spare_part_inventory.view'))<li><a class="dropdown-item @if(($active ?? '') === 'spare_part_inventory') active @endif" href="{{ route('spare_part_inventory.index') }}">Контроль наличия</a></li>@endif
+                <li><a class="dropdown-item @if(($active ?? '') === 'pricing_codes') active @endif" href="{{ route('pricing_codes.index') }}">Справочник расшифровок</a></li>
+                <li><a class="dropdown-item @if(($active ?? '') === 'spare_parts_help') active @endif" href="{{ route('spare_parts.help') }}">Справка</a></li>
+            </ul>
+        </li>
     @endif
 
-    @if(hasPermission('schedule.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'schedule') active @endif"
-                                href="{{ route('schedule.index', session('gp_schedule')) }}">График монтажей</a></li>
-    @endif
+    <li class="nav-item dropdown">
+        <a class="nav-link dropdown-toggle @if($stockActive) active @endif" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+            Склад наличие
+        </a>
+        <ul class="dropdown-menu">
+            <li><a class="dropdown-item @if(($active ?? '') === 'stock') active @endif" href="{{ route('stock.index') }}">Наличие</a></li>
+            <li><a class="dropdown-item @if(($active ?? '') === 'stock_orders') active @endif" href="{{ route('stock.orders') }}">Заказы</a></li>
+        </ul>
+    </li>
 
+    <li class="nav-item">
+        <a class="nav-link @if(($active ?? '') === 'common_catalog') active @endif" href="{{ route('common-catalog.index') }}">Каталог общий</a>
+    </li>
+
+    <li class="nav-item">
+        <a class="nav-link @if(($active ?? '') === 'documents') active @endif" href="{{ route('documents.index') }}">Документация</a>
+    </li>
+
+    <li class="nav-item">
+        <a class="nav-link @if(($active ?? '') === 'calculations') active @endif" href="{{ route('calculations.index') }}">Калькуляции</a>
+    </li>
 
-    @if(hasPermission('maf_orders.view'))
-        <li class="nav-item"><a class="nav-link @if($active == 'maf_order') active @endif"
-                                href="{{ route('maf_order.index', session('gp_maf_order')) }}">Заказы МАФ</a></li>
-    @endif
-    @if(hasPermission('contractors.view') && !hasAnyPermission([
-        'contracts.view',
-        'users.view',
-        'admin.settings.view',
-        'districts.view',
-        'areas.view',
-        'admin.notification_logs.view',
-        'import.view',
-        'admin.year_data.view',
-        'admin.clear_data.view',
-    ]))
-        <li class="nav-item"><a class="nav-link @if($active == 'contractors') active @endif"
-                                href="{{ route('contractors.index', session('gp_contractors')) }}">Подрядчики</a></li>
-    @endif
     @if(hasAnyPermission([
         'contractors.view',
         'contracts.view',
@@ -66,24 +139,22 @@
         'admin.clear_data.view',
     ]))
         <li class="nav-item dropdown">
-            <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
-               aria-haspopup="true" aria-expanded="false">
-                Администрирование
+            <a class="nav-link dropdown-toggle @if($adminActive) active @endif" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                Администратор
             </a>
             <ul class="dropdown-menu dropdown-menu-end">
-                @if(hasPermission('contractors.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('contractors.index', session('gp_contractors')) }}">Подрядчики</a></li>@endif
-                @if(hasPermission('contracts.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('contract.index', session('gp_contracts')) }}">Договоры</a></li>@endif
-                @if(hasPermission('users.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('user.index', session('gp_users')) }}">Пользователи</a></li>@endif
-                @if(hasPermission('admin.roles'))<li class="dropdown-item"><a class="nav-link" href="{{ route('admin.roles.index') }}">Роли и права</a></li>@endif
-                @if(hasPermission('admin.settings.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('admin.settings.index') }}">Настройки</a></li>@endif
-                @if(hasPermission('districts.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('admin.district.index') }}">Округа</a></li>@endif
-                @if(hasPermission('areas.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('admin.area.index') }}">Районы</a></li>@endif
-                @if(hasPermission('admin.notification_logs.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('admin.notifications.log') }}">Журнал уведомлений</a></li>@endif
-                @if(hasPermission('import.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('import.index', session('gp_import')) }}">Импорт</a></li>@endif
-                @if(hasPermission('admin.year_data.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('year-data.index') }}">Экспорт/Импорт года</a></li>@endif
-                @if(hasPermission('admin.clear_data.view'))<li class="dropdown-item"><a class="nav-link" href="{{ route('clear-data.index') }}">Удалить данные</a></li>@endif
+                @if(hasPermission('contractors.view'))<li><a class="dropdown-item @if(($active ?? '') === 'contractors') active @endif" href="{{ route('contractors.index', session('gp_contractors')) }}">Подрядчики</a></li>@endif
+                @if(hasPermission('contracts.view'))<li><a class="dropdown-item @if(($active ?? '') === 'contracts') active @endif" href="{{ route('contract.index', session('gp_contracts')) }}">Договоры</a></li>@endif
+                @if(hasPermission('users.view'))<li><a class="dropdown-item @if(($active ?? '') === 'users') active @endif" href="{{ route('user.index', session('gp_users')) }}">Пользователи</a></li>@endif
+                @if(hasPermission('admin.roles'))<li><a class="dropdown-item @if(($active ?? '') === 'roles') active @endif" href="{{ route('admin.roles.index') }}">Роли и права</a></li>@endif
+                @if(hasPermission('admin.settings.view'))<li><a class="dropdown-item @if(($active ?? '') === 'admin_settings') active @endif" href="{{ route('admin.settings.index') }}">Настройки</a></li>@endif
+                @if(hasPermission('districts.view'))<li><a class="dropdown-item @if(($active ?? '') === 'admin_districts') active @endif" href="{{ route('admin.district.index') }}">Округа</a></li>@endif
+                @if(hasPermission('areas.view'))<li><a class="dropdown-item @if(($active ?? '') === 'admin_areas') active @endif" href="{{ route('admin.area.index') }}">Районы</a></li>@endif
+                @if(hasPermission('admin.notification_logs.view'))<li><a class="dropdown-item @if(($active ?? '') === 'notification_logs') active @endif" href="{{ route('admin.notifications.log') }}">Журнал уведомлений</a></li>@endif
+                @if(hasPermission('import.view'))<li><a class="dropdown-item @if(($active ?? '') === 'import') active @endif" href="{{ route('import.index', session('gp_import')) }}">Импорт</a></li>@endif
+                @if(hasPermission('admin.year_data.view'))<li><a class="dropdown-item @if(($active ?? '') === 'year-data') active @endif" href="{{ route('year-data.index') }}">Экспорт/Импорт года</a></li>@endif
+                @if(hasPermission('admin.clear_data.view'))<li><a class="dropdown-item @if(($active ?? '') === 'clear-data') active @endif" href="{{ route('clear-data.index') }}">Удалить данные</a></li>@endif
             </ul>
         </li>
-
     @endif
 @endif

+ 28 - 22
resources/views/partials/table.blade.php

@@ -215,30 +215,36 @@
                                 <span class="badge bg-primary">Шифр расценки</span>
                             @endif
                         @elseif($id === 'pricing_codes' && $headerName === 'description')
-                            <div class="d-flex justify-content-between align-items-center">
-                                <span class="description-text-{{ $string->id }}">{{ $string->description }}</span>
-                                <button type="button" class="btn btn-sm btn-link edit-description"
-                                        data-id="{{ $string->id }}"
-                                        data-description="{{ $string->description }}">
-                                    <i class="bi bi-pencil"></i>
-                                </button>
-                            </div>
-                            <form action="{{ route('pricing_codes.update', $string) }}" method="POST" class="edit-form-{{ $string->id }} is-hidden">
-                                @csrf
-                                @method('PUT')
-                                <div class="input-group input-group-sm">
-                                    <input type="text" name="description" class="form-control" value="{{ $string->description }}">
-                                    <button type="submit" class="btn btn-success">Сохранить</button>
-                                    <button type="button" class="btn btn-secondary cancel-edit" data-id="{{ $string->id }}">Отмена</button>
+                            @if(hasPermission('pricing_codes.update'))
+                                <div class="d-flex justify-content-between align-items-center">
+                                    <span class="description-text-{{ $string->id }}">{{ $string->description }}</span>
+                                    <button type="button" class="btn btn-sm btn-link edit-description"
+                                            data-id="{{ $string->id }}"
+                                            data-description="{{ $string->description }}">
+                                        <i class="bi bi-pencil"></i>
+                                    </button>
                                 </div>
-                            </form>
+                                <form action="{{ route('pricing_codes.update', $string) }}" method="POST" class="edit-form-{{ $string->id }} is-hidden">
+                                    @csrf
+                                    @method('PUT')
+                                    <div class="input-group input-group-sm">
+                                        <input type="text" name="description" class="form-control" value="{{ $string->description }}">
+                                        <button type="submit" class="btn btn-success">Сохранить</button>
+                                        <button type="button" class="btn btn-secondary cancel-edit" data-id="{{ $string->id }}">Отмена</button>
+                                    </div>
+                                </form>
+                            @else
+                                {{ $string->description }}
+                            @endif
                         @elseif($id === 'pricing_codes' && $headerName === 'actions')
-                            <form action="{{ route('pricing_codes.destroy', $string) }}" method="POST" class="d-inline js-confirm-submit"
-                                  data-confirm-message="Удалить код {{ $string->code }}?">
-                                @csrf
-                                @method('DELETE')
-                                <button type="submit" class="btn btn-sm btn-danger">Удалить</button>
-                            </form>
+                            @if(hasPermission('pricing_codes.delete'))
+                                <form action="{{ route('pricing_codes.destroy', $string) }}" method="POST" class="d-inline js-confirm-submit"
+                                      data-confirm-message="Удалить код {{ $string->code }}?">
+                                    @csrf
+                                    @method('DELETE')
+                                    <button type="submit" class="btn btn-sm btn-danger">Удалить</button>
+                                </form>
+                            @endif
                         @elseif($id === 'contractor_prices' && $headerName === 'status_name')
                             @if($string->available)
                                 <span class="badge text-bg-success">Доступен</span>

+ 23 - 15
resources/views/pricing_codes/index.blade.php

@@ -13,16 +13,20 @@
                         Каталог
                     </a>
                 </li>
-                <li class="nav-item">
-                    <a class="nav-link" href="{{ route('spare_part_orders.index', session('gp_spare_part_orders', [])) }}">
-                        Заказы деталей
-                    </a>
-                </li>
-                <li class="nav-item">
-                    <a class="nav-link" href="{{ route('spare_part_inventory.index') }}">
-                        Контроль наличия
-                    </a>
-                </li>
+                @if(hasPermission('spare_part_orders.view'))
+                    <li class="nav-item">
+                        <a class="nav-link" href="{{ route('spare_part_orders.index', session('gp_spare_part_orders', [])) }}">
+                            Заказы запчастей
+                        </a>
+                    </li>
+                @endif
+                @if(hasPermission('spare_part_inventory.view'))
+                    <li class="nav-item">
+                        <a class="nav-link" href="{{ route('spare_part_inventory.index') }}">
+                            Контроль наличия
+                        </a>
+                    </li>
+                @endif
                 <li class="nav-item">
                     <a class="nav-link active" href="{{ route('pricing_codes.index', session('gp_pricing_codes', [])) }}">
                         Справочник расшифровок
@@ -37,11 +41,13 @@
             </ul>
 
             {{-- Кнопки управления --}}
-            <div class="mb-3">
-                <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addCodeModal">
-                    Добавить код
-                </button>
-            </div>
+            @if(hasPermission('pricing_codes.create'))
+                <div class="mb-3">
+                    <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addCodeModal">
+                        Добавить код
+                    </button>
+                </div>
+            @endif
 
             @if(isset($pricing_codes))
                 @include('partials.table', [
@@ -63,6 +69,7 @@
     </div>
 </div>
 
+@if(hasPermission('pricing_codes.create'))
 {{-- Модальное окно добавления кода --}}
 <div class="modal fade" id="addCodeModal" tabindex="-1" aria-labelledby="addCodeModalLabel" aria-hidden="true">
     <div class="modal-dialog">
@@ -98,6 +105,7 @@
         </div>
     </div>
 </div>
+@endif
 
 @push('scripts')
 <script type="module">

+ 16 - 14
resources/views/spare_parts/index.blade.php

@@ -14,25 +14,27 @@
                         Каталог
                     </a>
                 </li>
-                <li class="nav-item">
-                    <a class="nav-link {{ ($tab ?? '') === 'orders' ? 'active' : '' }}"
-                       href="{{ route('spare_part_orders.index', session('gp_spare_part_orders', [])) }}">
-                        Заказы деталей
-                    </a>
-                </li>
-                <li class="nav-item">
-                    <a class="nav-link {{ ($tab ?? '') === 'inventory' ? 'active' : '' }}"
-                       href="{{ route('spare_part_inventory.index') }}">
-                        Контроль наличия
-                    </a>
-                </li>
-                @if(hasAnyPermission(['spare_parts.create', 'spare_parts.import', 'spare_parts.export', 'spare_parts.delete']))
+                @if(hasPermission('spare_part_orders.view'))
+                    <li class="nav-item">
+                        <a class="nav-link {{ ($tab ?? '') === 'orders' ? 'active' : '' }}"
+                           href="{{ route('spare_part_orders.index', session('gp_spare_part_orders', [])) }}">
+                            Заказы запчастей
+                        </a>
+                    </li>
+                @endif
+                @if(hasPermission('spare_part_inventory.view'))
+                    <li class="nav-item">
+                        <a class="nav-link {{ ($tab ?? '') === 'inventory' ? 'active' : '' }}"
+                           href="{{ route('spare_part_inventory.index') }}">
+                            Контроль наличия
+                        </a>
+                    </li>
+                @endif
                 <li class="nav-item">
                     <a class="nav-link" href="{{ route('pricing_codes.index', session('gp_pricing_codes', [])) }}">
                         Справочник расшифровок
                     </a>
                 </li>
-                @endif
                 <li class="nav-item">
                     <a class="nav-link {{ ($tab ?? '') === 'help' ? 'active' : '' }}"
                        href="{{ route('spare_parts.help', session('gp_spare_parts', [])) }}">

+ 39 - 1
routes/web.php

@@ -26,6 +26,7 @@ use App\Http\Controllers\SparePartController;
 use App\Http\Controllers\SparePartInventoryController;
 use App\Http\Controllers\SparePartOrderController;
 use App\Http\Controllers\SparePartReservationController;
+use App\Http\Controllers\UnderDevelopmentController;
 use App\Http\Controllers\UserController;
 use App\Http\Controllers\UserNotificationController;
 use Illuminate\Http\Request;
@@ -151,6 +152,43 @@ Route::middleware(['auth:web', 'route.permission'])->group(function () {
 
     Route::get('get-filters', [FilterController::class, 'getFilters'])->name('getFilters');
 
+    Route::get('common-catalog', UnderDevelopmentController::class)
+        ->defaults('title', 'Каталог общий')
+        ->defaults('active', 'common_catalog')
+        ->name('common-catalog.index');
+
+    Route::prefix('schedules')->name('schedule.')->group(function () {
+        Route::get('orders', UnderDevelopmentController::class)
+            ->defaults('title', 'График заказов')
+            ->defaults('active', 'schedule_orders')
+            ->name('orders');
+        Route::get('deliveries', UnderDevelopmentController::class)
+            ->defaults('title', 'График доставок')
+            ->defaults('active', 'schedule_deliveries')
+            ->name('deliveries');
+    });
+
+    Route::prefix('stock')->name('stock.')->group(function () {
+        Route::get('', UnderDevelopmentController::class)
+            ->defaults('title', 'Склад наличие')
+            ->defaults('active', 'stock')
+            ->name('index');
+        Route::get('orders', UnderDevelopmentController::class)
+            ->defaults('title', 'Заказы склада')
+            ->defaults('active', 'stock_orders')
+            ->name('orders');
+    });
+
+    Route::get('documents', UnderDevelopmentController::class)
+        ->defaults('title', 'Документация')
+        ->defaults('active', 'documents')
+        ->name('documents.index');
+
+    Route::get('calculations', UnderDevelopmentController::class)
+        ->defaults('title', 'Калькуляции')
+        ->defaults('active', 'calculations')
+        ->name('calculations.index');
+
     Route::group([], function () {
         Route::prefix('responsibles')->group(function (){
             Route::get('', [ResponsibleController::class, 'index'])->name('responsible.index');
@@ -347,7 +385,7 @@ Route::middleware(['auth:web', 'route.permission'])->group(function () {
         Route::post('/{sparePart}/upload-image', [SparePartController::class, 'uploadImage'])->name('upload_image');
     });
 
-    // Заказы деталей
+    // Заказы запчастей
     Route::prefix('spare-part-orders')->name('spare_part_orders.')->group(function () {
         Route::get('/', [SparePartOrderController::class, 'index'])->name('index');
         Route::get('/create', [SparePartOrderController::class, 'create'])->name('create');

+ 156 - 0
tests/Feature/ManagerMenuTest.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Models\Permission;
+use App\Models\PricingCode;
+use App\Models\Role;
+use App\Models\User;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Tests\TestCase;
+
+class ManagerMenuTest extends TestCase
+{
+    use RefreshDatabase;
+
+    protected $seed = true;
+
+    public function test_admin_sees_existing_sections_in_new_menu_groups(): void
+    {
+        $user = $this->createSystemRoleUser(Role::ADMIN);
+
+        $response = $this->actingAs($user)->get(route('common-catalog.index'));
+
+        $response->assertOk()
+            ->assertSeeText('ДКР')
+            ->assertSee(route('order.index'), false)
+            ->assertSee(route('product_sku.index'), false)
+            ->assertSee(route('catalog.index'), false)
+            ->assertSee(route('reports.index'), false)
+            ->assertSee(route('responsible.index'), false)
+            ->assertSee(route('maf_order.index'), false)
+            ->assertSeeText('Графики')
+            ->assertSee(route('schedule.index'), false)
+            ->assertSeeText('Запчасти')
+            ->assertSee(route('spare_parts.index'), false)
+            ->assertSee(route('spare_part_orders.index'), false)
+            ->assertSee(route('spare_part_inventory.index'), false)
+            ->assertSee(route('pricing_codes.index'), false)
+            ->assertSeeText('Администратор')
+            ->assertSee(route('contractors.index'), false)
+            ->assertSee(route('contract.index'), false)
+            ->assertSee(route('user.index'), false)
+            ->assertSee(route('admin.roles.index'), false)
+            ->assertSee(route('admin.settings.index'), false)
+            ->assertSee(route('admin.district.index'), false)
+            ->assertSee(route('admin.area.index'), false)
+            ->assertSee(route('admin.notifications.log'), false)
+            ->assertSee(route('import.index'), false)
+            ->assertSee(route('year-data.index'), false)
+            ->assertSee(route('clear-data.index'), false)
+            ->assertDontSeeText('Администрирование');
+
+        $this->assertSame(1, substr_count($response->getContent(), '>Подрядчики</a>'));
+    }
+
+    public function test_contractor_viewer_sees_contractors_only_inside_admin_group(): void
+    {
+        $user = $this->createUserWithPermissions('contractor_viewer', ['contractors.view']);
+
+        $response = $this->actingAs($user)->get(route('common-catalog.index'));
+
+        $response->assertOk()
+            ->assertSeeText('Администратор')
+            ->assertSee(route('contractors.index'), false);
+
+        $this->assertSame(1, substr_count($response->getContent(), '>Подрядчики</a>'));
+    }
+
+    public function test_spare_parts_viewer_can_read_pricing_codes_without_edit_controls(): void
+    {
+        $user = $this->createUserWithPermissions('spare_parts_reader', ['spare_parts.view']);
+        $pricingCode = PricingCode::query()->create([
+            'type' => PricingCode::TYPE_PRICING_CODE,
+            'code' => 'TEST-01',
+            'description' => 'Тестовая расшифровка',
+        ]);
+
+        $response = $this->actingAs($user)->get(route('pricing_codes.index'));
+
+        $response->assertOk()
+            ->assertSeeText('Справочник расшифровок')
+            ->assertSeeText('TEST-01')
+            ->assertSeeText('Тестовая расшифровка')
+            ->assertSee(route('spare_parts.index'), false)
+            ->assertSee(route('spare_parts.help'), false)
+            ->assertDontSee(route('spare_part_orders.index'), false)
+            ->assertDontSee(route('spare_part_inventory.index'), false)
+            ->assertDontSee('data-bs-target="#addCodeModal"', false)
+            ->assertDontSee('action="'.route('pricing_codes.update', $pricingCode).'"', false)
+            ->assertDontSee('action="'.route('pricing_codes.destroy', $pricingCode).'"', false)
+            ->assertDontSee('data-confirm-message=', false);
+
+        $this->actingAs($user)
+            ->post(route('pricing_codes.store'), [
+                'type' => PricingCode::TYPE_PRICING_CODE,
+                'code' => 'TEST-02',
+            ])
+            ->assertForbidden();
+    }
+
+    public function test_spare_parts_subsections_set_their_own_active_menu_item(): void
+    {
+        $user = $this->createSystemRoleUser(Role::ADMIN);
+
+        $this->actingAs($user)
+            ->get(route('spare_part_orders.index'))
+            ->assertOk()
+            ->assertViewHas('active', 'spare_part_orders')
+            ->assertSeeText('Заказы запчастей');
+
+        $this->actingAs($user)
+            ->get(route('spare_part_inventory.index'))
+            ->assertOk()
+            ->assertViewHas('active', 'spare_part_inventory');
+
+        $this->actingAs($user)
+            ->get(route('pricing_codes.index'))
+            ->assertOk()
+            ->assertViewHas('active', 'pricing_codes');
+
+        $this->actingAs($user)
+            ->get(route('spare_parts.help'))
+            ->assertOk()
+            ->assertViewHas('active', 'spare_parts_help');
+    }
+
+    private function createSystemRoleUser(string $slug): User
+    {
+        $role = Role::query()->where('slug', $slug)->firstOrFail();
+
+        return User::factory()->create([
+            'role' => $role->slug,
+            'role_id' => $role->id,
+        ]);
+    }
+
+    private function createUserWithPermissions(string $slug, array $permissionSlugs): User
+    {
+        $role = Role::query()->create([
+            'slug' => $slug,
+            'name' => $slug,
+            'is_system' => false,
+            'is_active' => true,
+        ]);
+        $permissions = Permission::query()
+            ->whereIn('slug', $permissionSlugs)
+            ->pluck('id')
+            ->mapWithKeys(fn (int $id): array => [$id => ['effect' => 'allow']]);
+        $role->permissions()->sync($permissions);
+
+        return User::factory()->create([
+            'role' => $role->slug,
+            'role_id' => $role->id,
+        ]);
+    }
+}