Your IP : 3.136.37.122


Current Path : /home/bitrix/ext_www/crm.klimatlend.ua/bitrix/components/bitrix/tasks.kanban/
Upload File :
Current File : /home/bitrix/ext_www/crm.klimatlend.ua/bitrix/components/bitrix/tasks.kanban/class.php

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
	die();
}

use \Bitrix\Main\Localization\Loc;
use \Bitrix\Main\Config\Option;
use \Bitrix\Main\Type\DateTime;
use \Bitrix\Main\Entity\Query;
use \Bitrix\Main\Entity\Query\Join;
use \Bitrix\Main\Entity\ExpressionField;
use \Bitrix\Main\Entity\ReferenceField;
use \Bitrix\Main\Error;
use \Bitrix\Main\Loader;
use \Bitrix\Main\Application;

use Bitrix\Socialnetwork\Item\Workgroup;
use \Bitrix\Tasks\Grid\Row\Content\Date;
use \Bitrix\Tasks\Kanban\StagesTable;
use \Bitrix\Tasks\Kanban\TaskStageTable;
use \Bitrix\Tasks\Kanban\TimeLineTable;
use \Bitrix\Tasks\ProjectsTable;
use \Bitrix\Tasks\Helper\Filter;
use \Bitrix\Tasks\Internals\Task;
use \Bitrix\Tasks\Internals\UserOption;
use \Bitrix\Tasks\Integration\Disk\Connector\Task as ConnectorTask;
use \Bitrix\Tasks\Access\ActionDictionary;

use \Bitrix\Tasks\Integration\SocialNetwork;
use Bitrix\Tasks\Scrum\Internal\ItemTable;
use Bitrix\Tasks\Scrum\Service\ItemService;

class TasksKanbanComponent extends \CBitrixComponent
{
	const TASK_TYPE_USER = 'user';
	const TASK_TYPE_GROUP = 'group';

	const USER_DEPARTMENT_CODE = 'UF_DEPARTMENT';
	const USER_WEBDAV_CODE = 'UF_TASK_WEBDAV_FILES';
	const USER_CRM_CODE = 'UF_USER_CRM_ENTITY';

	const USER_TYPE_MAIL = 'email';

	const DEF_PAGE_SIZE = 20;

	const SESS_DATA_KEY = 'KANBAN_DATA';

	protected $filterInstance;

	protected $userId = 0;
	protected $timeOffset = 0;
	protected $taskType = '';
	protected $application = null;
	protected $select = array();
	protected $filter = array();
	protected $order = array();
	protected $listParams = array();
	protected $errors = array();
	protected $avatarSize = array('width' => 38, 'height' => 38);
	protected $previewSize = array('width' => 1000, 'height' => 1000);

	/**
	 * Init class' vars, check conditions.
	 * @return bool
	 */
	protected function init()
	{
		static $init = null;

		if ($init !== null)
		{
			return $init;
		}

		$init = true;
		$params =& $this->arParams;
		$result =& $this->arResult;
		$this->application = $GLOBALS['APPLICATION'];
		$this->timeOffset = \CTimeZone::GetOffset();

		Loc::loadMessages($this->getFile());

		// check fatal errors
		if ($init && !$this->application)
		{
			$this->addError('TASK_LIST_TASK_NOT_INSTALLED');
			$init = false;
		}
		if ($init && !Loader::includeModule('tasks'))
		{
			$this->addError('TASK_LIST_TASK_NOT_INSTALLED');
			$init = false;
		}
		if ($init && !\CBXFeatures::IsFeatureEnabled('Tasks'))
		{
			$this->addError('TASK_LIST_NOT_AVAILABLE_IN_THIS_EDITION');
			$init = false;
		}
		if ($init && ($this->userId = (int) \Bitrix\Tasks\Util\User::getId()) <= 0)
		{
			$this->addError('TASK_LIST_ACCESS_DENIED');
			$init = false;
		}

		// init vars or exit on fatal
		if (!$init)
		{
			return $init;
		}
		else
		{
			$this->initVars();
		}

		// get data of user or group
		if ($this->taskType == static::TASK_TYPE_USER)
		{
			$result['USER'] = \CUser::GetByID($params['USER_ID'])->fetch();
			if (!$result['USER'])
			{
				$this->addError('TASK_LIST_USER_NOT_FOUND');
				$init = false;
			}
		}
		elseif ($this->taskType == static::TASK_TYPE_GROUP)
		{
			$result['GROUP'] = \CSocNetGroup::GetByID($params['GROUP_ID']);
			if (!$result['GROUP'])
			{
				$this->addError('TASK_LIST_GROUP_NOT_FOUND');
				$init = false;
			}
			// check sonet perms
			elseif (!$this->canReadGroupTasks($params['GROUP_ID']))
			{
				$this->addError('TASK_LIST_ACCESS_TO_GROUP_DENIED');
				$init = false;
			}
		}

		return $init;
	}

	/**
	 * Set some vars.
	 */
	protected function initVars()
	{
		$params =& $this->arParams;

		// set from request
		$request = $this->request('params');
		$allowFromRequest = [
			'USER_ID', 'GROUP_ID', 'GROUP_ID_FORCED',
			'PERSONAL', 'TIMELINE_MODE', 'SPRINT_ID'
		];
		foreach ($allowFromRequest as $code)
		{
			if (isset($request[$code]))
			{
				$params[$code] = $request[$code];
			}
		}

		// vars
		$params['SONET_LOAD'] = $sonetOn = Loader::includeModule('socialnetwork');
		$params['GROUP_ID_FORCED'] = isset($params['GROUP_ID_FORCED']) ? $params['GROUP_ID_FORCED'] : false;
		$params['IS_AJAX'] = isset($params['IS_AJAX']) && $params['IS_AJAX'] == 'Y' ? 'Y' : 'N';
		$params['SET_TITLE'] = isset($params['SET_TITLE']) && $params['SET_TITLE'] == 'Y' ? 'Y' : 'N';
		$params['PERSONAL'] = isset($params['PERSONAL']) && $params['PERSONAL'] == 'Y' ? 'Y' : 'N';
		$params['TIMELINE_MODE'] = isset($params['TIMELINE_MODE']) && $params['TIMELINE_MODE'] == 'Y' ? 'Y' : 'N';
		$params['USER_ID'] = !isset($params['USER_ID']) || intval($params['USER_ID']) <= 0 ? $this->userId : intval($params['USER_ID']);
		$params['GROUP_ID'] = !isset($params['GROUP_ID']) || !$sonetOn ? 0 : max(0, intval($params['GROUP_ID']));
		$params['SPRINT_ID'] = !isset($params['SPRINT_ID']) ? -1 : intval($params['SPRINT_ID']);
		$params['~NAME_TEMPLATE'] = !isset($params['~NAME_TEMPLATE'])
									? \CSite::GetNameFormat(false)
									: str_replace(array('#NOBR#', '#/NOBR#'), array('', ''), trim($params['~NAME_TEMPLATE']));

		$group = Bitrix\Socialnetwork\Item\Workgroup::getById($params['GROUP_ID']);
		if ($group && $group->isScrumProject())
		{
			if (isset($params['SPRINT_ID']) && $params['SPRINT_ID'] >= 0)
			{
				StagesTable::setWorkMode(StagesTable::WORK_MODE_ACTIVE_SPRINT);
				$params['SPRINT_SELECTED'] = 'Y';
			}
			else
			{
				$params['SPRINT_ID'] = 0;
				$params['SPRINT_SELECTED'] = 'N';
			}
		}
		else
		{
			if (isset($params['SPRINT_ID']) && $params['SPRINT_ID'] >= 0)
			{
				$sprint = \Bitrix\Tasks\Kanban\SprintTable::getSprint(
					$params['GROUP_ID'],
					$params['SPRINT_ID']
				);
				if ($sprint)
				{
					$params['SPRINT_ID'] = $sprint['ID'];
				}
				else
				{
					$params['SPRINT_ID'] = 0;
				}
				StagesTable::setWorkMode(StagesTable::WORK_MODE_SPRINT);
				$params['SPRINT_SELECTED'] = 'Y';
			}
		}

		// force set last user group
		if ($params['PERSONAL'] == 'Y')
		{
			if ($params['GROUP_ID'] > 0)
			{
				$params['GROUP_ID_FORCED'] = true;
			}
			$this->arParams['STAGES_ENTITY_ID'] = $params['USER_ID'];
			if ($this->arParams['TIMELINE_MODE'] == 'Y')
			{
				StagesTable::setWorkMode(StagesTable::WORK_MODE_TIMELINE);
			}
			else
			{
				StagesTable::setWorkMode(StagesTable::WORK_MODE_USER);
			}
		}
		else if ($params['SPRINT_ID'] >= 0)
		{
			$this->arParams['STAGES_ENTITY_ID'] = $params['SPRINT_ID'];
		}
		else
		{
			if ($params['GROUP_ID'] == 0 && $sonetOn)
			{
				$params['GROUP_ID'] = $this->getLastGroupId();
				$params['GROUP_ID_FORCED'] = true;
			}
			else
			{
				if (isset($params['GROUP_ID_FORCED']))
				{
					$params['GROUP_ID_FORCED'] = (boolean)$params['GROUP_ID_FORCED'];
				}
			}
			$this->arParams['STAGES_ENTITY_ID'] = $params['GROUP_ID'];
			StagesTable::setWorkMode(StagesTable::WORK_MODE_GROUP);
		}

		// remeber view
//		Filter\Task::getListStateInstance()->setViewMode(
//			$params['PERSONAL'] == 'Y'
//			? \CTaskListState::VIEW_MODE_PLAN
//			: \CTaskListState::VIEW_MODE_KANBAN
//		);

		// preview sizes
		if (isset($params['PREVIEW_WIDTH']) && $params['PREVIEW_WIDTH'] > 0)
		{
			$this->previewSize['width'] = $params['PREVIEW_WIDTH'];
		}
		if (isset($params['PREVIEW_HEIGHT']) && $params['PREVIEW_HEIGHT'] > 0)
		{
			$this->previewSize['height'] = $params['PREVIEW_HEIGHT'];
		}

		// sonet group
		if ($params['GROUP_ID'] > 0)
		{
			$this->taskType = static::TASK_TYPE_GROUP;
		}
		else
		{
			$this->taskType = static::TASK_TYPE_USER;
		}

		//		if (
		//			$this->taskType == static::TASK_TYPE_GROUP /*&&
		//			!$params['GROUP_ID_FORCED']*/
		//		)
		//		{
		//			Filter\Task::setGroupId($params['GROUP_ID']);
		//		}
		//		else
		//		{
		//			Filter\Task::setUserId($params['USER_ID']);
		//		}

		$this->filterInstance = Filter::getInstance($this->arParams["USER_ID"], $this->arParams["GROUP_ID"]);

		$this->arParams['DEFAULT_ROLEID'] = $this->filterInstance->getDefaultRoleId();
		$this->arResult['DEFAULT_PRESET_KEY'] = $this->filterInstance->getDefaultPresetKey();


		// navigation
		if (!isset($params['ITEMS_COUNT']))
		{
			$params['ITEMS_COUNT'] = static::DEF_PAGE_SIZE;
		}
		// temporary we set DEF_PAGE_SIZE always
		$params['ITEMS_COUNT'] = static::DEF_PAGE_SIZE;
		if (!isset($params['ITEMS_PAGE']))
		{
			$params['ITEMS_PAGE'] = 1;
		}

		// external filter
		if (isset($params['FILTER']) && is_array($params['FILTER']))
		{
			$this->filter = $params['FILTER'];
		}

		// pathes
		$params['~PATH_TO_USER_PROFILE'] = !isset($params['~PATH_TO_USER_PROFILE']) || trim($params['~PATH_TO_USER_PROFILE']) == ''
									? $this->getPage() . '?page=user_profile&user_id=#user_id#'
									: trim($params['~PATH_TO_USER_PROFILE']);
		$params['~PATH_TO_USER_TASKS'] = !isset($params['~PATH_TO_USER_TASKS']) || trim($params['~PATH_TO_USER_TASKS']) == ''
									? Option::get('tasks', 'paths_task_user', null, SITE_ID)
									: trim($params['~PATH_TO_USER_TASKS']);
		$params['~PATH_TO_USER_TASKS_TASK'] = !isset($params['~PATH_TO_USER_TASKS_TASK']) || trim($params['~PATH_TO_USER_TASKS_TASK']) == ''
									? Option::get('tasks', 'paths_task_user_action', null, SITE_ID)
									: trim($params['~PATH_TO_USER_TASKS_TASK']);
		$params['~PATH_TO_USER_TASKS_TEMPLATES'] = !isset($params['~PATH_TO_USER_TASKS_TEMPLATES']) || trim($params['~PATH_TO_USER_TASKS_TEMPLATES']) == ''
									? $this->getPage() . '?page=user_templates&user_id=#user_id#'
									: trim($params['~PATH_TO_USER_TASKS_TEMPLATES']);
		$params['~PATH_TO_GROUP_TASKS'] = !isset($params['~PATH_TO_GROUP_TASKS']) || trim($params['~PATH_TO_GROUP_TASKS']) == ''
									? Option::get('tasks', 'paths_task_group', null, SITE_ID)
									: trim($params['~PATH_TO_GROUP_TASKS']);
		$params['~PATH_TO_GROUP_TASKS_TASK'] = !isset($params['~PATH_TO_GROUP_TASKS_TASK']) || trim($params['~PATH_TO_GROUP_TASKS_TASK']) == ''
									? Option::get('tasks', 'paths_task_group_action', null, SITE_ID)
									: trim($params['~PATH_TO_GROUP_TASKS_TASK']);

		// replace for id
		if ($this->taskType == static::TASK_TYPE_USER || $params['GROUP_ID_FORCED'])
		{
			$params['~PATH_TO_TASKS'] = str_replace('#user_id#', $params['USER_ID'], $params['~PATH_TO_USER_TASKS']);
			$params['~PATH_TO_TASKS_TASK'] = str_replace('#user_id#', $params['USER_ID'], $params['~PATH_TO_USER_TASKS_TASK']);
		}
		elseif ($this->taskType == static::TASK_TYPE_GROUP)
		{
			$params['~PATH_TO_TASKS'] = str_replace('#group_id#', $params['GROUP_ID'], $params['~PATH_TO_GROUP_TASKS']);
			$params['~PATH_TO_TASKS_TASK'] = str_replace('#group_id#', $params['GROUP_ID'], $params['~PATH_TO_GROUP_TASKS_TASK']);
		}
		$params['~PATH_TO_TEMPLATES'] = str_replace('#user_id#', $this->userId, $params['~PATH_TO_USER_TASKS_TEMPLATES']);
	}

