Документация ЛК
Модули

Календарь

Модуль "Календарь производства"

Версия: 1.0
Дата: 2025-11-26
Статус: Реализован с проблемами


Содержание

  1. Общее описание
  2. Текущая реализация
  3. Требования заказчика
  4. Выявленные проблемы
  5. Рекомендации по улучшению
  6. Неоптимальности в коде

1. Общее описание

1.1 Предназначение модуля

Модуль "Календарь производства" предназначен для:

  • Планирования производства — визуализация загрузки заводов по дням
  • Отслеживания заказов — просмотр заказов на конкретную дату
  • Бронирования слотов — резервирование производственного времени
  • Управления датами — перемещение заказов между датами (Drag&Drop)

1.2 Два типа календаря

ТипURLНазначение
Календарь производства/calendar/*Для админов, менеджеров, производства
Календарь дилера/calendar-dealer/*Для дилеров (только свои заказы)

1.3 Режимы просмотра

РежимURLОписание
День/calendar/day/{date}Таблица заказов на один день
Неделя/calendar/week/{dates}7 колонок с заказами, Drag&Drop
Месяц/calendar/month/{dates}Сетка дней с заказами, Drag&Drop

2. Текущая реализация

2.1 Архитектура

┌─────────────────────────────────────────────────────────────────────────────┐
│                        МОДУЛЬ КАЛЕНДАРЬ                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  РОУТЫ (routes/web.php)                                                      │
│  ├── /calendar/day/{day?}       → CalendarController@day                     │
│  ├── /calendar/week/{days?}     → CalendarController@week                    │
│  ├── /calendar/month/{days?}    → CalendarController@month                   │
│  ├── /calendar-dealer/day/{day?}    → CalendarDealerController@day           │
│  ├── /calendar-dealer/week/{days?}  → CalendarDealerController@week          │
│  └── /calendar-dealer/month/{days?} → CalendarDealerController@month         │
│                                                                              │
│  LIVEWIRE КОМПОНЕНТЫ                                                         │
│  ├── Calendar/                      │ CalendarDealer/                        │
│  │   ├── Calendar.php (DatePicker) │   ├── Calendar.php (DatePicker)        │
│  │   ├── Selector.php (завод)      │   ├── Day.php (только свои заказы)     │
│  │   ├── Day.php (таблица)         │   ├── Week.php                         │
│  │   ├── Week.php                  │   ├── WeekDay.php                      │
│  │   ├── WeekDay.php               │   ├── Month.php                        │
│  │   ├── Month.php                 │   └── MonthDay.php                     │
│  │   ├── MonthDay.php              │                                        │
│  │   └── MonthCalendar.php         │                                        │
│                                                                              │
│  СЕРВИСЫ                                                                     │
│  └── App\Services\Septik                                                     │
│      ├── getFreeTime()      — свободное время на дату                        │
│      ├── getFreeDates()     — свободные даты для товара                      │
│      ├── getFreeDatesByTime()— свободные даты по времени                     │
│      └── getProductivityTime()— производительность завода по дню недели      │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 Модели данных

Таблица manufactures (Заводы/Производства)

ПолеТипОписание
idbigintPK
namestringНазвание завода
citystringГород
activebooleanАктивен
productivityjsonГрафик производительности по дням

Пример productivity:

[
  {"days": [1,2,3,4,5], "time": 600},  // Пн-Пт: 600 единиц
  {"days": [6,7], "time": 400}         // Сб-Вс: 400 единиц
]

Таблица reservations (Бронирования)

ПолеТипОписание
idbigintPK
datedateДата бронирования
manufacture_idFKЗавод
user_idFKКто забронировал
timeintЗабронированное время

Связь с заказами

// Заказ содержит:
- date_make дата производства (занимает слот)
- date_delivery дата доставки (не занимает слот)
- manufacture_id привязка к заводу

2.3 Права доступа

ПолитикаОписание
calendar_can_view_pathМожет видеть раздел календаря

Применение по ролям:

Роль/calendar/*/calendar-dealer/*Выбор заводаDrag&DropБронирование
Администратор✅ Все заводы
Менеджер✅ Все заводы
Производство⚠️ Только свой
Дилер

2.4 Интерфейс для Админа/Менеджера

Дневной вид (таблица Filament)

┌─────────────────────────────────────────────────────────────────────────────┐
│ Календарь производства                       [День] [Неделя] [Месяц]        │
├─────────────────────────────────────────────────────────────────────────────┤
│ [Производство Верховье ▾]                              [📅 04.12.2025]       │
├─────────────────────────────────────────────────────────────────────────────┤
│ [Поиск...]                                                    [Столбцы ▾]   │
├────┬─────────────────┬───────────┬────────────┬─────────┬──────────────────┤
│ ID │ Товар           │ Дилер     │ Дата произ.│ Сумма   │ Производство     │
├────┼─────────────────┼───────────┼────────────┼─────────┼──────────────────┤
│ 25 │ Тверь CLASSIC   │ ООО Рога  │ 04.12.2025 │ 125000₽ │ Верховье         │
│ 26 │ Кессон К-2      │ ИП Петров │ 04.12.2025 │ 45000₽  │ Верховье         │
└────┴─────────────────┴───────────┴────────────┴─────────┴──────────────────┘

Недельный вид

┌─────────────────────────────────────────────────────────────────────────────┐
│ Календарь производства                       [День] [Неделя] [Месяц]        │
├─────────────────────────────────────────────────────────────────────────────┤
│ [Производство Верховье ▾]               < 01.12.2025 - 07.12.2025 >         │
├─────────────────────────────────────────────────────────────────────────────┤
│ ПН 01.12      │ ВТ 02.12     │ СР 03.12     │ ЧТ 04.12      │ ...          │
│               │              │              │ Свободно: 600  │              │
│               │              │              │ ┌──────────┐   │              │
│               │              │              │ │Заказ #25 │   │              │
│               │              │              │ │Тверь CLS │   │              │
│               │              │              │ └──────────┘   │              │
│               │              │              │[Забронировать] │              │
└─────────────────────────────────────────────────────────────────────────────┘

Функции:
✅ Drag&Drop заказов между днями
✅ Кнопка "Забронировать" (модальное окно)
✅ Показ свободного времени
✅ Навигация по неделям (< >)
✅ Ссылки на карточки заказов

Месячный вид

┌─────────────────────────────────────────────────────────────────────────────┐
│ Календарь производства                       [День] [Неделя] [Месяц]        │
├─────────────────────────────────────────────────────────────────────────────┤
│ [Производство Верховье ▾]            [📅 01.12.2025] [📅 11.01.2026]         │
├───────────┬───────────┬───────────┬───────────┬───────────┬───────┬─────────┤
│ Понедельн.│ Вторник   │ Среда     │ Четверг   │ Пятница   │Суббота│Воскрес. │
├───────────┼───────────┼───────────┼───────────┼───────────┼───────┼─────────┤
│ 01 декабря│ 02 декабря│ 03 декабря│ 04 декабря│ 05 декабря│ 06 дек│ 07 дек  │
│           │           │           │ Свободно: │ Свободно: │Своб:  │ Своб:   │
│           │           │           │ 600       │ 600       │ 400   │ 400     │
│           │           │           │┌─────────┐│[Заброн.] │[Забр.]│[Забр.]  │
│           │           │           ││№25      ││          │       │         │
│           │           │           ││Тверь CLS││          │       │         │
│           │           │           │└─────────┘│          │       │         │
│           │           │           │[Заброн.] │          │       │         │
├───────────┼───────────┼───────────┼───────────┼───────────┼───────┼─────────┤
│ 08 декабря│ ...       │           │           │           │       │         │
└───────────┴───────────┴───────────┴───────────┴───────────┴───────┴─────────┘

Функции:
✅ Drag&Drop заказов между днями
✅ Кнопка "Забронировать"
✅ Выбор диапазона дат
✅ Цветовое выделение (прошлое — серое, будущее — синее)

2.5 Интерфейс для Дилера

Отличия от админского календаря:

ФункцияАдминДилер
Выбор производства
Показ свободного времени
Кнопка "Забронировать"
Drag&Drop
Фильтрация заказовПо заводуТолько свои
Разделение заказовПо дате производстваПроизводство + Поставка

Недельный вид дилера

┌─────────────────────────────────────────────────────────────────────────────┐
│ Календарь                                    [День] [Неделя] [Месяц]        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                     < 01.12.2025 - 07.12.2025 >             │
├─────────────────────────────────────────────────────────────────────────────┤
│ ПН 01.12      │ ВТ 02.12     │ СР 03.12     │ ЧТ 04.12      │ ...          │
│               │              │              │               │              │
│               │              │ Производство │               │              │
│               │              │ ┌──────────┐ │               │              │
│               │              │ │Заказ #25 │ │               │              │
│               │              │ │Тверь CLS │ │               │              │
│               │              │ └──────────┘ │               │              │
│               │              │              │               │              │
│               │              │ Поставка     │               │              │
│               │              │ ┌──────────┐ │               │              │
│               │              │ │Заказ #26 │ │               │              │
│               │              │ └──────────┘ │               │              │
└─────────────────────────────────────────────────────────────────────────────┘

Показывает:
- date_make — под заголовком "Производство"
- date_delivery — под заголовком "Поставка"

2.6 Бизнес-логика

Расчёт свободного времени

// App\Services\Septik::getFreeTime()

свободное_время = производительность_завода - время_заказов - время_бронирований

// Где:
// - производительность_завода = из productivity JSON по дню недели
// - время_заказов = SUM(product.time) для всех заказов на эту дату
// - время_бронирований = SUM(reservation.time) для всех бронирований

Drag&Drop логика

// week.blade.php / month.blade.php

// 1. При dragstart — сохраняем ID заказа
dragged = event.target; // data-id содержит order.id

// 2. При drop — вызываем Livewire метод
$wire.changeDate(dragged.dataset.id, event.target.dataset.day)

// 3. В Livewire компоненте
public function changeDate($id, $date) {
    $order = Order::find($id);
    $order->update(['date_make' => Carbon::parse($date)]);
    $this->dispatch('refresh-week-day'); // или refresh-moth-day
}

Бронирование

// WeekDay.php / MonthDay.php

public function createAction(): CreateAction {
    return CreateAction::make('create')
        ->label('Забронировать')
        ->action(function (array $data): void {
            Reservation::create([
                'user_id' => Auth::id(),
                'date' => $this->date,
                'manufacture_id' => session('calendar_selector'),
                'time' => $data['time'],
            ]);
        })
        ->form([
            TextInput::make('time')->numeric()->required(),
        ]);
}

3. Требования заказчика

Источники: /docs/Список_проблем_и_задач_по_системе_ЛК_дилеров.docx, /docs/Пояснительная_записка_к_системе_ЛК.docx

3.1 Из списка проблем

#ТребованиеСтатусКомментарий
1Проблемы с видимостью — дилер видит, админ НЕ видит⚠️Нужно проверить
2Drag&Drop перемещение заказов между датамиРеализовано
3Два статуса в календаре — производство и отгрузка⚠️Частично (для дилера)
4Баг при удалении даты в фильтреНужно исправить
5Заказы "Произведено" не занимают слотНе реализовано
6Роль Логиста для перемещения заказов к отгрузкеРоль не создана

3.2 Из пояснительной записки

#ТребованиеСтатус
1Календарь с монтажами для менеджера❌ Нет монтажей
2Фильтры по менеджеру❌ Нет
3Уведомления перед монтажом❌ Нет
4Галочка готовности монтажа❌ Нет
5Синхронизация с Google Calendar❌ Нет
6Синхронизация с двумя заводами✅ Реализовано
7Показ свободных мест✅ Реализовано

3.3 Нереализованные требования

  1. Календарь монтажей — отдельный функционал для планирования монтажных работ
  2. Роль Логиста — перемещение заказов в статусе "Произведено" для планирования отгрузки
  3. Разделение слотов — производственные слоты vs слоты отгрузки
  4. Синхронизация с внешними календарями — Google Calendar

4. Выявленные проблемы

4.1 Критические

#ПроблемаВлияниеРешение
1Нет проверки доступа при Drag&DropБезопасностьДобавить проверку прав в changeDate()
2Нет валидации свободного времениПерегрузка производстваПроверять время перед перемещением
3Тайпо в событии refresh-moth-dayБагИсправить на refresh-month-day

4.2 Средние

#ПроблемаВлияниеРешение
4Дублирование кода Calendar vs CalendarDealerПоддержкаРефакторинг в общие компоненты
5Нет лимита бронированияМожно забронировать больше, чем естьДобавить валидацию
6Нет отмены бронированияUXДобавить кнопку удаления
7Прошлые дни draggableUX путаницаОтключить draggable для прошлого

4.3 Низкие

#ПроблемаВлияниеРешение
8Нет индикатора загрузки при Drag&DropUXДобавить loader
9Не сохраняется выбор завода в URLUXДобавить в query string
10Заголовок "Очередь производства" для дилера некорректенUXПоменять на "Мои заказы"

5. Рекомендации по улучшению

5.1 Приоритет: Высокий

5.1.1 Исправить тайпо в событии

Файл: app/Livewire/Calendar/Month.php (строка 47)

// Было:
$this->dispatch('refresh-moth-day');

// Стало:
$this->dispatch('refresh-month-day');

Файл: app/Livewire/Calendar/MonthDay.php (строка 35)

// Было:
#[On('refresh-moth-day')]

// Стало:
#[On('refresh-month-day')]

5.1.2 Добавить проверку прав при перемещении

// Calendar/Week.php и Month.php

public function changeDate($id, $date)
{
    $user = auth()->user();
    
    // Проверка прав
    if (!$user->check_access('calendar_can_edit')) {
        Notification::make()->title('Нет прав на редактирование')->danger()->send();
        return;
    }
    
    $order = Order::find($id);
    
    // Проверка владения (для менеджера — только заказы своих дилеров)
    if ($user->group_id == 3) { // Менеджер
        $dealerIds = User::where('manager_id', $user->id)->pluck('id');
        if (!$dealerIds->contains($order->dealer_id)) {
            Notification::make()->title('Заказ не принадлежит вашим дилерам')->danger()->send();
            return;
        }
    }
    
    // Проверка свободного времени
    $neededTime = $order->product?->time ?? 0;
    $freeTime = Septik::getFreeTime(session('calendar_selector'), Carbon::parse($date));
    
    if ($neededTime > $freeTime) {
        Notification::make()
            ->title('Недостаточно свободного времени')
            ->body("Требуется: {$neededTime}, доступно: {$freeTime}")
            ->danger()
            ->send();
        return;
    }
    
    $order->update(['date_make' => Carbon::parse($date)]);
    $this->dispatch('refresh-week-day');
}

5.1.3 Разделить слоты производства и отгрузки

// В Septik::getFreeTime() не учитывать заказы со статусом "Произведено"

$orders = Order::where('manufacture_id', $manufacture)
    ->whereDate('date_make', $date)
    ->whereNotIn('status_id', [/* ID статусов "Произведено", "Отгружен" */])
    ->get();

