Рабочий календарь - график, праздники, расчёт срока
Считаем сроки с учётом выходных и праздников: хранение календаря, расчёт даты через несколько рабочих дней и понятный вывод покупателю.
Что нужно знать заранее
Праздники меняются каждый год и переносами внутри года. Держать их в коде нельзя: список обязан лежать данными, которые редакция правит без разработчика.
График работы точки выдачи и календарь праздников - это две разные вещи. График говорит, когда работает склад или пункт выдачи, а календарь - какие дни считаются рабочими для срока доставки.
Время отсечки приёма заказов сдвигает весь расчёт срока сразу на целые сутки. Заказ, оформленный вечером, уезжает следующим рабочим днём, и покупателю честнее сразу показать реальную дату, а не обещание «завтра».
Шаги
- Завести хранилище календаря: список праздников и правила обычных выходных дней недели.
- Написать одну общую функцию расчёта даты через нужное число рабочих дней.
- Учесть время отсечки заказа и часовой пояс покупателя уже при самом выводе.
- Показывать покупателю готовую дату словами, а не число рабочих дней в скобках.
- Проверить расчёт на длинных январских праздниках и на переносах рабочих суббот года.
Решение
Храним календарь данными:
// инфоблок «Календарь»: элемент на каждый особый день года$holidays = \CIBlockElement::GetList([], ['IBLOCK_ID' => $calendarId, 'ACTIVE' => 'Y'], false, false, ['ID', 'ACTIVE_FROM', 'PROPERTY_TYPE'])->Fetch();// тип дня: выходной или перенесённый рабочий, остальное считается по неделеХранение календаря в данных - главное решение этой задачи. Каждый декабрь редакция вносит новые праздники сама, и код при этом не трогают вовсе.
Считаем дату через несколько рабочих дней:
function addWorkDays(\Bitrix\Main\Type\DateTime $from, int $days, array $calendar): \Bitrix\Main\Type\DateTime{ $date = clone $from; while ($days > 0) { $date->add('+1 day'); $key = $date->format('Y-m-d'); $isWorkDay = $calendar[$key] ?? !in_array((int)$date->format('N'), [6, 7], true); if ($isWorkDay) { $days--; } // выходные и праздники не уменьшают счётчик } return $date;}Перебор по дням понятнее любой формулы и не ломается на переносах. Скорость здесь не важна: речь о десятке итераций, а не о миллионе.
Учитываем время отсечки:
$now = new \Bitrix\Main\Type\DateTime();$cutoff = (new \Bitrix\Main\Type\DateTime())->setTime(16, 0);$start = $now > $cutoff ? $now->add('+1 day') : $now; // после отсечки считаем со следующего дня$delivery = addWorkDays($start, 2, $calendar);Отсечку задают настройкой, а не константой в коде. Склад меняет её на время распродаж, и правка настройки не должна превращаться в выкладку новой версии сайта.
Показываем дату понятно:
printf("Доставим %s\n", \Bitrix\Main\Type\Date::createFromTimestamp($delivery->getTimestamp()) ->format('j F, D'));// «Доставим 3 сентября, ср» понятнее, чем «срок 2 рабочих дня»Выводим график работы на витрине:
$schedule = ['пн-пт' => '9:00-19:00', 'сб' => '10:00-16:00', 'вс' => 'выходной'];foreach ($schedule as $days => $hours) { printf("%-6s %s\n", $days, $hours); }// график хранят рядом с пунктом выдачи, а не в вёрстке шаблонаГрафик работы держат рядом с точкой: у каждого пункта выдачи он свой. Общий график в шаблоне живёт ровно до открытия второго пункта с другими часами.
Типичные проблемы
Срок доставки попадает на праздники.
Календарь праздников не заведён вовсе, а расчёт учитывает только субботу и воскресенье. Особые дни года держат отдельными данными и обновляют их каждый новый год.
Вечерние заказы обещают доставку на завтра.
В расчёте не учтено время отсечки приёма заказов на самом складе магазина. Отсечку задают отдельной настройкой и сдвигают старт расчёта на следующий рабочий день.
Дата на витрине отличается от даты в письме.
Расчёт выполняется в двух разных местах проекта и совершенно разным кодом. Расчёт держат в одной общей функции проекта и зовут её из всех мест.
У покупателя из другого часового пояса дата на день раньше.
Время считается в часовом поясе сервера, без всякого учёта пояса самого покупателя. Пояс учитывают только при выводе, а хранят время в едином поясе проекта.
После переноса рабочей субботы расчёт неверен.
Календарь хранит только выходные дни и совсем не умеет помечать рабочие субботы. У особого дня календаря заводят свой тип: выходной или перенесённый рабочий день.
Частые вопросы
Где хранить календарь праздников?
В инфоблоке или своей таблице с типом дня. Главное, чтобы список правила редакция без разработчика: праздники меняются ежегодно.
Нужен ли отдельный календарь на каждый склад?
Если графики отличаются - да, и это обычная ситуация для сети пунктов выдачи. Общий календарь праздников при этом остаётся один.
Как показать срок в карточке товара?
Считать дату при выводе и кэшировать её на несколько часов. Пересчитывать на каждый показ незачем: дата меняется раз в сутки.
Что делать с часовыми поясами?
Хранить время в едином поясе проекта, а выводить в поясе посетителя. Смешение поясов даёт расхождение ровно на один день у дальних регионов.
Стоит ли обещать точную дату?
Да, но с оговоркой про службу доставки: покупателю понятнее дата, чем число дней. Обещание без оговорки превращается в жалобу при первой задержке.
Смежное
-
Даты в проекте - оглавление подтемы
-
Даты в коде: объект, часовой пояс, фильтр по периоду - работа с датами в коде
-
Вывод дат на витрине: падежи, форматы сайта, относительное время - как показывать дату посетителю
-
Дата сохранилась не та: разбор причин - когда дата уезжает на день
-
Самовывоз и пункты выдачи: склады, список на оформлении, заказ - графики работы точек
-
Своя служба доставки с внешним API: регистрация, расчёт, статусы - расчёт срока службой доставки
-
Ядро D7 - устройство ядра целиком
-
Запись на услугу: слоты, занятость, защита от двойной брони - график работы в онлайн-записи