	/**
	 * Returns all members of tasks.
	 * @param array $taskData Task data (single item from $this->getData).
	 * @return array
	 */
	protected function getMembersTask(array $taskData): array
	{
		$members = [];

		// author and responsible
		$members[] = $taskData['data']['author']['id'];
		$members[] = $taskData['data']['responsible']['id'];

		// other task members
		$res = \CTaskMembers::getList(
			[],
			['TASK_ID' => $taskData['id']]
		);
		while ($row = $res->fetch())
		{
			$members[] = $row['USER_ID'];
		}

		// group members
		if ($this->arParams['GROUP_ID'])
		{
			$groupMembers = SocialNetwork\User::getUsersCanPerformOperation(
				$this->arParams['GROUP_ID'],
				'view_all'
			);
			$members = array_unique(array_merge($members, $groupMembers));
		}

		return $members;
	}

	/**
	 * Check access to group tasks for current user.
	 * @param int $groupId Id of group.
	 * @return boolean
	 */
	protected function canReadGroupTasks($groupId)
	{
		return \Bitrix\Tasks\Integration\SocialNetwork\Group::canReadGroupTasks($this->userId, $groupId);
	}

	/**
	 * Check access to sort tasks for current user.
	 * @return boolean
	 */
	protected function canSortTasks()
	{
		static $access = null;

		if ($access !== null)
		{
			return $access;
		}

		$groupId = $this->arParams['GROUP_ID'];

		if ($this->arParams['PERSONAL'] == 'Y')
		{
			$access = $this->isAdmin();
		}
		elseif ($groupId > 0)
		{
			$featurePerms = \CSocNetFeaturesPerms::CurrentUserCanPerformOperation(SONET_ENTITY_GROUP, array($groupId), 'tasks', 'sort');
			$access = is_array($featurePerms) && isset($featurePerms[$groupId]) && $featurePerms[$groupId];
		}
		else
		{
			$access = $this->isAdmin();
		}

		return $access;
	}

	/**
	 * Check access to create tasks for current user.
	 * @return boolean
	 */
	protected function canCreateTasks()
	{
		if (!\Bitrix\Tasks\Util\Restriction::canManageTask())
		{
			return false;
		}
		if ($this->taskType == static::TASK_TYPE_GROUP)
		{
			$groupId = $this->arParams['GROUP_ID'];
			$featurePerms = \CSocNetFeaturesPerms::CurrentUserCanPerformOperation(SONET_ENTITY_GROUP, array($groupId), 'tasks', 'create_tasks');
			return is_array($featurePerms) && isset($featurePerms[$groupId]) && $featurePerms[$groupId];
		}
		return true;
	}

	/**
	 * Get admins and group moderators.
	 * @return array
	 */
	protected function getAdmins()
	{
		$users = array();
		// site admins
		$res = \Bitrix\Main\UserGroupTable::getList(array(
			'filter' => array(
				'GROUP_ID' => 1
			)
		));
		while ($row = $res->fetch())
		{
			$users[] = $row['USER_ID'];
		}
		// group admins
		if ($this->arParams['SONET_LOAD'])
		{
			$res = \CSocNetUserToGroup::GetList(
				array(),
				array(
					'GROUP_ID' => $this->arParams['GROUP_ID'],
					'ROLE' => array(
						SONET_ROLES_OWNER,
						SONET_ROLES_MODERATOR
					),
					'USER_ACTIVE' => 'Y',
					'GROUP_ACTIVE' => 'Y'
				),
				false, false,
				array(
					'USER_ID'
				)
			);
			while ($row = $res->fetch())
			{
				$users[] = $row['USER_ID'];
			}
		}

		if (!empty($users))
		{
			$res = \CUser::GetList(
					($by = 'timestamp_x'),
					($order = 'desc'),
					array(
						'ID' => implode(' | ', $users),
						'ACTIVE' => 'Y',
						'!ID' => $this->userId
					)
			);
			$users = array();
			while ($row = $res->fetch())
			{
				if ($row['PERSONAL_PHOTO'])
				{
					$row['PERSONAL_PHOTO'] = \CFile::ResizeImageGet(
						$row['PERSONAL_PHOTO'],
						$this->avatarSize,
						BX_RESIZE_IMAGE_EXACT
					);
					if ($row['PERSONAL_PHOTO'])
					{
						$row['PERSONAL_PHOTO'] = $row['PERSONAL_PHOTO']['src'];
					}
				}
				$users[$row['ID']] = array(
					'id' => $row['ID'],
					'name' => \CUser::FormatName(
						$this->arParams['~NAME_TEMPLATE'],
						$row, true, false
					),
					'img' => $row['PERSONAL_PHOTO']
				);
			}
		}

		return $users;
	}

	/**
	 * Get last by activity group id.
	 * @return int
	 */
	protected function getLastGroupId()
	{
		$lastGroupId = 0;

		if (!$this->arParams['SONET_LOAD'])
		{
			return $lastGroupId;
		}

		return \Bitrix\Tasks\Integration\SocialNetwork\Group::getLastViewedProject($this->userId);
	}

	/**
	 * Is debug hit?
	 * @return boolean
	 */
	protected function isDebug()
	{
		return $this->request('debug') == 'Y';
	}

	/**
	 * Get request-var for http-hit.
	 * @param string $var Request-var code.
	 * @return mixed
	 */
	protected function request($var)
	{
		static $request = null;

		if ($request === null)
		{
			$context = Application::getInstance()->getContext();
			$request = $context->getRequest();
		}

		return $request->get($var);
	}

	/**
	 * Reload current page.
	 * @param array $deleteParams Params to delete from uri.
	 * @return void
	 */
	protected function reloadPage(array $deleteParams = array())
	{
		$context = Application::getInstance()->getContext();
		$uri = new \Bitrix\Main\Web\Uri($context->getRequest()->getRequestUri());
		\LocalRedirect($uri->deleteParams($deleteParams)->getUrl(), true);
	}

	/**
	 * Get current page.
	 * @return string
	 */
	protected function getPage()
	{
		static $page = null;
		if ($page === null)
		{
			$context = Application::getInstance()->getContext();
			$request = $context->getRequest();
			$uri = new \Bitrix\Main\Web\Uri($request->getRequestUri());
			$page = $uri->getPath();
		}
		return $page;
	}

	/**
	 * Get current URI.
	 * @return string
	 */
	protected function getURI()
	{
		static $uri = null;
		if ($uri === null)
		{
			$context = Application::getInstance()->getContext();
			$request = $context->getRequest();
			$uri = new \Bitrix\Main\Web\Uri($request->getRequestUri());
		}
		return $uri;
	}

	/**
	 * Get __FILE__.
	 * @return string
	 */
	protected function getFile()
	{
		return __FILE__;
	}

	/**
	 * Get current errors.
	 * @param bool $string Convert Errors to string.
	 * @return array
	 */
	protected function getErrors($string = true)
	{
		if ($string)
		{
			$errors = array();
			foreach ($this->errors as $error)
			{
				$errors[] = $error->getMessage();
			}
			return $errors;
		}
		else
		{
			return $this->errors;
		}
	}

	/**
	 * Add one more error.
	 * @param string $code Code of error (lang code).
	 * @return void
	 */
	protected function addError($code)
	{
		$message = Loc::getMessage($code);
		$this->errors[] = new Error($message != '' ? $message : $code, $code);
	}

	/**
	 * Copy error from other instance.
	 * @param array $errors Array of Error.
	 * @return void
	 */
	protected function copyError(array $errors)
	{
		foreach ($errors as $error)
		{
			$this->errors[] = new Error(
				$error->getMessage(),
				$error->getCode()
			);
		}
	}

	/**
	 * I am the owner of this list?
	 * @return bool
	 */
	protected function itsMyTasks()
	{
		return (
					$this->taskType == static::TASK_TYPE_USER &&
					$this->userId == $this->arParams['USER_ID']
				)
				||
				(
					$this->userId == $this->arParams['USER_ID'] &&
					$this->arParams['GROUP_ID_FORCED']
				);
	}

	/**
	 * Current user is admin for this Kanban?
	 * @return bool
	 */
	protected function isAdmin()
	{
		if ($this->arParams['PERSONAL'] == 'Y')
		{
			return $this->itsMyTasks();
		}

		if ($this->taskType == static::TASK_TYPE_GROUP)
		{
			$right = \CSocNetUserToGroup::GetUserRole($this->userId, $this->arParams['GROUP_ID']);
			if ($right == SONET_ROLES_OWNER || $right == SONET_ROLES_MODERATOR)
			{
				return true;
			}
		}

		return $GLOBALS['USER']->isAdmin() || \CTasksTools::IsPortalB24Admin();
	}

	/**
	 * Get select array.
	 * @return array
	 */
	protected function getSelect()
	{
		// by default
		$this->select[] = 'ID';
		$this->select = array_merge(
			$this->select,
			[
				'TITLE',
				'REAL_STATUS',
				'PRIORITY',
				'DEADLINE',
				'DATE_START',
				'CREATED_DATE',
				'CREATED_BY',
				'RESPONSIBLE_ID',
				'AUDITORS',
				'ACCOMPLICES',
				'ALLOW_CHANGE_DEADLINE',
				'ALLOW_TIME_TRACKING',
				'NEW_COMMENTS_COUNT',
				'TIME_SPENT_IN_LOGS',
				'TIME_ESTIMATE',
				'STAGE_ID',
				'SORTING',
				'IS_MUTED',
			]
		);

		$this->select = array_unique($this->select);

		return $this->select;
	}

	/**
	 * Add one item to the filter.
	 * @param string $key Key for select.
	 * @return void
	 */
	protected function addSelect($key)
	{
		$this->filter[] = $key;
	}