5.2 Приоритет: Средний

5.2.1 Добавить политику calendar_can_edit

// DefaultDataSeeder.php
Policy::firstOrCreate(
    ['key' => 'calendar_can_edit'],
    [
        'name' => 'Может редактировать календарь',
        'category' => 'Календарь',
    ]
);

5.2.2 Добавить удаление бронирования

// WeekDay.php / MonthDay.php

public function deleteReservationAction(): Action
{
    return Action::make('deleteReservation')
        ->label('Удалить')
        ->color('danger')
        ->size(ActionSize::ExtraSmall)
        ->requiresConfirmation()
        ->action(function (array $arguments): void {
            Reservation::find($arguments['id'])?->delete();
            $this->dispatch('refresh-week-day');
        });
}

5.3 Приоритет: Низкий

5.3.1 Рефакторинг — общий базовый класс

// app/Livewire/Calendar/BaseWeekDay.php

abstract class BaseWeekDay extends Component
{
    public Carbon $date;
    public $minTime;
    
    abstract protected function getOrders(): Collection;
    abstract protected function canBook(): bool;
    
    // Общая логика...
}

// app/Livewire/Calendar/WeekDay.php
class WeekDay extends BaseWeekDay
{
    protected function getOrders(): Collection
    {
        return Order::where('manufacture_id', session('calendar_selector'))
            ->whereDate('date_make', $this->date)->get();
    }
    
