Матвей Земсков

Заметки веб-мастера

Понедельник, 03 мая 2021 13:27

Битрикс24 – организуем контроль за крайними сроками в задачах

Оцените материал
(212 голосов)

У одного из наших клиентов возникла потребность реализовать контроль за крайними сроками в задачах. Если рассмотреть требования задачи более детально, они заключались в следующем:

  • при создании задачи нужно сохранять первоначальный крайний срок
  • далее в случае переноса крайнего срока – этот факт должен фиксироваться, а именно вычисляться количество переносов крайнего срока
  • нужно вычислять, сохранять и показывать в задаче количество рабочих дней от первоначального крайнего срока до текущего

Должна быть возможность отключить отображение указанных полей в списке задач, а также использовать значения этих полей в фильтре задачам.

Итак, на основе списка требований нам нужно сохранять перечисленные данные в задаче. Для этого подойдут пользовательские поля. Они должны отображаться в списке. Это, так же как и возможность отключить отображение, является стандартным функционалом и проблем здесь возникнуть не должно.

У клиента есть ряд особенностей, которые нужно учесть при решении задачи:

  • На портале существуют задачи без крайнего срока
  • На портале много регулярных задач – эти задачи создаются на основе запуска агентов (на хитах или по cron)
  • Пользователи активно используют планировщик задач (расположенный в блоке управления рабочим днем)

Мне эта задача показалась интересной, поэтому я решил описать процесс решения и особенности с которыми столкнулся при этом. Возможно, кто-то из читателей захочет реализовать у себя на портале что-то подобное.

Задача решается с помощью обработчиков событий OnTaskAdd и OnTaskUpdate, но перед тем, как приступить к их разработке, нужно провести подготовительные процедуры.

Наша задача связана с вычислением дат (количества рабочих дней), поэтому нужно сохранить где-то все нерабочие и праздничные даты года. По идее можно воспользоваться API модуля «Календарь» для их получения из настроек. Это было бы правильно, но судя по событиям 2020 года, когда было большое количество незапланированных нерабочих дней (не учтённых в настройках календаря), лучше использовать альтернативный вариант. Я решил создать массив для хранения дат по годам и разместил его в файле settings.php. У этого подхода есть свой недостаток – в начале каждого года нужно добавлять даты в массив, но если за этим следить, то ничего страшного нет.

PHP

/*массив нерабочих дней по годам*/
$arHolidays = Array(
'2020' => Array(/*нерабочие дни за 2020 год*/),
'2021' => Array('2021-01-01', '2021-01-04', '2021-01-05', '2021-01-06', '2021-01-07', '2021-01-08', '2021-02-23', '2021-03-08', '2021-05-03', '2021-05-04', '2021-05-05', '2021-05-06', '2021-05-07', '2021-05-10',	'2021-06-14', '2021-02-23', '2021-11-04', '2021-11-05'));

Массив с датами по годам – это, конечно, хорошо, но еще как-то нужно вычислять количество рабочих дней при переносе крайних сроков. И снова воспользуемся сторонним решением, а именно функцией getWorkingDays, которая будет выполнять эту работу. По мимо этой функции нам понадобятся и другие вспомогательные функции для работы с датами. Поэтому я объединил их все в класс TasksHelper и разместил его в файле tasks.php.

PHP

class TasksHelper{
	/*Вычисляем количество рабочих дней в указанном диапазоне*/
	public static function getWorkingDays($startDate, $endDate, $holidays){
	   $endDate = strtotime($endDate);
	   $startDate = strtotime($startDate);
	   $days = ($endDate - $startDate) / 86400 + 1;
	   $no_full_weeks = floor($days / 7);
	   $no_remaining_days = fmod($days, 7);
	   $the_first_day_of_week = date("N", $startDate);
	   $the_last_day_of_week = date("N", $endDate);

	   if ($the_first_day_of_week <= $the_last_day_of_week) {
		   if ($the_first_day_of_week <= 6 && 6 <= $the_last_day_of_week) $no_remaining_days--;
		   if ($the_first_day_of_week <= 7 && 7 <= $the_last_day_of_week) $no_remaining_days--;
	   } else {
		   if ($the_first_day_of_week == 7) {
			   $no_remaining_days--;
			   if ($the_last_day_of_week == 6) {
				   $no_remaining_days--;
			   }
		   } else {
			   $no_remaining_days -= 2;
		   }
	   }
	   $workingDays = $no_full_weeks * 5;

	   if ($no_remaining_days > 0)
	   {
		   $workingDays += $no_remaining_days;
	   }
	   foreach($holidays as $holiday){
		   $time_stamp=strtotime($holiday);
		   if ($startDate <= $time_stamp && $time_stamp <= $endDate && date("N",$time_stamp) != 6 && date("N",$time_stamp) != 7)
			   $workingDays--;
	   }
	   return $workingDays;
}

Обычно обработчики событий в Битриксе хранятся в файле init.php, расположенном в папке php_interface. Правильным решением будет разместить файлы settings.php и tasks.php в папке php_interface/include/tasks. После этого не забудем подключить указанные файлы в init.php.

Далее переходим к созданию пользовательских полей. Я, например, добавил следующие:

  • UF_PRIMARY_DL – поле для хранения первоначального крайнего срока (тип – дата)
  • UF_POSTPONEMENTS_NUM – поле для хранения количества переносов крайних сроков (тип - число)
  • UF_WORKDAYS_NUM – поле для хранения количества рабочих дней от первоначального крайнего срока до текущего (тип - число)

Делал это со стороны административной части портала, так как при этом есть возможность дать им свои имена, а не те которые сгенерирует Битрикс.

Приступаем к основной части задачи – написанию кода функций-обработчиков событий, которые мы разместим непосредственно в файле init.php.

Начнем с обработчика события OnTaskAdd.

PHP

EventManager::getInstance()->addEventHandler('tasks', 'OnTaskAdd', 'OnTaskAddHandler');

Им является функция OnTaskAddHandler. В качестве аргументов ей передаются: идентификатор задачи $idTask и массив полей $arTask. Код функции несложный, но важно учесть следующие моменты:

  • Созданная задача может быть без крайнего срока
  • При добавлении задачи из планировщика отсутствует элемент массива $arTask['DEADLINE']

В первом случае мы выходим из обработчика:

PHP

if(array_key_exists('DEADLINE', $arTask) && empty($arTask['DEADLINE'])){
	return true;
}

Во втором формируем DEADLINE из элемента массива $arTask['CREATED_DATE']. На мой взгляд это правильный подход, так как подразумевается, что задачи которые создаются в планировщике должны быть выполнены в тот же день.

PHP

if(!isset($arTask['DEADLINE'])){
	$cd = explode(' ', $arTask['CREATED_DATE'])[0]; //22.01.2021 13:49:43 => 22.01.2021
	$arTask['DEADLINE'] = $cd.' 19:00:00'; //22.01.2021 => 22.01.2021 19:00:00
}

Далее подключаем модули sale и highloadblock. Так как основной задачей обработчика является запись первоначального крайнего срока в поле UF_PRIMARY_DL задачи, я приведу фрагмент кода, который выполняет это действие:

PHP

$obTask = new CTasks;
$arTaskFields = Array('UF_PRIMARY_DL' => $arTask['DEADLINE']);
$tskres = $obTask->Update($arTask['ID'], $arTaskFields);

Если все проходит без ошибок, то нужно сформировать сообщение об этом для последующей записи в лог. Сообщение об ошибке также формируем. В итоге записываем в файл лога с помощью функции file_put_contents(). В коде мы используем метод CTasks::Update(), а это как известно запускает обработчик события OnTaskUpdate. Поэтому при создании задачи срабатывают оба наших обработчика. Это также нужно запомнить.

По желанию вы можете сохранять информацию об изменениях в задачах в HL-блок для того, чтобы потом сформировать на основе нее какой-нибудь отчет.

PHP

// выбираем информацию о сущности из базы данных
$arHLBlock = Bitrix\Highloadblock\HighloadBlockTable::getById(TASK_DEADLINES_HL_ID)->fetch();
//затем инициализируем класс сущности
$obEntity = Bitrix\Highloadblock\HighloadBlockTable::compileEntity($arHLBlock);
$strEntityDataClass = $obEntity->getDataClass();
$arTaskItemArray =  Array();
$arTaskItemArray['UF_TASK_ID'] = $arTask['ID'];
$arTaskItemArray['UF_TASK_TITLE'] = $arTask['TITLE'];
$arTaskItemArray['UF_CUR_DEADLINE'] = $arTask['DEADLINE'];	
//записываем данные в элемент HL-блока
$obResult = $strEntityDataClass::add($arTaskItemArray);
$hlres = $obResult->isSuccess();

На этом код обработчика OnTaskAddHandler заканчивается.

Выше я писал, что на портале создается много регулярных задач и что нужно это учесть. Обращаю внимание, обработчики событий при этом срабатывают и наш код будет выполняться. Главное, чтобы был корректно настроен запуск агентов, если они выполняются на cron’е.

Переходим к обработчику события OnTaskUpdate.

PHP

EventManager::getInstance()->addEventHandler('tasks', 'OnTaskUpdate', 'OnTaskUpdateHandler');

В качестве аргументов ей передаются: идентификатор задачи $id, массив полей $arFields и копию массива $arTaskCopy.

В самом начале также подключаем модули sale и highloadblock. Обращаю внимание на то, что при обновлении может не существовать $arFields['DEADLINE'] и $arFields['UF_PRIMARY_DL'], зато есть $arFields['META:PREV_FIELDS']['DEADLINE'] и $arFields['META:PREV_FIELDS']['UF_PRIMARY_DL']. Массив $arFields['META:PREV_FIELDS'] в данном случае является очень полезным, потому что в нем сохранены значения полей, которые были до изменения задачи.

В самом начале функции нужно удостовериться, что выполняется условие:

PHP

if(is_array($arFields['META:PREV_FIELDS']) && count($arFields['META:PREV_FIELDS']) > 0){ }

Далее снова проверка задачи – есть ли у нее крайний срок. Если его нет, то обработчик завершается.

Если до добавления обработчика на портале уже существовали задачи, а такое может быть очень вероятно, в коде нужно добавить одну условную конструкцию. Она заключается в том, что нужно проверить заполнено ли поле Первоначальный крайний срок (UF_PRIMARY_DL). Если UF_PRIMARY_DL не заполнено, то заполняем его текущим крайним сроком и обнуляем счетчики (значения полей UF_POSTPONEMENTS_NUM и UF_WORKDAYS_NUM).

PHP

if(array_key_exists('UF_PRIMARY_DL', $arCurTsk) && empty($arCurTsk['UF_PRIMARY_DL'])){
    // если поле не заполнено, то заполняем его текущим крайним сроком и обнулим счетчики
   $obCurTask = new CTasks;
   $arCurTaskFields = Array(
	'UF_PRIMARY_DL' => $arCurTsk['DEADLINE'],
	'UF_POSTPONEMENTS_NUM' => "0", 
	'UF_WORKDAYS_NUM' => "0"
   );
  $tskres = $obCurTask->Update($arCurTsk['ID'], $arCurTaskFields);
  $arFields['META:PREV_FIELDS']['UF_PRIMARY_DL'] = $arCurTsk['DEADLINE'];			
}

Далее: если условие

PHP

if(isset($arFields['DEADLINE']) && $arFields['DEADLINE'] !== $arFields['META:PREV_FIELDS']['DEADLINE']){}

вернет true, значит изменился именно крайний срок. В этом случае выполняем основные задачи обработчика.