	/**
	 * Get filter array.
	 * @return array
	 */
	protected function getFilter()
	{
		static $filling = false;

		if ($filling)
		{
			return $this->filter;
		}
		else
		{
			$filling = true;
		}

		$params =& $this->arParams;
		$filter =& $this->filter;

		$uiFilter = $this->filterInstance->process();
		if (array_key_exists('GROUP_ID', $this->arParams) && (int)$this->arParams["GROUP_ID"] > 0)
		{
			$uiFilter['GROUP_ID'] = $this->arParams["GROUP_ID"];
			unset($uiFilter['MEMBER']);
		}

		$filter = array_merge($filter, $uiFilter);

		// by default
		if (!array_key_exists('CHECK_PERMISSIONS', $filter))
		{
			$filter['CHECK_PERMISSIONS'] = 'Y';
		}
		if (!array_key_exists('ZOMBIE', $filter))
		{
			$filter['ZOMBIE'] = 'N';
		}
		if ($params['PERSONAL'] != 'Y' || $params['GROUP_ID'] > 0)
		{
			$filter['GROUP_ID'] = $params['GROUP_ID'];
		}
		$filter['ONLY_ROOT_TASKS'] = 'N';

		return $filter;
	}

	/**
	 * Add one item to the filter.
	 * @param string $key Key for filter.
	 * @param string $value Value for filter.
	 * @return void
	 */
	protected function addFilter($key, $value)
	{
		$this->filter[$key] = $value;
	}

	/**
	 * Remove one item to the filter.
	 * @param string $key Key for filter.
	 * @return void
	 */
	protected function deleteFilter(string $key): void
	{
		if (isset($this->filter[$key]))
		{
			unset($this->filter[$key]);
		}
	}

	/**
	 * Set page id / page number.
	 * @param int $id Page id.
	 * @return void
	 */
	protected function setPageId($id)
	{
		$this->arParams['ITEMS_PAGE'] = $id;
	}

	/**
	 * Returns order array.
	 * @return array
	 */
	protected function getOrder()
	{
		if ($this->getNewTaskOrder() == 'actual')
		{
			$this->order = [
				'ACTIVITY_DATE' => 'desc',
				'ID' => 'asc'
			];
		}
		else
		{
			$this->order = [
				'SORTING' => 'ASC',
				'STATUS_COMPLETE' => 'ASC',
				'DEADLINE' => 'ASC,NULLS',
				'ID' => 'ASC'
			];
		}

		return $this->order;
	}

	/**
	 * Get nav and other list params.
	 * @return array
	 */
	protected function getListParams()
	{
		$this->listParams['NAV_PARAMS'] = array(
			'nPageSize' => $this->arParams['ITEMS_COUNT'],
			'iNumPage' => $this->arParams['ITEMS_PAGE'],
			'bDescPageNumbering' => false,
			'NavShowAll' => false,
			'bShowAll' => false
		);
		// personl sort
		if (
			$this->arParams['PERSONAL'] != 'Y' &&
			$this->taskType == static::TASK_TYPE_GROUP
		)
		{
			$this->listParams['SORTING_GROUP_ID'] = $this->arParams['GROUP_ID'];
		}
		return $this->listParams;
	}

	/**
	 * Make query like ORM.
	 * @param array $params Params with order, filter, nav, select keys.
	 * @param bolean $asIs Return result as is.
	 * @return array
	 */
	protected function getList(array $params, $asIs = false)
	{
		// if personal, we look at another field
		if (
			(
				$this->arParams['PERSONAL'] == 'Y' ||
				$this->arParams['SPRINT_ID'] > 0
			)
			&&
			isset($params['filter']['STAGE_ID'])
		)
		{
			$params['filter']['STAGES_ID'] = $params['filter']['STAGE_ID'];
			unset($params['filter']['STAGE_ID']);
		}
		[$rows, $res] = \CTaskItem::fetchList(
			$this->userId,
			isset($params['order']) ? $params['order'] : array(),
			isset($params['filter']) ? $params['filter'] : array(),
			isset($params['navigate']) ? $params['navigate'] : array(),
			isset($params['select']) ? $params['select'] : array()
		);

		return $asIs ? array($rows, $res) : $rows;
	}

	/**
	 * Fill data-array with files count and background.
	 * @param array $items Task items.
	 * @return array
	 */
	protected function getFiles(array $items)
	{
		if (empty($items))
		{
			return $items;
		}

		if (Loader::includeModule('disk'))
		{
			// get counts
			$cnt = ConnectorTask::getFilesCount(array_keys($items));
			foreach ($cnt as $taskId => $c)
			{
				$items[$taskId]['data']['count_files'] = $c;
			}
			// get covers
			$covers = ConnectorTask::getCover(
				array_keys($items),
				$this->previewSize['width'],
				$this->previewSize['height']
			);
			foreach ($covers as $taskId => $cover)
			{
				$items[$taskId]['data']['background'] = $cover;
			}
		}

		return $items;
	}

	/**
	 * Fill data-array with checklist counts.
	 * @param array $items Task items.
	 * @return array
	 */
	protected function getCheckList(array $items)
	{
		if (empty($items))
		{
			return $items;
		}

		$query = new Query(Task\CheckListTable::getEntity());
		$query->setSelect(['TASK_ID', 'IS_COMPLETE', new ExpressionField('CNT', 'COUNT(TASK_ID)')]);
		$query->setFilter(['TASK_ID' => array_keys($items), ]);
		$query->setGroup(['TASK_ID', 'IS_COMPLETE']);
		$query->registerRuntimeField('', new ReferenceField(
			'IT',
			Task\CheckListTreeTable::class,
			Join::on('this.ID', 'ref.CHILD_ID')->where('ref.LEVEL', 1),
			['join_type' => 'INNER']
		));

		$res = $query->exec();
		while ($row = $res->fetch())
		{
			$checkList =& $items[$row['TASK_ID']]['data']['check_list'];
			$checkList[$row['IS_COMPLETE'] == 'Y' ? 'complete' : 'work'] = $row['CNT'];
		}

		return $items;
	}

	/**
	 * Fill data-array with tags.
	 * @param array $items Task items.
	 * @return array
	 */
	protected function getTags(array $items)
	{
		if (empty($items))
		{
			return $items;
		}

		$res = Task\TagTable::getList(array(
			'select' => array(
				'TASK_ID', 'NAME'
			),
			'filter' => array(
				'TASK_ID' => array_keys($items)
			)
		));
		while ($row = $res->fetch())
		{
			$tags =& $items[$row['TASK_ID']]['data']['tags'];
			$tags[] = $row['NAME'];
		}

		return $items;
	}

	/**
	 * Fill data-array with time starting delta.
	 * @param array $items Task items.
	 * @return array
	 */
	protected function getTimeStarted(array $items)
	{
		if (empty($items))
		{
			return $items;
		}

		$res = \Bitrix\Tasks\Internals\Task\TimerTable::getList(array(
			'filter' => array(
				'TASK_ID' => array_keys($items),
				'>TIMER_STARTED_AT' => 0
			)
		));
		while ($row = $res->fetch())
		{
			$delta = time() - $row['TIMER_STARTED_AT'];
			$items[$row['TASK_ID']]['data']['time_logs'] += $delta;
			//$items[$row['TASK_ID']]['data']['time_logs_start'] += $delta;
		}

		return $items;
	}

	/**
	 * Fill data-array with new log-data.
	 * @param array $items Task items.
	 * @return array
	 */
	protected function getNewLog(array $items)
	{
		if (empty($items))
		{
			return $items;
		}
		// first get last viewed dates
		$res = Task\ViewedTable::getList(array(
			'filter' => array(
				'USER_ID' => $this->userId,
				'TASK_ID' => array_keys($items)
			)
		));
		while ($row = $res->fetch())
		{
			$items[$row['TASK_ID']]['data']['date_view'] = $row['VIEWED_DATE'];
		}
		// then get new log after view
		$filterLog = array(
			'LOGIC' => 'OR'
		);
		foreach ($items as $id => &$item)
		{
			if ($item['data']['date_view'])
			{
				$filterLog[] = array(
					'>CREATED_DATE' => $item['data']['date_view'],
					'TASK_ID' => $id
				);
			}
			$item['data']['date_view'] = $item['data']['date_view']->getTimestamp();
		}
		unset($item);
		$res = Task\LogTable::getList(array(
			'select' => array(
				'TASK_ID', 'FIELD', 'FROM_VALUE', 'TO_VALUE'
			),
			'filter' => array(
				'!USER_ID' => $this->userId,
				'FIELD' => array(
					'COMMENT', static::USER_WEBDAV_CODE,
					'CHECKLIST_ITEM_CREATE'
				),
				$filterLog
			)
		));
		while ($row = $res->fetch())
		{
			$log =& $items[$row['TASK_ID']]['data']['log'];

			// wee need only files and comments
			if ($row['FIELD'] == 'COMMENT')
			{
				$log['comment']++;
			}
			elseif ($row['FIELD'] == static::USER_WEBDAV_CODE)
			{
				$row['FROM_VALUE'] = $row['FROM_VALUE'] == '' ? 0 : count(explode(',', $row['FROM_VALUE']));
				$row['TO_VALUE'] = $row['TO_VALUE'] == '' ? 0 : count(explode(',', $row['TO_VALUE']));
				if ($row['TO_VALUE'] > $row['FROM_VALUE'])
				{
					$log['file'] += ($row['TO_VALUE'] - $row['FROM_VALUE']);
				}
			}
			elseif ($row['FIELD'] == 'CHECKLIST_ITEM_CREATE')
			{
				$log['checklist']++;
			}
		}

		return $items;
	}

	/**
	 * Get info about users.
	 * @param array $items Task items.
	 * @return array
	 */
	protected function getUsers(array $items)
	{
		// get users
		$members = array();
		foreach ($items as $item)
		{
			if ($item['data']['author'] > 0)
			{
				$members[] = $item['data']['author'];
			}
			if ($item['data']['responsible'] > 0)
			{
				$members[] = $item['data']['responsible'];
			}
		}
		// set users
		if (!empty($members))
		{
			$users = array();
			$select = array(
				'ID', 'PERSONAL_PHOTO', 'NAME', 'LAST_NAME', 'SECOND_NAME', 'EXTERNAL_AUTH_ID',
				static::USER_DEPARTMENT_CODE
			);
			if (Loader::includeModule('crm'))
			{
				$select[] = static::USER_CRM_CODE;
			}
			$res = \Bitrix\Main\UserTable::getList(array(
				'select' => $select,
				'filter' => array(
					'ID' => $members
				)
			));
			while ($row = $res->fetch())
			{
				if ($row['PERSONAL_PHOTO'])
				{
					$row['PERSONAL_PHOTO'] = \CFile::ResizeImageGet(
						$row['PERSONAL_PHOTO'],
						$this->avatarSize,
						BX_RESIZE_IMAGE_EXACT
					);
				}
				$row['USER_NAME'] = \CUser::FormatName(
					$this->arParams['~NAME_TEMPLATE'],
					$row, true, false);
				$users[$row['ID']] = array(
					'id' => $row['ID'],
					'photo' => $row['PERSONAL_PHOTO'],
					'name' => $row['USER_NAME'],
					'crm' => false,
					'mail' => false,
					'extranet' => false
				);
				if (
					isset($row[static::USER_CRM_CODE]) &&
					$row[static::USER_CRM_CODE]
				)
				{
					$users[$row['ID']]['crm'] = true;
				}
				elseif ($row['EXTERNAL_AUTH_ID'] == static::USER_TYPE_MAIL)
				{
					$users[$row['ID']]['mail'] = true;
				}
				elseif (
					!isset($row[static::USER_DEPARTMENT_CODE][0]) ||
					!$row[static::USER_DEPARTMENT_CODE][0]
				)
				{
					$users[$row['ID']]['extranet'] = true;
				}
			}
		}
		// fill users
		if (!empty($members))
		{
			foreach ($items as &$item)
			{
				$item['data']['author'] = isset($users[$item['data']['author']]) ? $users[$item['data']['author']] : null;
				$item['data']['responsible'] = isset($users[$item['data']['responsible']]) ? $users[$item['data']['responsible']] : null;
			}
			unset($item);
		}

		return $items;
	}