    protected function canBook(): bool
    {
        return true;
    }
}

// app/Livewire/CalendarDealer/WeekDay.php
class WeekDay extends BaseWeekDay
{
    protected function getOrders(): Collection
    {
        return Order::where('dealer_id', auth()->id())
            ->whereDate('date_make', $this->date)->get();
    }
    
    protected function canBook(): bool
    {
        return false;
    }
}

6. Неоптимальности в коде

6.1 Дублирование кода

Проблема: Почти идентичные компоненты в Calendar/ и CalendarDealer/

Файл CalendarФайл CalendarDealerРазличия
Calendar.phpCalendar.phpТолько route name
Day.phpDay.phpФильтр по manufacture vs dealer
Week.phpWeek.phpБез Drag&Drop у дилера
WeekDay.phpWeekDay.phpБронирование vs разделение по типу
Month.phpMonth.phpИдентичны
MonthDay.phpMonthDay.phpБронирование vs разделение
MonthCalendar.phpMonthCalendar.phpТолько route name

Рекомендация: Создать базовые классы и наследовать от них.

6.2 Неиспользуемые переменные

// Calendar/Day.php строка 23
public $manufacture; // Не используется явно, берётся из session

// Calendar/Month.php строка 15
public $status; // Присваивается, но не используется

6.3 Потенциальная N+1 проблема

