Модули
Календарь
Версия: 1.0
Дата: 2025-11-26
Статус: Реализован с проблемами
Общее описание
Текущая реализация
Требования заказчика
Выявленные проблемы
Рекомендации по улучшению
Неоптимальности в коде
Модуль "Календарь производства" предназначен для:
Планирования производства — визуализация загрузки заводов по дням
Отслеживания заказов — просмотр заказов на конкретную дату
Бронирования слотов — резервирование производственного времени
Управления датами — перемещение заказов между датами (Drag&Drop)
Тип URL Назначение Календарь производства /calendar/*Для админов, менеджеров, производства Календарь дилера /calendar-dealer/*Для дилеров (только свои заказы)
Режим URL Описание День /calendar/day/{date}Таблица заказов на один день Неделя /calendar/week/{dates}7 колонок с заказами, Drag&Drop Месяц /calendar/month/{dates}Сетка дней с заказами, Drag&Drop
┌─────────────────────────────────────────────────────────────────────────────┐
│ МОДУЛЬ КАЛЕНДАРЬ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ РОУТЫ (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()— производительность завода по дню недели │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Поле Тип Описание idbigint PK namestring Название завода citystring Город activeboolean Активен productivityjson График производительности по дням
Пример productivity:
[
{ "days" : [ 1 , 2 , 3 , 4 , 5 ], "time" : 600 }, // Пн-Пт: 600 единиц
{ "days" : [ 6 , 7 ], "time" : 400 } // Сб-Вс: 400 единиц
]
Поле Тип Описание idbigint PK datedate Дата бронирования manufacture_idFK Завод user_idFK Кто забронировал timeint Забронированное время
// Заказ содержит:
- date_make — дата производства ( занимает слот )
- date_delivery — дата доставки ( не занимает слот )
- manufacture_id — привязка к заводу
Политика Описание calendar_can_view_pathМожет видеть раздел календаря
Применение по ролям:
Роль /calendar/*/calendar-dealer/*Выбор завода Drag&Drop Бронирование Администратор ✅ ❌ ✅ Все заводы ✅ ✅ Менеджер ✅ ❌ ✅ Все заводы ✅ ✅ Производство ✅ ❌ ⚠️ Только свой ✅ ✅ Дилер ❌ ✅ ❌ ❌ ❌
┌─────────────────────────────────────────────────────────────────────────────┐
│ Календарь производства [День] [Неделя] [Месяц] │
├─────────────────────────────────────────────────────────────────────────────┤
│ [Производство Верховье ▾] [📅 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 заказов между днями
✅ Кнопка "Забронировать"
✅ Выбор диапазона дат
✅ Цветовое выделение (прошлое — серое, будущее — синее)
Функция Админ Дилер Выбор производства ✅ ❌ Показ свободного времени ✅ ❌ Кнопка "Забронировать" ✅ ❌ Drag&Drop ✅ ❌ Фильтрация заказов По заводу Только свои Разделение заказов По дате производства Производство + Поставка
┌─────────────────────────────────────────────────────────────────────────────┐
│ Календарь [День] [Неделя] [Месяц] │
├─────────────────────────────────────────────────────────────────────────────┤
│ < 01.12.2025 - 07.12.2025 > │
├─────────────────────────────────────────────────────────────────────────────┤
│ ПН 01.12 │ ВТ 02.12 │ СР 03.12 │ ЧТ 04.12 │ ... │
│ │ │ │ │ │
│ │ │ Производство │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │Заказ #25 │ │ │ │
│ │ │ │Тверь CLS │ │ │ │
│ │ │ └──────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Поставка │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │Заказ #26 │ │ │ │
│ │ │ └──────────┘ │ │ │
└─────────────────────────────────────────────────────────────────────────────┘
Показывает:
- date_make — под заголовком "Производство"
- date_delivery — под заголовком "Поставка"
// App\Services\Septik::getFreeTime()
свободное_время = производительность_завода - время_заказов - время_бронирований
// Где:
// - производительность_завода = из productivity JSON по дню недели
// - время_заказов = SUM(product.time) для всех заказов на эту дату
// - время_бронирований = SUM(reservation.time) для всех бронирований
// 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 (),
]);
}
Источники: /docs/Список_проблем_и_задач_по_системе_ЛК_дилеров.docx, /docs/Пояснительная_записка_к_системе_ЛК.docx
# Требование Статус Комментарий 1 Проблемы с видимостью — дилер видит, админ НЕ видит⚠️ Нужно проверить 2 Drag&Drop перемещение заказов между датами✅ Реализовано 3 Два статуса в календаре — производство и отгрузка⚠️ Частично (для дилера) 4 Баг при удалении даты в фильтре ❌ Нужно исправить 5 Заказы "Произведено" не занимают слот ❌ Не реализовано 6 Роль Логиста для перемещения заказов к отгрузке❌ Роль не создана
# Требование Статус 1 Календарь с монтажами для менеджера ❌ Нет монтажей 2 Фильтры по менеджеру ❌ Нет 3 Уведомления перед монтажом ❌ Нет 4 Галочка готовности монтажа ❌ Нет 5 Синхронизация с Google Calendar ❌ Нет 6 Синхронизация с двумя заводами ✅ Реализовано 7 Показ свободных мест ✅ Реализовано
Календарь монтажей — отдельный функционал для планирования монтажных работ
Роль Логиста — перемещение заказов в статусе "Произведено" для планирования отгрузки
Разделение слотов — производственные слоты vs слоты отгрузки
Синхронизация с внешними календарями — Google Calendar
# Проблема Влияние Решение 1 Нет проверки доступа при Drag&Drop Безопасность Добавить проверку прав в changeDate() 2 Нет валидации свободного времени Перегрузка производства Проверять время перед перемещением 3 Тайпо в событии refresh-moth-dayБаг Исправить на refresh-month-day
# Проблема Влияние Решение 4 Дублирование кода Calendar vs CalendarDealerПоддержка Рефакторинг в общие компоненты 5 Нет лимита бронирования Можно забронировать больше, чем есть Добавить валидацию 6 Нет отмены бронирования UX Добавить кнопку удаления 7 Прошлые дни draggable UX путаница Отключить draggable для прошлого
# Проблема Влияние Решение 8 Нет индикатора загрузки при Drag&DropUX Добавить loader 9 Не сохраняется выбор завода в URLUX Добавить в query string 10 Заголовок "Очередь производства" для дилера некорректенUX Поменять на "Мои заказы"
Файл: 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' )]
// 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' );
}
// В Septik::getFreeTime() не учитывать заказы со статусом "Произведено"
$orders = Order :: where ( 'manufacture_id' , $manufacture)
-> whereDate ( 'date_make' , $date)
-> whereNotIn ( 'status_id' , [ /* ID статусов "Произведено", "Отгружен" */ ])
-> get ();
// DefaultDataSeeder.php
Policy :: firstOrCreate (
[ 'key' => 'calendar_can_edit' ],
[
'name' => 'Может редактировать календарь' ,
'category' => 'Календарь' ,
]
);
// 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' );
});
}
// 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 ;
}
}
Проблема: Почти идентичные компоненты в Calendar/ и CalendarDealer/
Файл Calendar Файл CalendarDealer Различия Calendar.php Calendar.php Только route name Day.php Day.php Фильтр по manufacture vs dealer Week.php Week.php Без Drag&Drop у дилера WeekDay.php WeekDay.php Бронирование vs разделение по типу Month.php Month.php Идентичны MonthDay.php MonthDay.php Бронирование vs разделение MonthCalendar.php MonthCalendar.php Только route name
Рекомендация: Создать базовые классы и наследовать от них.
// Calendar/Day.php строка 23
public $manufacture; // Не используется явно, берётся из session
// Calendar/Month.php строка 15
public $status; // Присваивается, но не используется
// 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 ();
// Плохо:
public $manufacture;
public $date;
// Хорошо:
public ?int $manufacture = null ;
public ? Carbon $date = null ;
// month-day.blade.php — хардкод месяцев
$months = [
1 => 'января' ,
// ...
];
// Лучше использовать Carbon с локализацией:
Carbon :: setLocale ( 'ru' );
$this -> date -> translatedFormat ( 'd F' );
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
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 ;
Документ подготовлен на основе анализа исходного кода проекта и требований заказчика