	/**
	 * @param array $taskData
	 * @return array
	 */
	protected function getDeadlineProps(array $taskData): array
	{
		$value = Date::formatDate($taskData['DEADLINE']);
		$color = '';
		$fill = false;

		$state = Date\Deadline::getDeadlineStateData($taskData);
		if ($state['state'])
		{
			$value = $state['state'];
			$color = "ui-label-{$state['color']}";
			$fill = ($state['fill'] ? true : false);
		}

		return [
			'value' => $value,
			'color' => $color,
			'fill' => $fill,
		];
	}

	/**
	 * @param array $taskData
	 * @return array
	 */
	protected function getCounterProps(array $taskData): array
	{
		$status = (int)$taskData['REAL_STATUS'];
		$deadline = $taskData['DEADLINE'];

		$isExpired = ($deadline && $this->isExpired($this->getDateTimestamp($deadline)));
		$isDeferred = ($status === CTasks::STATE_DEFERRED);
		$isWaitCtrlCounts = (
			$status === CTasks::STATE_SUPPOSEDLY_COMPLETED
			&& (int)$taskData['CREATED_BY'] === $this->userId
			&& (int)$taskData['RESPONSIBLE_ID'] !== $this->userId
		);
		$isCompletedCounts = (
			$status === CTasks::STATE_COMPLETED
			|| ($status === CTasks::STATE_SUPPOSEDLY_COMPLETED && (int)$taskData['CREATED_BY'] !== $this->userId)
		);

		$value = ((int)$taskData['NEW_COMMENTS_COUNT'] > 0 ? (int)$taskData['NEW_COMMENTS_COUNT'] : 0);
		$color = 'success';

		if ($isExpired && !$isCompletedCounts && !$isWaitCtrlCounts && !$isDeferred)
		{
			$value++;
			$color = 'danger';
		}

		if ($taskData['IS_MUTED'] === 'Y')
		{
			$color = 'gray';
		}

		return [
			'value' => ($this->isMember($this->userId, $taskData) ? $value : 0),
			'color' => "ui-counter-{$color}",
		];
	}

	/**
	 * @param int $userId
	 * @param array $taskData
	 * @return bool
	 */
	protected function isMember(int $userId, array $taskData): bool
	{
		$members = array_unique(
			array_merge(
				[$taskData['CREATED_BY'], $taskData['RESPONSIBLE_ID']],
				$taskData['ACCOMPLICES'],
				$taskData['AUDITORS']
			)
		);
		$members = array_map('intval', $members);

		return in_array($userId, $members, true);
	}

	/**
	 * @param string $date
	 * @return int
	 */
	protected function getDateTimestamp($date): int
	{
		$timestamp = MakeTimeStamp($date);

		if ($timestamp === false)
		{
			$timestamp = strtotime($date);
			if ($timestamp !== false)
			{
				$timestamp += CTimeZone::GetOffset()
					- \Bitrix\Tasks\Util\Type\DateTime::createFromTimestamp($timestamp)->getSecondGmt();
			}
		}

		return $timestamp;
	}

	/**
	 * @param int $timestamp
	 * @return bool
	 */
	protected function isExpired(int $timestamp): bool
	{
		return $timestamp && ($timestamp <= $this->getNow());
	}

	/**
	 * @return int
	 */
	protected function getNow(): int
	{
		return (new \Bitrix\Tasks\Util\Type\DateTime())->getTimestamp() + CTimeZone::GetOffset($this->userId);
	}