// WeekDay.php
$orders = Order::where('manufacture_id', session('calendar_selector'))
    ->whereDate('date_make', $this->date)->get();

// В шаблоне:
@foreach ($orders as $order)
    {{ $order->product->name }} // N+1 запрос!

Решение:

$orders = Order::with('product')
    ->where('manufacture_id', session('calendar_selector'))
    ->whereDate('date_make', $this->date)->get();

6.4 Отсутствие типизации

// Плохо:
public $manufacture;
public $date;

// Хорошо:
public ?int $manufacture = null;
public ?Carbon $date = null;

6.5 Хардкод в шаблонах

// month-day.blade.php — хардкод месяцев
$months = [
    1 => 'января',
    // ...
];

// Лучше использовать Carbon с локализацией:
Carbon::setLocale('ru');
$this->date->translatedFormat('d F');

Приложение A: Структура файлов

app/
├── Http/Controllers/
│   ├── CalendarController.php
│   └── CalendarDealerController.php
├── Livewire/
│   ├── Calendar/
│   │   ├── Calendar.php
│   │   ├── Day.php
│   │   ├── Month.php
│   │   ├── MonthCalendar.php
│   │   ├── MonthDay.php
│   │   ├── Selector.php
│   │   ├── Week.php
│   │   └── WeekDay.php
│   └── CalendarDealer/
│       ├── Calendar.php
│       ├── Day.php
│       ├── Month.php
│       ├── MonthCalendar.php
│       ├── MonthDay.php
│       ├── Week.php
│       └── WeekDay.php
├── Models/
│   ├── Manufacture.php
│   └── Reservation.php
└── Services/
    └── Septik.php

