/
vchechun
/
task_checklist_export
Обзор
Документация
Войти
/
vchechun
/
task_checklist_export
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/CustomFields.php
512 строк
11 KB
Vsevolod Chechun
init
07 май 2026, 07:31
07 май 2026, 07:31
fde5953
Код
Авторство
О чём код?
<?php namespace BitrixExt\TaskChecklistExport; use Bitrix\Main\Application; use Bitrix\Main\Loader; final class CustomFields { private const ENTITY_ID = 'TASKS_TASK'; private const OPTION_MODULE = 'main'; private const OPTION_FIELDS_VERSION = 'task_checklist_export_fields_version'; private const OPTION_AGENT_INSTALLED = 'task_checklist_export_agent_installed'; private const OPTION_AGENT_LAST_ID = 'task_checklist_export_agent_last_id'; private const OPTION_BOOTSTRAP_TS = 'task_checklist_export_bootstrap_ts'; private const AGENT_LIMIT = 50; private const BOOTSTRAP_LIMIT = 100; private const BOOTSTRAP_INTERVAL = 30; private static array $pendingTaskIds = []; private static array $pendingCheckListIds = []; private const FIELDS = [ 'UF_TASK_CHECKLIST_SUMMARY' => [ 'label' => 'Чек-лист', 'sort' => 510, ], 'UF_TASK_EXECUTOR_COMPLETED_DATE' => [ 'label' => 'Дата завершения (исполнителем)', 'sort' => 520, ], 'UF_TASK_ACCEPTED_DATE' => [ 'label' => 'Дата принятия (постановщиком)', 'sort' => 530, ], ]; public static function onProlog(): void { self::ensureInstalled(); self::refreshOpenedTask(); self::captureCheckListRequestIds(); self::runBootstrapBackfill(); } public static function onTaskChange($taskId = null): void { $taskId = (int)$taskId; if ($taskId <= 0) { return; } self::ensureInstalled(); self::refreshTask($taskId); } public static function onAfterEpilog(): void { if (!self::isCheckListRequest()) { return; } $taskIds = array_merge( self::$pendingTaskIds, self::extractTaskIds($_REQUEST) ); $checkListIds = array_merge( self::$pendingCheckListIds, self::extractCheckListIds($_REQUEST) ); if (!empty($checkListIds)) { $taskIds = array_merge($taskIds, self::getTaskIdsByCheckListIds($checkListIds)); } $taskIds = array_values(array_unique(array_filter(array_map('intval', $taskIds)))); if (!empty($taskIds)) { self::refreshTasks($taskIds); } } public static function refreshTask(int $taskId): void { if ($taskId <= 0 || !Loader::includeModule('tasks')) { return; } self::refreshTasks([$taskId]); } public static function refreshTasks(array $taskIds): void { $taskIds = array_values(array_unique(array_filter(array_map('intval', $taskIds)))); if (empty($taskIds) || !Loader::includeModule('tasks')) { return; } $summaries = ChecklistSummary::getFormattedByTaskIds($taskIds); $dates = TaskDates::getFormattedByTaskIds($taskIds); global $USER_FIELD_MANAGER; if (!is_object($USER_FIELD_MANAGER) || !method_exists($USER_FIELD_MANAGER, 'Update')) { return; } foreach ($taskIds as $taskId) { $USER_FIELD_MANAGER->Update(self::ENTITY_ID, $taskId, [ 'UF_TASK_CHECKLIST_SUMMARY' => $summaries[$taskId] ?? '0', 'UF_TASK_EXECUTOR_COMPLETED_DATE' => ($dates[$taskId]['executorCompletedDate'] ?? '') ?: '-', 'UF_TASK_ACCEPTED_DATE' => ($dates[$taskId]['acceptedDate'] ?? '') ?: '-', ]); } } public static function runAgent(): string { self::ensureInstalled(); self::runBackfillBatch(self::AGENT_LIMIT); return self::getAgentName(); } private static function ensureInstalled(): void { if (!Loader::includeModule('tasks')) { return; } self::ensureUserFields(); self::ensureAgent(); } private static function ensureUserFields(): void { if ((int)\COption::GetOptionString(self::OPTION_MODULE, self::OPTION_FIELDS_VERSION, '0') >= 2) { return; } $existing = self::getExistingUserFields(); $userTypeEntity = new \CUserTypeEntity(); foreach (self::FIELDS as $fieldName => $config) { if (isset($existing[$fieldName])) { $userTypeEntity->Update((int)$existing[$fieldName]['ID'], [ 'SORT' => $config['sort'], 'EDIT_FORM_LABEL' => self::makeLanguageLabels($config['label']), 'LIST_COLUMN_LABEL' => self::makeLanguageLabels($config['label']), 'LIST_FILTER_LABEL' => self::makeLanguageLabels($config['label']), ]); continue; } $userTypeEntity->Add([ 'ENTITY_ID' => self::ENTITY_ID, 'FIELD_NAME' => $fieldName, 'USER_TYPE_ID' => 'string', 'XML_ID' => $fieldName, 'SORT' => $config['sort'], 'MULTIPLE' => 'N', 'MANDATORY' => 'N', 'SHOW_FILTER' => 'N', 'SHOW_IN_LIST' => 'Y', 'EDIT_IN_LIST' => 'N', 'IS_SEARCHABLE' => 'N', 'EDIT_FORM_LABEL' => self::makeLanguageLabels($config['label']), 'LIST_COLUMN_LABEL' => self::makeLanguageLabels($config['label']), 'LIST_FILTER_LABEL' => self::makeLanguageLabels($config['label']), 'SETTINGS' => [ 'SIZE' => 40, 'ROWS' => 1, ], ]); } $existing = self::getExistingUserFields(); $allInstalled = true; foreach (array_keys(self::FIELDS) as $fieldName) { if (!isset($existing[$fieldName])) { $allInstalled = false; break; } } if ($allInstalled) { \COption::SetOptionString(self::OPTION_MODULE, self::OPTION_FIELDS_VERSION, '2'); } } private static function ensureAgent(): void { if (\COption::GetOptionString(self::OPTION_MODULE, self::OPTION_AGENT_INSTALLED, 'N') === 'Y') { return; } if (class_exists(\CAgent::class)) { \CAgent::AddAgent(self::getAgentName(), '', 'N', 60); } \COption::SetOptionString(self::OPTION_MODULE, self::OPTION_AGENT_INSTALLED, 'Y'); } private static function runBootstrapBackfill(): void { $lastRun = (int)\COption::GetOptionString(self::OPTION_MODULE, self::OPTION_BOOTSTRAP_TS, '0'); if ($lastRun > 0 && (time() - $lastRun) < self::BOOTSTRAP_INTERVAL) { return; } \COption::SetOptionString(self::OPTION_MODULE, self::OPTION_BOOTSTRAP_TS, (string)time()); self::runBackfillBatch(self::BOOTSTRAP_LIMIT); } private static function runBackfillBatch(int $limit): void { if (!Loader::includeModule('tasks')) { return; } $lastId = (int)\COption::GetOptionString(self::OPTION_MODULE, self::OPTION_AGENT_LAST_ID, '0'); $taskIds = self::getNextTaskIds($lastId, $limit); if (empty($taskIds) && $lastId > 0) { $lastId = 0; $taskIds = self::getNextTaskIds($lastId, $limit); } if (empty($taskIds)) { return; } self::refreshTasks($taskIds); \COption::SetOptionString(self::OPTION_MODULE, self::OPTION_AGENT_LAST_ID, (string)max($taskIds)); } private static function refreshOpenedTask(): void { $taskId = self::getOpenedTaskId(); if ($taskId <= 0) { return; } self::refreshTask($taskId); } private static function getOpenedTaskId(): int { $path = (string)parse_url((string)($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH); if ($path === '') { return 0; } if (preg_match('#/tasks/task/(?:view|edit)/(\d+)(?:/|$)#i', $path, $matches)) { return (int)$matches[1]; } return 0; } private static function captureCheckListRequestIds(): void { if (!self::isCheckListRequest()) { return; } $payload = self::getRequestPayload(); self::$pendingTaskIds = array_values(array_unique(array_merge( self::extractTaskIds($_REQUEST), self::extractTaskIds($payload) ))); self::$pendingCheckListIds = array_values(array_unique(array_merge( self::extractCheckListIds($_REQUEST), self::extractCheckListIds($payload) ))); } private static function isCheckListRequest(): bool { $action = mb_strtolower( (string)($_REQUEST['action'] ?? '') . ' ' . (string)($_REQUEST['c'] ?? '') ); $normalizedAction = str_replace(['.', '_', '-', ':'], '', $action); return $normalizedAction !== '' && mb_strpos($normalizedAction, 'checklist') !== false; } private static function getRequestPayload(): array { $raw = (string)file_get_contents('php://input'); if ($raw === '') { return []; } $data = json_decode($raw, true); if (is_array($data)) { return $data; } parse_str($raw, $data); return is_array($data) ? $data : []; } private static function getExistingUserFields(): array { $result = []; $rows = \CUserTypeEntity::GetList([], [ 'ENTITY_ID' => self::ENTITY_ID, ]); while ($field = $rows->Fetch()) { $result[(string)$field['FIELD_NAME']] = $field; } return $result; } private static function makeLanguageLabels(string $label): array { $languages = ['ru', 'en']; $result = []; foreach ($languages as $languageId) { $result[$languageId] = $label; } if (defined('LANGUAGE_ID')) { $result[LANGUAGE_ID] = $label; } return $result; } private static function getNextTaskIds(int $lastId, int $limit): array { $connection = Application::getConnection(); $lastId = max(0, $lastId); $limit = max(1, $limit); $rows = $connection->query(" SELECT ID FROM b_tasks WHERE ID > {$lastId} ORDER BY ID ASC LIMIT {$limit} "); $taskIds = []; while ($row = $rows->fetch()) { $taskIds[] = (int)$row['ID']; } return $taskIds; } private static function extractTaskIds(array $source, array $path = []): array { $result = []; foreach ($source as $key => $value) { $key = (string)$key; $upperKey = mb_strtoupper($key); $currentPath = array_merge($path, [mb_strtolower($key)]); if (is_array($value)) { $result = array_merge($result, self::extractTaskIds($value, $currentPath)); continue; } if ( in_array($upperKey, ['TASK_ID', 'TASKID', 'ENTITY_ID'], true) && (int)$value > 0 ) { $result[] = (int)$value; continue; } if ( $upperKey === 'ID' && (int)$value > 0 && self::pathContains($currentPath, ['task', 'taskdata']) ) { $result[] = (int)$value; } } return array_values(array_unique($result)); } private static function extractCheckListIds(array $source, array $path = []): array { $result = []; foreach ($source as $key => $value) { $key = (string)$key; $upperKey = mb_strtoupper($key); $currentPath = array_merge($path, [mb_strtolower($key)]); if (is_array($value)) { $result = array_merge($result, self::extractCheckListIds($value, $currentPath)); continue; } if ( in_array($upperKey, ['CHECKLIST_ID', 'CHECKLISTID', 'CHECK_LIST_ID', 'CHECKLISTITEMID', 'CHECK_LIST_ITEM_ID'], true) && (int)$value > 0 ) { $result[] = (int)$value; continue; } if ( $upperKey === 'ID' && (int)$value > 0 && self::pathContains($currentPath, ['checklist', 'checklistitem', 'checklistitems']) ) { $result[] = (int)$value; } } return array_values(array_unique($result)); } private static function pathContains(array $path, array $needles): bool { foreach ($path as $part) { $normalized = str_replace(['_', '-', '.'], '', (string)$part); foreach ($needles as $needle) { if ($normalized === $needle) { return true; } } } return false; } private static function getTaskIdsByCheckListIds(array $checkListIds): array { $checkListIds = array_values(array_unique(array_filter(array_map('intval', $checkListIds)))); if (empty($checkListIds)) { return []; } $connection = Application::getConnection(); $ids = implode(',', $checkListIds); $rows = $connection->query(" SELECT DISTINCT TASK_ID FROM b_tasks_checklist_items WHERE ID IN ({$ids}) "); $taskIds = []; while ($row = $rows->fetch()) { $taskIds[] = (int)$row['TASK_ID']; } return array_values(array_unique(array_filter($taskIds))); } private static function getAgentName(): string { return '\\' . self::class . '::runAgent();'; } }