	/**
	 * Send handler event.
	 * @param string $codeEvent Code of event.
	 * @param mixed $data Data for event.
	 * @return array
	 */
	protected function sendEvent($codeEvent, $data)
	{
		$event = new \Bitrix\Main\Event('tasks', $codeEvent, array(
			'DATA' => $data,
		));
		$event->send();
		foreach ($event->getResults() as $result)
		{
			if ($result->getResultType() != \Bitrix\Main\EventResult::ERROR)
			{
				if (($modified = $result->getModified()))
				{
					if (isset($modified['DATA']))
					{
						$data = $modified['DATA'];
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Get demo columns.
	 * @param int $viewId View id.
	 * @return array
	 */
	protected function getDemoColumns($viewId)
	{
		$columns = array();

		foreach (StagesTable::getStages($viewId) as $stage)
		{
			$columns[] = array(
				'id' => -1 * $stage['ID'],
				'name' => $stage['TITLE'],
				'color' => $stage['COLOR'],
				'type' => $stage['SYSTEM_TYPE'],
				'sort' => $stage['SORT'],
				'total' => 0,
				'canSort' => true
			);
		}

		return $columns;
	}

	/**
	 * Base method for getting columns.
	 * @param bool $assoc Return as associative.
	 * @return array
	 */
	protected function getColumns($assoc = false)
	{
		$columns = array();
		$counts = array();
		$canSort = $this->canSortTasks();
		$timeLineMode = $this->arParams['TIMELINE_MODE'] == 'Y';
		$filter = $this->getFilter();

		// get counts
		if (!$timeLineMode)
		{
			$res = StagesTable::getStagesCount(
				$filter,
				$this->arParams['USER_ID'] ? $this->arParams['USER_ID'] : false
			);
			while ($row = $res->fetch())
			{
				$counts[$row['STAGE_ID']] = $row['CNT'];
			}
		}

		// get columns
		foreach (StagesTable::getStages($this->arParams['STAGES_ENTITY_ID']) as $stage)
		{
			$count = 0;

			if ($stage['ADDITIONAL_FILTER'])
			{
				$filterTmp = array_merge(
					$filter,
					$stage['ADDITIONAL_FILTER']
				);
				[$rows, ] = $this->getList(array(
					'select' => ['ID'],
					'filter' => $filterTmp
				), true);
				$count = count($rows);
			}
			else
			{
				$stages = (array)StagesTable::getStageIdByCode(
					$stage['ID'],
					$this->arParams['STAGES_ENTITY_ID']
				);
				foreach ($stages as $stId)
				{
					if (isset($counts[$stId]))
					{
						$count += $counts[$stId];
					}
				}
			}

			$columns[$stage['ID']] = array(
				'id' => $stage['ID'],
				'name' => $stage['TITLE'],
				'color' => $stage['COLOR'],
				'type' => $stage['SYSTEM_TYPE'],
				'sort' => $stage['SORT'],
				'total' => $count,
				'canSort' => $this->isAdmin() && $this->arParams['TIMELINE_MODE'] != 'Y',
				'canAddItem' => $stage['TO_UPDATE_ACCESS'] !== false
			);
		}

		$columns = $this->sendEvent('KanbanComponentGetColumns', $columns);

		return $assoc ? $columns : array_values($columns);
	}

	/**
	 * Fill one item with task data.
	 * @param object $task Task item.
	 * @return array
	 */
	protected function fillData(\CTaskItem $task)
	{
		static $endDayTime = null;
		static $date = null;

		if ($date === null)
		{
			$date = new DateTime;
		}

		if ($endDayTime === null)
		{
			$endDayTime = Option::get('calendar', 'work_time_end', 19);
		}

		$item = $task->getData();

		$storyPoints = '';
		if ($this->arParams['SPRINT_SELECTED'] == 'Y')
		{
			$itemService = new ItemService();
			$storyPoints = $itemService->getItemStoryPointsBySourceId($item['ID']);
		}

		$canEdit = $task->isActionAllowed(\CTaskItem::ACTION_EDIT);
		$data = array(
			// base
			'id' => $item['ID'],
			'stage_id' => $item['STAGE_ID'],
			'name' => $item['TITLE'],
			'background' => '',
			'author' => $item['CREATED_BY'],
			'responsible' => $item['RESPONSIBLE_ID'],
			'tags' => [],
			'counter' => $this->getCounterProps($item),
			'deadline' => $this->getDeadlineProps($item),
			// time
			'time_tracking' => $item['ALLOW_TIME_TRACKING'] == 'Y',
			'time_logs' => (int)$item['TIME_SPENT_IN_LOGS'],
			'time_logs_start' => (int)$item['TIME_SPENT_IN_LOGS'],
			'time_estimate' => $item['TIME_ESTIMATE'],
			// rights
			'allow_change_deadline' => $task->isActionAllowed(\CTaskItem::ACTION_CHANGE_DEADLINE),
			'allow_delegate' => $task->isActionAllowed(\CTaskItem::ACTION_DELEGATE) || $canEdit,
			'allow_complete' => $task->isActionAllowed(\CTaskItem::ACTION_COMPLETE),
			'allow_start' => $task->isActionAllowed(\CTaskItem::ACTION_START) || $task->isActionAllowed(\CTaskItem::ACTION_PAUSE),
			'allow_time_tracking' => $task->isActionAllowed(\CTaskItem::ACTION_START_TIME_TRACKING),
			'allow_edit' => $canEdit,
			// dates
			'date_activity' => $item['ACTIVITY_DATE'],
			'date_activity_ts' => MakeTimeStamp($item['ACTIVITY_DATE']),
			'date_deadline' => MakeTimeStamp($item['DEADLINE']),
			'date_deadline_parse' => ParseDateTime($item['DEADLINE']),
			'date_start' => $item['DATE_START'] != '' ? new DateTime($item['DATE_START']) : '',
			'date_view' => new DateTime($item['CREATED_DATE']),
			'date_day_end' => mktime($endDayTime - $this->timeOffset/3600, 0, 0),
			// counts
			'count_comments' => (int)$item['COMMENTS_COUNT'],
			'count_files' => 0,
			'count_members' => count(array_unique(array_merge(
				$item['AUDITORS'],
				$item['ACCOMPLICES']
			))),
			'check_list' => array(
				'work' => 0,
				'complete' => 0
			),
			'log' => array(
				'comment' => 0,
				'file' => 0,
				'checklist' => 0
			),
			// statuses
			'overdue' => false,
			'is_expired' => $this->isExpired($this->getDateTimestamp($item['DEADLINE'])),
			'high' => $item['PRIORITY'] == \CTasks::PRIORITY_HIGH,
			'new' => $item['STATUS'] == \CTasks::METASTATE_VIRGIN_NEW,
			'in_progress' => $item['REAL_STATUS'] == \CTasks::STATE_IN_PROGRESS,
			'deferred' => $item['STATUS'] == \CTasks::STATE_DEFERRED,
			'completed' => $item['STATUS'] == \CTasks::STATE_COMPLETED,
			'completed_supposedly' => $item['STATUS'] == \CTasks::STATE_SUPPOSEDLY_COMPLETED,
			'muted' => $item['IS_MUTED'] === 'Y',
			'storyPoints' => $storyPoints
		);
		if ($data['date_start'])
		{
			$data['date_start'] = $data['date_start']->getTimestamp();
		}

		return $data;
	}

	/**
	 * Get one task item.
	 * @param int $id Item id.
	 * @param string|int $columnId Code of stage (if know).
	 * @param bool $bCheckPermission Check permissions.
	 * @return array
	 */
	protected function getRawData($taskId, $columnId = false, $bCheckPermission = true)
	{
		if ($columnId === null)
		{
			$columnId = false;
		}
		$row = $this->getList(array(
			'select' => $this->getSelect(),
			'filter' => array(
				'ID' => $taskId,
				'CHECK_PERMISSIONS' => $bCheckPermission ? 'Y' : 'N'
			),
			'order' => $this->getOrder()
		));
		if (!empty($row))
		{
			$row = array_pop($row);
			if (($task = $this->fillData($row)))
			{
				$task = array(
					$task['id'] => array(
						'id' => $task['id'],
						'columnId' => $task['stage_id'] > 0 ? $task['stage_id'] : $columnId,
						'data' => $task,
						'isSprintView' => ($this->arParams['SPRINT_ID'] > 0 ? 'Y' : 'N')
					)
				);

				// get other data
				$task = $this->getUsers($task);
				$task = $this->getNewLog($task);
				$task = $this->getFiles($task);
				$task = $this->getCheckList($task);
				$task = $this->getTags($task);

				$task = $this->sendEvent('TimelineComponentGetItems', $task);

				return array_pop($task);
			}
		}

		return array();
	}

	/**
	 * Returns stages by task Id.
	 * @param int $taskId
	 * @return array
	 */
	protected function getStages(int $taskId): array
	{
		$stages = [];
		$res = TaskStageTable::getList([
			'select' => [
				'STAGE_ID'
			],
			'filter' => [
				'TASK_ID' => $taskId
			]
		]);
		while ($row = $res->fetch())
		{
			$stages[] = (int)$row['STAGE_ID'];
		}

		return $stages;
	}

	/**
	 * Base method for getting data.
	 * @param array $additionalFilter Additional filter.
	 * @param bool $skipCommonFilter Skip merge with common filter.
	 * @return array
	 */
	protected function getData(array $additionalFilter = [], $skipCommonFilter = false)
	{
		$items = array();
		$order = $this->getOrder();
		$filter = $this->getFilter();
		$listParams = $this->getListParams();
		$select = $this->getSelect();

		if ($skipCommonFilter)
		{
			$filter = $additionalFilter;
		}
		else if ($additionalFilter)
		{
			$filter = array_merge(
				$filter,
				$additionalFilter
			);
		}

		if (isset($filter['ONLY_STAGE_ID']))
		{
			$onlyStageId = $filter['ONLY_STAGE_ID'];
			unset($filter['ONLY_STAGE_ID']);
		}

		$listParams['MAKE_ACCESS_FILTER'] = true;

		// get tasks by stages
		foreach (StagesTable::getStages($this->arParams['STAGES_ENTITY_ID']) as $column)
		{
			$stageId = StagesTable::getStageIdByCode(
				$column['ID'],
				$this->arParams['STAGES_ENTITY_ID']
			);
			if (
				isset($onlyStageId) &&
				!in_array($onlyStageId, (array)$stageId)
			)
			{
				continue;
			}

			if ($column['ADDITIONAL_FILTER'])
			{
				$filterTmp = array_merge(
					$filter,
					$column['ADDITIONAL_FILTER']
				);
			}
			else
			{
				$filterTmp = array_merge(
					$filter,
					array(
						'STAGE_ID' => $stageId
					)
				);
			}

			[$rows, $res] = $this->getList(array(
				'select' => $select,
				'filter' => $filterTmp,
				'order' => $order,
				'navigate' => $listParams
			), true);
			// something wrong
			if (
				count($rows) > 0 && $res->NavPageNomer !=
				$listParams['NAV_PARAMS']['iNumPage']
			)
			{
				break;
			}
			foreach ($rows as $row)
			{
				$item = $this->fillData($row);
				$items[$item['id']] = array(
					'id' => $item['id'],
					'columnId' => $column['ID'],
					'data' => $item,
					'isSprintView' => ($this->arParams['SPRINT_SELECTED'] == 'Y' ? 'Y' : 'N')
				);
				if (
					isset($filterTmp['ID']) &&
					$filterTmp['ID'] == $item['ID']
				)
				{
					break 2;
				}
			}
		}

		// get other data
		$items = $this->getUsers($items);
		$items = $this->getNewLog($items);
		$items = $this->getFiles($items);
		$items = $this->getCheckList($items);
		$items = $this->getTags($items);
		$items = $this->getTimeStarted($items);

		$items = $this->sendEvent('KanbanComponentGetItems', $items);

		return array_values($items);
	}

	/**
	 * Exist or not mandatory fields in the task.
	 * @return boolean
	 */
	protected function mandatoryExists()
	{
		static $result = null;

		if ($result === null)
		{
			$res = \CUserTypeEntity::getList(
				array(),
				array(
					'ENTITY_ID' => 'TASKS_TASK',
					'MANDATORY' => 'Y'
				)
			);
			if ($res->fetch())
			{
				$result = true;
			}
			else
			{
				$result = false;
			}
		}

		return $result;
	}

	/**
	 * Set title.
	 * @return void
	 */
	protected function setTitle()
	{
		$params = $this->arParams;

		if ($params['SET_TITLE'] == 'Y')
		{
			if ($params['PROJECT_VIEW'] === 'Y')
			{
				$this->application->setTitle(Loc::getMessage('TASK_PROJECT_TITLE'));
			}
			else if ($params['TIMELINE_MODE'] == 'Y')
			{
				$this->application->setTitle(Loc::getMessage('TASK_LIST_TITLE_TIMELINE'));
			}
			else if ($this->itsMyTasks())
			{
				$this->application->setTitle(Loc::getMessage('TASK_KANBAN'.($params['PERSONAL']!='N'?'_PERSONAL':'').'_TITLE'));
			}
			elseif ($params['SPRINT_SELECTED'] == 'Y')
			{
				$this->application->setTitle(Loc::getMessage('TASK_LIST_TITLE_SPRINT'));
			}
			elseif ($this->taskType == static::TASK_TYPE_GROUP && !$params['GROUP_ID_FORCED'])
			{
				$this->application->setTitle(Loc::getMessage('TASK_LIST_TITLE_GROUP'));
			}
			else
			{
				$userName = \CUser::FormatName($params['~NAME_TEMPLATE'], $this->arResult['USER'], true, false);
				$userName = \htmlspecialcharsbx($userName);
				$this->application->setPageProperty('title', $userName . ': ' . Loc::getMessage('TASK_KANBAN'.($params['PERSONAL']!='N'?'_PERSONAL':'').'_TITLE'));
				$this->application->setTitle(Loc::getMessage('TASK_KANBAN'.($params['PERSONAL']!='N'?'_PERSONAL':'').'_TITLE'));
			}
		}
	}

	/**
	 * Convert charset from utf-8 to site.
	 * @param mixed $data
	 * @param bool $fromUtf Direction - from (true) or to (false).
	 * @return mixed
	 */
	protected function convertUtf($data, $fromUtf)
	{
		if (SITE_CHARSET != 'UTF-8')
		{
			$from = $fromUtf ? 'UTF-8' : SITE_CHARSET;
			$to = !$fromUtf ? 'UTF-8' : SITE_CHARSET;
			if (is_array($data))
			{
				$data = $this->application->ConvertCharsetArray($data, $from, $to);
			}
			else
			{
				$data = $this->application->ConvertCharset($data, $from, $to);
			}
		}
		return $data;
	}

	/**
	 * Check views for current Kanban. Ask admin for create default.
	 * @return boolean
	 */
	protected function checkViews()
	{
		$params = $this->arParams;
		$this->arResult['MP_CONVERTER'] = 0;

		// for sprint's kanban create by default
		if ($params['SPRINT_ID'])
		{
			return true;
		}

		// for personal - create defaults and copy tasks to default stage
		if ($params['PERSONAL'] == 'Y')
		{
			$checkStages = StagesTable::getStages($params['STAGES_ENTITY_ID'], true);
			if ($params['TIMELINE_MODE'] == 'Y')
			{
				return true;
			}
			if (empty($checkStages))
			{
				$this->arResult['MP_CONVERTER'] = 1;
			}
			// for new versions of personal we need to insert sone new tasks
			else
			{
				$userVersion = \CUserOptions::getOption(
					'tasks',
					'personal_kanban_version',
					1,
					$params['STAGES_ENTITY_ID']
				);
				if ($userVersion < StagesTable::MY_PLAN_VERSION)
				{
					$this->arResult['MP_CONVERTER'] = 1;
				}
			}
			return true;
		}

		// stages is empty - ask admin for copy stages
		if ($this->isAdmin() && $params['SONET_LOAD'])
		{
			$checkStages = StagesTable::getStages($params['STAGES_ENTITY_ID'], true);
			if (empty($checkStages))
			{
				if ($params['IS_AJAX'] == 'Y')
				{
					return false;
				}
				else
				{
					if (
						$this->taskType == static::TASK_TYPE_GROUP &&
						!\CSocNetGroup::GetByID($params['GROUP_ID'])
					)
					{
						return true;
					}

					$views = array();
					if (empty($views))
					{
						// create default view, if not exists
						StagesTable::getStages(0);
						// get all stages views
						$res = StagesTable::getList(array(
							'select' => array(
								'ENTITY_ID'
							),
							'filter' => array(
								'=ENTITY_TYPE' => StagesTable::WORK_MODE_GROUP
							),
							'group' => array(
								'ENTITY_ID'
							)
						));
						while ($row = $res->fetch())
						{
							$views[$row['ENTITY_ID']] = array();
						}
						// get current user groups
						if (!empty($views))
						{
							$res = \CSocNetUserToGroup::GetList(
								array(
									'GROUP_DATE_ACTIVITY' => 'DESC'
								),
								array(
									'GROUP_ID' => array_keys($views),
									'USER_ID' => $this->userId
								),
								false, false,
								array(
									'ID', 'GROUP_NAME', 'GROUP_ID'
								)
							);
							while ($row = $res->fetch())
							{
								if ($this->canReadGroupTasks($row['GROUP_ID']))
								{
									$views[$row['GROUP_ID']] = array(
										'ID' => $row['GROUP_ID'],
										'NAME' => $row['GROUP_NAME']
									);
								}
							}
						}
						// check disable views for current user
						$viewsTmp = array();
						foreach ($views as $vId => $view)
						{
							if (!empty($view))
							{
								$viewsTmp[$vId] = $view;
							}
						}
						$views = $viewsTmp;
						unset($viewsTmp);
						// if view not exists - copy from default
						if (empty($views))
						{
							StagesTable::copyView(0, $params['GROUP_ID']);
							$this->reloadPage();
						}
						// if user submit view, what he want
						$setView = $this->request('set_view_id');
						if ($setView !== null && ($setView == 0 || isset($views[$setView])))
						{
							StagesTable::copyView($setView, $params['GROUP_ID']);
							$this->reloadPage();
						}
						// if default
						if ($params['GROUP_ID'] == 0)
						{
							$this->reloadPage();
						}
						// else ask user
						$this->arResult['VIEWS'] = $views;
						$this->arResult['DATA'] = array(
							'columns' => $this->getDemoColumns(0),
							'items' => array(),
							'demo' => true
						);
						return false;
					}
				}
			}
		}

		return true;
	}

	/**
	 * Get order for new tasks.
	 * @return string
	 */
	protected function getNewTaskOrder()
	{
		$groupId = $this->arParams['GROUP_ID'];

		if ($groupId > 0 && $this->arParams['PERSONAL'] != 'Y')
		{
			if (($row = ProjectsTable::getById($groupId)->fetch()))
			{
				return $row['ORDER_NEW_TASK'] ? $row['ORDER_NEW_TASK'] : 'actual';
			}
			else
			{
				return 'actual';
			}
		}
		else
		{
			return \CUserOptions::getOption('tasks', 'order_new_task_v2', 'actual');
		}
	}

	/**
	 * Init arResult.
	 * @return void
	 */
	protected function initResult()
	{
		$this->arResult['ERRORS'] = $this->getErrors();
		$this->arResult['ERRORS_COLLECTION'] = $this->getErrors(false);
		$this->arResult['TASK_TYPE'] = $this->taskType;
		$this->arResult['PAGE'] = $this->getURI();
		$this->arResult['CURRENT_USER_ID'] = $this->userId;
		$this->arResult['ACCESS_CONFIG_PERMS'] = $this->isAdmin();
		$this->arResult['ACCESS_CREATE_PERMS'] = $this->canCreateTasks();
		$this->arResult['ACCESS_SORT_PERMS'] = $this->canSortTasks();
		$this->arResult['NEW_TASKS_ORDER'] = $this->getNewTaskOrder();
		$this->arResult['ITS_MY_TASKS'] = $this->itsMyTasks();
		$this->arResult['ADMINS'] = array();

		$this->arResult['DEFAULT_PRESET_KEY'] = $this->filterInstance->getDefaultPresetKey();

		if (!$this->arResult['VIEWS'])
		{
			$this->arResult['VIEWS'] = array();
		}

		if (!$this->arResult['ACCESS_CONFIG_PERMS'])
		{
			$this->arResult['ADMINS'] = $this->getAdmins();
		}
	}

	/**
	 * Checks session key.
	 */
	protected function checkSessionDataKey()
	{
		if (
			!isset($_SESSION[$this::SESS_DATA_KEY]) ||
			!is_array($_SESSION[$this::SESS_DATA_KEY])
		)
		{
			$_SESSION[$this::SESS_DATA_KEY] = [];
		}
	}

	/**
	 * Stores data in session.
	 * @param string $key Data key.
	 * @param mixed $value Key payload.
	 * @return void
	 */
	protected function setSessionData($key, $value)
	{
		if (!is_string($key) && !is_int($key))
		{
			return;
		}
		$this->checkSessionDataKey();
		$_SESSION[$this::SESS_DATA_KEY][$key] = $value;
	}

	/**
	 * Returns data from session by key.
	 * @param string $key Data key.
	 * @return mixed
	 */
	protected function getSessionData($key)
	{
		if (is_string($key) || is_int($key))
		{
			$this->checkSessionDataKey();
			if (array_key_exists($key, $_SESSION[$this::SESS_DATA_KEY]))
			{
				return $_SESSION[$this::SESS_DATA_KEY][$key];
			}
		}
		return null;
	}

	/**
	 * Base executable method.
	 * @return void
	 */
	public function executeComponent()
	{
		$init = $this->init();

		// if ajax, go to the actions
		if ($this->arParams['IS_AJAX'] == 'Y')
		{
			$this->executeComponentAjax();
			return;
		}

		$this->arResult['NEED_SET_CLIENT_DATE'] = true;
		/*$clientTimeTTL = $this->getSessionData('clientTimeTTL');
		$clientTimeStored = $this->getSessionData('clientTimeStored');
		$this->arResult['NEED_SET_CLIENT_DATE'] = (time() > $clientTimeTTL) ||
													(time() - $clientTimeStored > 600);*/

		if (!$this->arResult['NEED_SET_CLIENT_DATE'])
		{
			TimeLineTable::setDateClient(
				$this->getSessionData('clientDate')
			);
			TimeLineTable::setDateTimeClient(
				$this->getSessionData('clientTime')
			);
		}

		// if all right, get data
		if ($this->checkViews() && $init)
		{
			$this->arResult['DATA'] = array(
				'columns' => $this->getColumns(),
				'items' => $this->getData(),
				'demo' => false
			);
		}

		// if need converter, check current task count
		if ($this->arResult['MP_CONVERTER'] > 0)
		{
			$res = \CTasks::GetList(array(
				//
			), array(
				'MEMBER' => $this->arParams['STAGES_ENTITY_ID']
			), array(
				'ID'
			), array(
				'nPageSize' => 1
			));
			if ($res->fetch())
			{
				$this->arResult['MP_CONVERTER'] = 1;//some bug in tasks
			}
			else
			{
				$this->arResult['MP_CONVERTER'] = 0;
				\CUserOptions::setOption(
					'tasks',
					'personal_kanban_version',
					StagesTable::MY_PLAN_VERSION,
					false,
					$this->arParams['STAGES_ENTITY_ID']
				);
			}
		}

		$this->arResult['FATAL'] = !$init;
		$this->arResult['MANDATORY_EXISTS'] = $this->mandatoryExists();

		$this->initResult();
		$this->setTitle();
		$this->IncludeComponentTemplate();
	}

	/**
	 * Ajax-hit, make some actions.
	 * @return void
	 */
	public function executeComponentAjax()
	{
		$fatal = false;
		// call action
		if ($this->init())
		{
			if (check_bitrix_sessid())
			{
				$action = $this->request('action');
				if ($action && is_callable(array($this, 'action' . $action)))
				{
					try
					{
						$this->setSessionData('clientTimeTTL', 0);
						$return = $this->{'action' . $action}();
					}
					catch (TasksException $e)
					{
						$fatal = true;
						$return = array();
						$this->addError($e->getMessage());
					}

				}
			}
			else
			{
				$fatal = true;
				$return = array();
				$this->addError('TASK_LIST_SESS_EXPIRED');
			}
		}
		if (!isset($return))
		{
			$fatal = true;
			$return = array();
			$this->addError('TASK_LIST_UNKNOWN_ACTION');
		}
		// if is array, output as a json
		if (is_array($return))
		{
			if ($errors = $this->getErrors())
			{
				$return = array(
					'error' => implode("\n", $errors),
					'fatal' => $fatal
				);
			}
			$this->application->RestartBuffer();
			header('Content-Type: application/json');
			echo \Bitrix\Main\Web\Json::encode($return);
		}
		// else simple html
		else
		{
			echo $return;
		}
	}

	/**
	 * Set sorting to task.
	 * @param int $taskId
	 * @param int $beforeId
	 * @param int $afterId
	 * @return void
	 */
	protected function setSorting($taskId, $beforeId, $afterId = 0, $userId = false)
	{
		if ($userId === false)
		{
			$userId = $this->userId;
		}
		if ($beforeId > 0)
		{
			Task\SortingTable::setSorting(
				$userId,
				$this->arParams['GROUP_ID'],
				$taskId,
				$beforeId,
				true
			);
		}
		elseif ($afterId > 0)
		{
			Task\SortingTable::setSorting(
				$userId,
				$this->arParams['GROUP_ID'],
				$taskId,
				$afterId,
				false
			);
		}
	}

	/**
	 * Add new task.
	 * @return array
	 */
	protected function actionAddTask()
	{
		if (
			($columnId = $this->request('columnId')) &&
			($taskName = $this->request('taskName'))
		)
		{
			if ($this->mandatoryExists())
			{
				$this->addError('TASK_LIST_TASK_MANDATORY_EXISTS');
				return array();
			}

			if (!$this->canCreateTasks())
			{
				$this->addError('TASK_LIST_TASK_CREATE_DENIED');
				return array();
			}

			$params = $this->arParams;

			$responsibleId = $params['USER_ID'];
			if (StagesTable::getWorkMode() == StagesTable::WORK_MODE_ACTIVE_SPRINT &&
				Loader::includeModule('socialnetwork'))
			{
				$group = Workgroup::getById($params['GROUP_ID']);
				if ($group)
				{
					$responsibleId =$group->getScrumMaster();
				}
			}

			$stages = StagesTable::getStages($params['STAGES_ENTITY_ID']);
			$fields = array(
				'TITLE' => $this->convertUtf($taskName, true),
				'CREATED_BY' => $this->userId,
				'RESPONSIBLE_ID' => $responsibleId,
				'GROUP_ID' => $params['GROUP_ID'],
				'STAGE_ID' => isset($stages[$columnId]) ? $columnId : 0
			);
			if (isset($stages[$columnId]['TO_UPDATE']))
			{
				$fields = array_merge(
					$fields,
					$stages[$columnId]['TO_UPDATE']
				);
			}
			if (
				$params['PERSONAL'] == 'Y' ||
				$params['SPRINT_ID'] > 0
			)
			{
				unset($fields['STAGE_ID']);
			}
			// disable sort / link - its set below here
			if ($params['GROUP_ID'] == 0)
			{
				StagesTable::disablePinForUser($this->userId);
			}
			if (
				$params['PERSONAL'] == 'Y' &&
				$params['TIMELINE_MODE'] != 'Y'
			)
			{
				StagesTable::disableLinkForUser($params['USER_ID']);
			}
			$task = \CTaskItem::add($fields, $this->userId, ['DISABLE_BIZPROC_RUN' => true]);
			if ($task->getId() > 0)
			{
				$newId = $task->getId();
				// set link
				if (
					(
						$params['PERSONAL'] == 'Y' ||
						$params['SPRINT_ID'] > 0
					)
					&&
					$params['TIMELINE_MODE'] != 'Y'
				)
				{
					TaskStageTable::add(array(
						'TASK_ID' => $newId,
						'STAGE_ID' => $columnId
					));
				}
				// set sort
				if ($params['GROUP_ID'] == 0)
				{
					$this->setSorting(
						$newId,
						$this->request('beforeItemId'),
						$this->request('afterItemId')
					);
				}
				\Bitrix\Tasks\Integration\Bizproc\Listener::onTaskAdd($newId, $task->getData());


				if (StagesTable::getWorkMode() == StagesTable::WORK_MODE_ACTIVE_SPRINT)
				{
					$this->createScrumItem($params['SPRINT_ID'], $newId, $fields);
				}

				// output
				return $this->getRawData($newId, $columnId);
			}
			else
			{
				if (($e = $this->application->GetException()))
				{
					$this->addError($e->getString());
				}
			}
		}

		return array();
	}

	private function createScrumItem(int $sprintId, int $taskId, array $fields): void
	{
		try
		{
			$item = ItemTable::createItemObject();
			$item->setSourceId($taskId);
			$item->setEntityId($sprintId);
			$item->setSort(0);
			$item->setCreatedBy($fields['CREATED_BY']);
			$item->setItemType(ItemTable::TASK_TYPE);
			$result = ItemTable::add($item->getFieldsToCreateTaskItem());
			if (!$result->isSuccess())
			{
				$this->addError($result->getErrorMessages()[0]);
			}
		}
		catch(Exception $exception)
		{
			$this->addError($exception->getMessage());
		}
	}

	/**
	 * Move item from one stage to another.
	 * @return array
	 */
	protected function actionMoveTask()
	{
		$columnId = $this->request('columnId');
		$taskId = $this->request('itemId');
		if (!$taskId)
		{
			$taskId = $this->request('taskId');
		}
		if ($taskId && $columnId && $this->canSortTasks())
		{
			$groupAction = $this->request('groupAction') == 'Y';
			$stages = StagesTable::getStages($this->arParams['STAGES_ENTITY_ID']);

			if (isset($stages[$columnId]))
			{
				$task = new \CTasks;
				$taskIds = (array) $taskId;
				foreach ($taskIds as $taskId)
				{
					$rows = $this->getData(
						['ID' => $taskId],
						true
					);
					foreach ($rows as $data)
					{
						$beforeItemId = $this->request('beforeItemId');
						$afterItemId = $this->request('afterItemId');
						if ($beforeItemId || $afterItemId)
						{
							$this->setSorting(
								$data['id'],
								$this->request('beforeItemId'),
								$this->request('afterItemId')
							);
						}
						else
						{
							Task\SortingTable::deleteByTaskId($data['id']);
						}

						// if is the same column, just sorting
						if ($data['columnId'] == $columnId)
						{
							continue;
						}

					// update some fields, if fields is exists
					if (isset($stages[$columnId]['TO_UPDATE']))
					{
						$acceesAllowed = true;
						if ($stages[$columnId]['TO_UPDATE_ACCESS'])
						{
							$taskInst = \CTaskItem::getInstance($taskId, $this->userId);
							if (!$taskInst->checkAccess(ActionDictionary::getActionByLegacyId($stages[$columnId]['TO_UPDATE_ACCESS'])))
							{
								$acceesAllowed = false;
							}
						}
						if ($acceesAllowed)
						{
							$task->update(
								$data['id'],
								$stages[$columnId]['TO_UPDATE']
							);
						}
						else if (!$groupAction)
						{
							$this->addError('TASK_LIST_TASK_ACTION_DENIED');
							return [];
						}
					}

						// if is timeline, we don't have real columns
						if ($this->arParams['TIMELINE_MODE'] == 'Y')
						{
							continue;
						}

						// personal kanban
						if (
							$this->arParams['PERSONAL'] == 'Y' ||
							$this->arParams['SPRINT_ID'] > 0
						)
						{
							$resStg = TaskStageTable::getList(array(
								'filter' => array(
									'TASK_ID' => $data['id'],
									'=STAGE.ENTITY_TYPE' => StagesTable::getWorkMode(),
									'STAGE.ENTITY_ID' => $this->arParams['STAGES_ENTITY_ID']
								)
							));
							while ($rowStg = $resStg->fetch())
							{
								TaskStageTable::update($rowStg['ID'], array(
									'STAGE_ID' => $columnId
								));

								if (
									$this->arParams['PERSONAL'] == 'Y' &&
									$columnId !== $rowStg['STAGE_ID']
								)
								{
									\Bitrix\Tasks\Integration\Bizproc\Listener::onPlanTaskStageUpdate(
										$this->arParams['STAGES_ENTITY_ID'],
										$rowStg['TASK_ID'],
										$columnId
									);
								}
							}
							if (StagesTable::getWorkMode() === StagesTable::WORK_MODE_ACTIVE_SPRINT)
							{
								if ($stages[$columnId]['SYSTEM_TYPE'] === StagesTable::SYS_TYPE_FINISH)
								{
									$this->completeTask($taskId);
								}
								else
								{
									$this->renewTask($taskId);
								}
								StagesTable::setWorkMode(StagesTable::WORK_MODE_ACTIVE_SPRINT);
							}
						}
						// or common
						else
						{
							$task->update($data['id'], array(
								'STAGE_ID' => $columnId
							));
							if (\Bitrix\Main\Loader::includeModule('pull'))
							{
								\Bitrix\Pull\Event::add($this->getMembersTask($data), [
									'module_id' => 'tasks',
									'command' => 'stage_change',
									'params' => [
										'taskId' => $data['id']
									]
								]);
							}
						}
					}

					if (!$groupAction)
					{
						$rows = $this->getData(
							['ID' => $taskId],
							true
						);
						if ($rows)
						{
							$rows = array_shift($rows);
							// additional flag, if user can't see this task by his filter
							if (!$this->getData(['ID' => $taskId]))
							{
								$rows['data']['hiddenByFilter'] = true;
							}
							return $rows;
						}
					}
				}
			}
		}

		return array();
	}

	/**
	 * Get new task info.
	 * @return array
	 */
	protected function actionNewTask()
	{
		if (($taskId = $this->request('taskId')))
		{
			$this->addFilter('ID', $taskId);
			$data = $this->getData();
			if ($data)
			{
				return array_pop($data);
			}
		}

		return array();
	}

	/**
	 * Just refresh task info.
	 * @return array
	 */
	protected function actionRefreshTask()
	{
		if (($taskId = $this->request('taskId')))
		{
			if ($this->arParams['TIMELINE_MODE'] == 'Y')
			{
				$rows = $this->getData(
					['ID' => $taskId],
					true
				);
				if ($rows)
				{
					return array_shift($rows);
				}
			}
			else
			{
				$rows = $this->getData(['ID' => $taskId]);
				$data = array_shift($rows);
				$data['columns'] = $this->getStages($taskId);
				return $data;
			}
		}

		return array();
	}

	/**
	 * Get items from one column.
	 * @return array
	 */
	protected function actionGetColumnItems()
	{
		if (($columnId = $this->request('columnId')))
		{
			$this->addFilter('ONLY_STAGE_ID', $columnId);
			if (($pageId = $this->request('pageId')))
			{
				$this->setPageId($pageId);
			}
			return $this->getData();
		}
		return array();
	}

	/**
	 * Modify stage in Kanban.
	 * @return array
	 */
	protected function actionModifyColumn()
	{
		if ($this->arParams['TIMELINE_MODE'] == 'Y')
		{
			$this->addError('TASK_LIST_TASK_ACTION_DENIED');
			return [];
		}

		$fields = $this->request('fields');
		if (is_array($fields))
		{
			if ($this->isAdmin())
			{
				// check rights
				if (isset($fields['id']))
				{
					$stages = StagesTable::getStages($this->arParams['STAGES_ENTITY_ID']);
					if (!isset($stages[$fields['id']]))
					{
						unset($fields['id']);
					}
				}
				// delete
				if (
					isset($fields['id']) &&
					isset($fields['delete']) &&
					$fields['delete']
				)
				{
					if ($this->arParams['PERSONAL'] == 'Y')
					{
						$stageId = StagesTable::getStageIdByCode(
							$fields['id'],
							$this->arParams['STAGES_ENTITY_ID']
						);
						$rows = TaskStageTable::getList(array(
							'filter' => array(
								'STAGE_ID' => $stageId,
								'=STAGE.ENTITY_TYPE' => StagesTable::WORK_MODE_USER,
								'STAGE.ENTITY_ID' => $this->arParams['STAGES_ENTITY_ID']
							)
						))->fetch();
						// check for old refs
						if (!empty($rows))
						{
							$rows = $this->getList(array(
								'select' => array(
									'ID'
								),
								'navigate' => array(
									'NAV_PARAMS' => array(
										'nTopCount' => 1
									)
								),
								'filter' => array(
									'STAGES_ID' => $stageId,
									'MEMBER' => $this->arParams['STAGES_ENTITY_ID']
								)
							));
						}
					}
					else
					{
						$rows = $this->getList(array(
							'select' => array(
								'ID'
							),
							'navigate' => array(
								'NAV_PARAMS' => array(
									'nTopCount' => 1
								)
							),
							'filter' => array(
								'STAGE_ID' => StagesTable::getStageIdByCode(
									$fields['id'],
									$this->arParams['STAGES_ENTITY_ID']
								),
								'GROUP_ID' => $this->arParams['GROUP_ID'],
								'CHECK_PERMISSIONS' => 'N'
							)
						));
					}
					if (empty($rows))
					{
						$res = StagesTable::delete(
							$fields['id'],
							$this->arParams['STAGES_ENTITY_ID']
						);
						if (!$res->isSuccess())
						{
							$this->copyError($res->getErrors());
						}
					}
					else
					{
						$this->addError('TASK_LIST_COLUMN_NOT_EMPTY');
					}
				}
				// add / update
				else
				{
					if (isset($fields['columnName']) && trim($fields['columnName']) != '')
					{
						$fields = array(
							'ID' => isset($fields['id']) ? (int)$fields['id'] : 0,
							'COLOR' => isset($fields['columnColor']) ? $fields['columnColor'] : '',
							'TITLE' => $this->convertUtf($fields['columnName'], true),
							'AFTER_ID' => isset($fields['afterColumnId']) ? $fields['afterColumnId'] : null,
							'ENTITY_ID' => $this->arParams['STAGES_ENTITY_ID']
						);
						if ($fields['AFTER_ID'] === null)
						{
							unset($fields['AFTER_ID']);
						}
						$res = StagesTable::updateByCode($fields['ID'], $fields);
						if ($fields['ID'] == 0 && $res && $res->isSuccess())
						{
							$columns = $this->getColumns(true);
							return $columns[$res->getId()];
						}
					}
					else
					{
						$this->addError('TASK_LIST_COLUMN_TITLE_EMPTY');
					}
				}
			}
			else
			{
				$this->addError('TASK_LIST_TASK_ACTION_DENIED');
			}
		}
		return array();
	}

	/**
	 * Move column.
	 * @return array
	 */
	protected function actionMoveColumn()
	{
		if ($this->arParams['TIMELINE_MODE'] == 'Y')
		{
			$this->addError('TASK_LIST_TASK_ACTION_DENIED');
		}
		else if ($this->isAdmin() && ($columnId = $this->request('columnId')))
		{
			$afterColumnId = $this->request('afterColumnId');
			StagesTable::updateByCode($columnId, array(
				'AFTER_ID' => $afterColumnId,
				'ENTITY_ID' => $this->arParams['STAGES_ENTITY_ID']
			));
		}
		else
		{
			$this->addError('TASK_LIST_TASK_ACTION_DENIED');
		}

		return array();
	}

	/**
	 * Apply filter and restart.
	 * @return array
	 */
	protected function actionApplyFilter()
	{
		// filter will be applied in the getList
		return array(
			'columns' => $this->getColumns(),
			'items' => $this->getData()
		);
	}

	/**
	 * Set client date and time.
	 * @return array
	 */
	protected function actionSetClientDate()
	{
		$clientDate = $this->request('clientDate');
		$clientTime = $this->request('clientTime');

		// calc ttl for this data
		$clientTimeTS = \MakeTimeStamp($clientTime);
		$clientTimeTTL = time() +
						 (24 - 1 - date('H', $clientTimeTS)) * 3600 +
						 (60 - date('i', $clientTimeTS)) * 60 + 60;

		// save in session
		$this->setSessionData('clientDate', $clientDate);
		$this->setSessionData('clientTime', $clientTime);
		$this->setSessionData('clientTimeTTL', $clientTimeTTL);
		$this->setSessionData('clientTimeStored', time());

		// set for timeline stages
		TimeLineTable::setDateClient(
			$this->request('clientDate')
		);
		TimeLineTable::setDateTimeClient(
			$this->request('clientTime')
		);

		return array(
			'columns' => $this->getColumns(),
			'items' => $this->getData()
		);
	}

	/**
	 * Change group and restart.
	 * @return array
	 */
	protected function actionChangeGroup()
	{
		// remember group and filter will be applied in the getList
		if ($this->arParams['GROUP_ID'] > 0)
		{
			\Bitrix\Socialnetwork\WorkgroupViewTable::set(array(
				'GROUP_ID' => $this->arParams['GROUP_ID'],
				'USER_ID' => $this->userId
			));
		}
		$isAdmin = $this->isAdmin();
		return array(
			'columns' => $this->getColumns(),
			'items' => $this->getData(),
			'canAddColumn' => $isAdmin,
			'canEditColumn' => $isAdmin,
			'canRemoveColumn' => $isAdmin,
			'canAddItem' => $this->canCreateTasks(),
			'canSortItem' => $this->canSortTasks(),
			'newTaskOrder' => $this->getNewTaskOrder(),
			'admins' => !$isAdmin ? array_values($this->getAdmins()) : array()
		);
	}

	/**
	 * Change demo view to other.
	 * @return array
	 */
	protected function actionChangeDemoView()
	{
		$viewId = (int)$this->request('viewId');

		return array(
			'columns' => $this->canReadGroupTasks($viewId)
						? $this->getDemoColumns($viewId)
						: array(),
			'items' => array(),
			'demo' => true
		);
	}

	/**
	 * Notify admin for get access.
	 * @return array
	 */
	protected function actionNotifyAdmin()
	{
		if (
			($userId = $this->request('userId')) &&
			Loader::includeModule('im')
		)
		{
			$admins = $this->getAdmins();
			if (isset($admins[$userId]))
			{
				$params = $this->arParams;
				$groupId = $params['GROUP_ID'];
				\CIMNotify::Add(array(
					'TO_USER_ID' => $userId,
					'FROM_USER_ID' => $this->userId,
					'NOTIFY_TYPE' => IM_NOTIFY_FROM,
					'NOTIFY_MODULE' => 'tasks',
					'NOTIFY_TAG' => 'TASKS|NOTIFY_ADMIN|'.$userId.'|'.$this->userId,
					'NOTIFY_MESSAGE' => Loc::getMessage('TASK_ACCESS_NOTIFY_MESSAGE', array(
						'#URL#' => $groupId > 0
									? str_replace('#group_id#', $groupId, $params['~PATH_TO_GROUP_TASKS'])
									: $params['~PATH_TO_TASKS']
					))
				));
			}
		}
		return array(
			'status' => 'success'
		);
	}

	/**
	 * Set order for new task (desc, asc).
	 * @return array
	 */
	protected function actionSetNewTaskOrder()
	{
		$order = $this->request('order');
		$groupId = $this->arParams['GROUP_ID'];

		if (
			($order = $this->request('order')) &&
			in_array($order, ['asc', 'desc', 'actual']) &&
			$this->canSortTasks()
		)
		{
			if ($groupId > 0 && $this->arParams['PERSONAL'] != 'Y')
			{
				ProjectsTable::set($groupId, array(
					'ORDER_NEW_TASK' => $order
				));
			}
			else
			{
				\CUserOptions::setOption('tasks', 'order_new_task_v2', $order);
			}
		}

		return array(
			'newTaskOrder' => $this->getNewTaskOrder()
		);
	}

	/**
	 * Delegate task to other.
	 * @return array
	 */
	protected function actionDelegateTask()
	{
		if (
			($taskId = $this->request('taskId')) &&
			($responsible = $this->request('userId'))
		)
		{
			$groupAction = $this->request('groupAction') == 'Y';
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				$task = \CTaskItem::getInstance($taskId, $this->userId);
				$taskData = $task->getData();

				if (
					$taskData['CREATED_BY'] == $this->userId
					&& $task->checkAccess(ActionDictionary::ACTION_TASK_EDIT)
				)
				{
					$task->update(array(
						'RESPONSIBLE_ID' => $responsible
					));
				}
				elseif ($task->checkAccess(ActionDictionary::ACTION_TASK_DELEGATE, \Bitrix\Tasks\Access\Model\TaskModel::createFromId((int) $taskId)))
				{
					$task->delegate($responsible);
				}
				elseif ($task->checkAccess(ActionDictionary::ACTION_TASK_EDIT))
				{
					$task->update(array(
						'RESPONSIBLE_ID' => $responsible
					));
				}

				//tmp, bug #85959
				if (false && ($e = $this->application->GetException()))
				{
					$this->addError($e->getString());
				}
				else if (!$groupAction)
				{
					return $this->getRawData($taskId, $this->request('columnId'));
				}
			}
		}

		return array();
	}

	/**
	 * Provides some actions with task members.
	 * @param string Sub action code.
	 * @return array
	 */
	protected function addMemberTask($subAction)
	{
		if (
			($taskId = $this->request('taskId')) &&
			($userId = $this->request('userId'))
		)
		{
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				$task = \CTaskItem::getInstance($taskId, $this->userId);
				if ($subAction == 'addAccomplice')
				{
					if ($task->isActionAllowed(\CTaskItem::ACTION_EDIT))
					{
						\CTasks::addAccomplices($taskId, [$userId]);
					}
				}
				else if ($subAction == 'addAuditor')
				{
					\CTasks::addAuditors($taskId, [$userId]);
				}
			}
		}
		return [];
	}

	/**
	 * Provides action to add accomplice for task.
	 * @return array
	 */
	protected function actionAddAccompliceTask()
	{
		return $this->addMemberTask('addAccomplice');
	}

	/**
	 * Provides action to add auditor for task.
	 * @return array
	 */
	protected function actionAddAuditorTask()
	{
		return $this->addMemberTask('addAuditor');
	}

	/**
	 * Provides action to add to favorite task or to remove from.
	 * @param string Sub action code.
	 * @return array
	 */
	protected function favoriteTask($subAction)
	{
		if ($taskId = $this->request('taskId'))
		{
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				$task = \CTaskItem::getInstance($taskId, $this->userId);
				if ($subAction == 'add')
				{
					$task->addToFavorite();
				}
				else if ($subAction == 'delete')
				{
					$task->deleteFromFavorite();
				}
			}
		}
		return [];
	}

	/**
	 * Provides action to add to favorite task.
	 * @return array
	 */
	protected function actionAddFavoriteTask()
	{
		return $this->favoriteTask('add');
	}

	/**
	 * Provides action to delete from favorite task.
	 * @return array
	 */
	protected function actionDeleteFavoriteTask()
	{
		return $this->favoriteTask('delete');
	}

	/**
	 * Delegate author's rights of task to other.
	 * @return array
	 */
	protected function actionChangeAuthorTask()
	{
		if (
			($taskId = $this->request('taskId')) &&
			($author = $this->request('userId'))
		)
		{
			$groupAction = $this->request('groupAction') == 'Y';
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				$task = \CTaskItem::getInstance($taskId, $this->userId);
				if ($task->checkAccess(ActionDictionary::ACTION_TASK_EDIT))
				{
					try
					{
						$task->update(array('CREATED_BY' => $author));
					}
					catch (\TasksException $e)
					{
						if (!$groupAction)
						{
							$this->addError($e->getMessageOrigin());
							return array();
						}
					}
				}
				//tmp, bug #85959
				if (false && ($e = $this->application->GetException()))
				{
					$this->addError($e->getString());
				}
				else if (!$groupAction)
				{
					return $this->getRawData($taskId, $this->request('columnId'), false);
				}
			}
		}

		return array();
	}

	/**
	 * Set deadline to the task.
	 * @return array
	 */
	protected function actionDeadlineTask()
	{
		if (
			($taskId = $this->request('taskId')) &&
			($deadline = $this->request('deadline'))
		)
		{
			$groupAction = $this->request('groupAction') == 'Y';
			$deadlineDT = new DateTime($deadline);
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				$task = \CTaskItem::getInstance($taskId, $this->userId);
				if ($task->checkAccess(ActionDictionary::ACTION_TASK_DEADLINE))
				{
					$update = array(
						'DEADLINE' => $deadline
					);
					$fields = $task->getData();
					// if date start great then deadline
					if ($fields['START_DATE_PLAN'] != '')
					{
						$fields['START_DATE_PLAN'] = new DateTime($fields['START_DATE_PLAN']);
						if ($fields['START_DATE_PLAN']->getTimestamp() >= $deadlineDT->getTimestamp())
						{
							$update['START_DATE_PLAN'] = DateTime::createFromTimestamp($deadlineDT->getTimestamp() - $this->timeOffset - 3600);
						}
					}
					// update
					try
					{
						$task->update($update);
					}
					catch (\TasksException $e)
					{
						if (!$groupAction)
						{
							$message = $e->getMessage();
							$data = current(unserialize($message));
							$this->addError($data['text']);
							return array();
						}
					}
				}
				else if (!$groupAction)
				{
					$this->addError('TASK_LIST_ERROR_CHANGE_DEADLINE');
					return array();
				}

				/*if (($e = $this->application->GetException()))
				{
					$this->addError($e->getString());
					return array();
				}*/

				if (!$groupAction)
				{
					if ($this->arParams['TIMELINE_MODE'] == 'Y')
					{
						$rows = $this->getData(
							['ID' => $taskId]
						);
						if ($rows)
						{
							return array_shift($rows);
						}
					}
					else
					{
						return $this->getRawData($taskId, $this->request('columnId'));
					}
				}
			}
		}

		return array();
	}