resources/views/
├── components/
│   ├── calendar/
│   │   ├── nav.blade.php
│   │   └── nav-item.blade.php
│   └── calendar-dealer/
│       └── nav.blade.php
├── livewire/
│   ├── calendar/
│   │   ├── month.blade.php
│   │   ├── month-day.blade.php
│   │   ├── selector.blade.php
│   │   ├── week.blade.php
│   │   └── week-day.blade.php
│   └── calendar-dealer/
│       ├── month.blade.php
│       ├── month-day.blade.php
│       ├── selector.blade.php
│       ├── week.blade.php
│       └── week-day.blade.php
└── templates/
    ├── calendar/
    │   ├── day.blade.php
    │   ├── month.blade.php
    │   └── week.blade.php
    └── calendar-dealer/
        ├── day.blade.php
        ├── month.blade.php
        └── week.blade.php

Приложение B: SQL для аналитики календаря

Загрузка производства по дням

SELECT 
    o.date_make,
    m.name as manufacture,
    COUNT(o.id) as orders_count,
    SUM(p.time) as total_time
FROM orders o
JOIN manufactures m ON o.manufacture_id = m.id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.date_make >= CURDATE()
  AND o.deleted_at IS NULL
GROUP BY o.date_make, m.id
ORDER BY o.date_make;

Бронирования по пользователям

SELECT 
    u.lastname, u.name,
    COUNT(r.id) as reservations,
    SUM(r.time) as total_time
FROM reservations r
JOIN users u ON r.user_id = u.id
WHERE r.date >= CURDATE()
GROUP BY u.id
ORDER BY total_time DESC;

Документ подготовлен на основе анализа исходного кода проекта и требований заказчика

On this page

Модуль "Календарь производства"Содержание1. Общее описание1.1 Предназначение модуля1.2 Два типа календаря1.3 Режимы просмотра2. Текущая реализация2.1 Архитектура2.2 Модели данныхТаблица manufactures (Заводы/Производства)Таблица reservations (Бронирования)Связь с заказами2.3 Права доступа2.4 Интерфейс для Админа/МенеджераДневной вид (таблица Filament)Недельный видМесячный вид2.5 Интерфейс для ДилераОтличия от админского календаря:Недельный вид дилера2.6 Бизнес-логикаРасчёт свободного времениDrag&Drop логикаБронирование3. Требования заказчика3.1 Из списка проблем3.2 Из пояснительной записки3.3 Нереализованные требования4. Выявленные проблемы4.1 Критические4.2 Средние4.3 Низкие5. Рекомендации по улучшению5.1 Приоритет: Высокий5.1.1 Исправить тайпо в событии5.1.2 Добавить проверку прав при перемещении5.1.3 Разделить слоты производства и отгрузки5.2 Приоритет: Средний5.2.1 Добавить политику calendar_can_edit5.2.2 Добавить удаление бронирования5.3 Приоритет: Низкий5.3.1 Рефакторинг — общий базовый класс6. Неоптимальности в коде6.1 Дублирование кода6.2 Неиспользуемые переменные6.3 Потенциальная N+1 проблема6.4 Отсутствие типизации6.5 Хардкод в шаблонахПриложение A: Структура файловПриложение B: SQL для аналитики календаряЗагрузка производства по днямБронирования по пользователям