  • Преобразуем даты в определенный формат, который подходит для сохранения их в HL-блоке. Для этого используем метод TasksHelper::tranformDate()
  • Объединяем все праздники в 1 массив и вычисляем кол-во рабочих дней. Используем методы TasksHelper::getAllHolidaysList() и TasksHelper::getWorkingDays().Увеличиваем кол-во переносов крайнего срока на 1.
  • Обновляем данные в задаче

Вычисленное количество рабочих дней и актуальное количество переносов крайних сроков сохраняем в переменных $workDaysQuant и $ppNum соответственно. Затем используем их в массиве для обновления задачи.

PHP

// обновляем данные в задаче
$obTask = new CTasks;
$arTaskFields = Array('UF_POSTPONEMENTS_NUM' => $ppNum, 'UF_WORKDAYS_NUM' => $workDaysQuant);
$tskres = $obTask->Update($arFields['ID'], $arTaskFields);

Вы можете записать информацию об обновлении крайнего срока в HL-блок. Как это было в случае с обработчиком OnTaskAdd. Код будет выглядеть также как и приведенный выше, поэтому я не буду повторяться.

Обращу внимание читателя на то, что в конце файла init.php не нужно ставить закрывающий тег PHP ?>. Это может привести к некорректной работе кода обработчиков событий. В частности это касается тех случаев, когда регулярные задачи запускаются по cron, а также ошибки могут возникнут и в других случаях.

На этом статья с описанием создания инструмента по контролю за крайними сроками в задачах подходит к концу. В ней мы рассмотрели код 2 обработчиков событий, настройки и вспомогательные функции, а также способ хранения информации о крайних сроках задач в HL-блоке.

Надеюсь, статья была полезна для вас.

Скачать архив с кодом из статьи

Прочитано 18006 раз
Мои услуги

Предлагаю следующие услуги:

  • Верстка шаблона сайта из дизайн-макета для CMS «1С-Битрикс Управление сайтом» и CMS “Joomla”
  • Создание форм различной сложности (обратная связь, анкеты и тп) для указанных CMS
  • Настройка и кастомизация компонентов и модулей для указанных CMS
  • Доработка модулей и компонентов для указанных CMS, добавление нестандартного функционала
  • Разработка лендингов (landing-pages)

По все вопросам обращайтесь через форму обратной связи

Скачать

Предлагаю вашему вниманию:

Наверх