	/**
	 * Changes task group.
	 * @return array
	 */
	protected function actionChangeGroupTask()
	{
		if (
			($taskId = $this->request('taskId')) &&
			($groupId = $this->request('groupId')) &&
			Loader::includeModule('socialnetwork')
		)
		{
			$userInGroup = \CSocNetUserToGroup::getUserRole(
				$this->userId,
				$groupId
			) !== false;
			if (!$userInGroup)
			{
				return [];
			}
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				$task = \CTaskItem::getInstance($taskId, $this->userId);
				if ($task->isActionAllowed(\CTaskItem::ACTION_EDIT))
				{
					$task->update([
						'GROUP_ID' => $groupId
					]);
				}
			}
		}

		return [];
	}

	/**
	 * Deletes task.
	 * @return array
	 */
	protected function actionDeleteTask()
	{
		if ($taskId = $this->request('taskId'))
		{
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				$task = \CTaskItem::getInstance($taskId, $this->userId);
				if ($task->isActionAllowed(\CTaskItem::ACTION_REMOVE))
				{
					$task->delete();
				}
			}
		}

		return [];
	}

	/**
	 * Group similar actions.
	 * @param string $code Code of action.
	 * @return array
	 */
	protected function actionsSimilar($code)
	{
		if (($taskId = $this->request('taskId')))
		{
			$groupAction = $this->request('groupAction') == 'Y';
			$taskIds = (array) $taskId;
			foreach ($taskIds as $taskId)
			{
				switch ($code)
				{
					case 'start':
					case 'pause':
						$task = \CTaskItem::getInstance($taskId, $this->userId);
						if ($task->checkAccess($code == 'start' ? ActionDictionary::ACTION_TASK_START : ActionDictionary::ACTION_TASK_PAUSE))
						{
							if ($code == 'start')
							{
								$task->startExecution();
							}
							else
							{
								$task->pauseExecution();
							}
						}
						break;
					case 'complete':
						$this->completeTask($taskId);
						break;
					case 'mute':
						UserOption::add($taskId, $this->userId, UserOption\Option::MUTED);
						break;
					case 'unmute':
						UserOption::delete($taskId, $this->userId, UserOption\Option::MUTED);
						break;
				}
				//tmp, bug #85959
				if (false && ($e = $this->application->GetException()))
				{
					$this->addError($e->getString());
				}
				else if (!$groupAction)
				{
					return $this->getRawData($taskId, $this->request('columnId'));
				}
			}
		}

		return array();
	}

	private function completeTask(int $taskId)
	{
		try
		{
			$task = \CTaskItem::getInstance($taskId, $this->userId);
			if ($task->checkAccess(ActionDictionary::ACTION_TASK_COMPLETE) ||
				$task->checkAccess(ActionDictionary::ACTION_TASK_APPROVE))
			{
				$task->complete();
			}
		}
		catch(\Exception $exception)
		{
			$this->addError($exception->getMessage());
		}
	}

	private function renewTask(int $taskId)
	{
		try
		{
			$task = \CTaskItem::getInstance($taskId, $this->userId);
			if ($task->checkAccess(ActionDictionary::ACTION_TASK_RENEW) ||
				$task->checkAccess(ActionDictionary::ACTION_TASK_APPROVE))
			{
				$queryObject = \CTasks::getList(
					[],
					['ID' => $taskId, '=STATUS' => \CTasks::STATE_COMPLETED],
					['ID'],
					['USER_ID' => $this->userId]
				);
				if ($queryObject->fetch())
				{
					$task->renew();
				}
			}
		}
		catch(\Exception $exception)
		{
			$this->addError($exception->getMessage());
		}
	}

	/**
	 * Start task exection.
	 * @return array
	 */
	protected function actionStartTask()
	{
		return $this->actionsSimilar('start');
	}

	/**
	 * Pause task exection.
	 * @return array
	 */
	protected function actionPauseTask()
	{
		return $this->actionsSimilar('pause');
	}

	/**
	 * Mute task.
	 * @return array
	 */
	protected function actionMuteTask(): array
	{
		return $this->actionsSimilar('mute');
	}

	/**
	 * Unmute task.
	 * @return array
	 */
	protected function actionUnmuteTask(): array
	{
		return $this->actionsSimilar('unmute');
	}

	/**
	 * Complete the task.
	 * @return array
	 */
	protected function actionCompleteTask()
	{
		return $this->actionsSimilar('complete');
	}

	/**
	 * Converter tasks of My Plan.
	 * @return array
	 */
	protected function actionConverterMP()
	{
		$params = $this->arParams;

		if ($params['PERSONAL'] == 'Y')
		{
			$lastId = $this->request('last');
			$checkStages = StagesTable::getStages(
				$params['STAGES_ENTITY_ID'],
				true
			);
			[$rows, $res] = $this->getList(array(
				'select' => array(
					'ID'
				),
				'navigate' => array(
					'NAV_PARAMS' => array(
						'nTopCount' => 50
					)
				),
				'filter' => array(
					'MEMBER' => $params['STAGES_ENTITY_ID'],
					'>ID' => $lastId
				),
				'order' => array(
					'ID' => 'ASC'
				)
			), true);
			if (!empty($rows))
			{
				$defaultStageId = StagesTable::getDefaultStageId(
					$params['STAGES_ENTITY_ID']
				);
				// check for each task that it exists in TaskStageTable
				$ids = array();
				$checkTS = array();
				foreach ($rows as $row)
				{
					$ids[] = $row['ID'];
				}
				$resTS = TaskStageTable::getList(array(
					'filter' => array(
						'STAGE_ID' => array_keys($checkStages),
						'TASK_ID' => $ids
					)
				));
				while ($rowTS = $resTS->fetch())
				{
					if (!isset($checkTS[$rowTS['TASK_ID']]))
					{
						$checkTS[$rowTS['TASK_ID']] = array();
					}
					$checkTS[$rowTS['TASK_ID']][] = $rowTS['ID'];
				}
				foreach ($rows as $row)
				{
					// if double exists, remove all excepts last
					if (is_array($checkTS[$row['ID']]) && count($checkTS[$row['ID']]) > 1)
					{
						array_pop($checkTS[$row['ID']]);
						foreach ($checkTS[$row['ID']] as $tsId)
						{
							TaskStageTable::delete($tsId);
						}
					}
					// not exists in any stage - add in default
					elseif (!isset($checkTS[$row['ID']]))
					{
						TaskStageTable::add(array(
							'STAGE_ID' => $defaultStageId,
							'TASK_ID' => $row['ID']
						));
					}
					$lastId = $row['ID'];
				}
				$finish = false;
			}
			else
			{
				$finish = true;
			}
			// flag that all right
			if ($finish)
			{
				\CUserOptions::setOption(
					'tasks',
					'personal_kanban_version',
					StagesTable::MY_PLAN_VERSION,
					false,
					$params['STAGES_ENTITY_ID']
				);
			}

			return array(
				'processed' => count($rows),
				'last' => $lastId,
				'finish' => $finish
			);
		}
	}
}