привет, у меня есть битрикс24 в коробке в нем есть...
Создано: 7 апреля 2025 г.
Создано: 7 апреля 2025 г.
привет, у меня есть битрикс24 в коробке в нем есть отчеты пользователей, хоется для начала кастометезировать карточку отчета со стороны руководителя нам надо туда добавить еще одно свойство
оценка загруженности (от 1 до 5)
вот примерные данные отчеты все на компоненте:bitrix:timeman.report.weekly: 0.019 с
но нас интерисует именно детальная карточка отчета (пока не нашел где ее править)
Вот нашел где идет ответ по ее постронению:bitrix/modules/timeman/tools но возможно не туда копаю.
case 'admin_report_full':
$bReturnRes = true;
text$ID = intval($_POST["report_id"] ?? 0); $USER_ID = intval($_POST["user_id"] ?? 0); $OBJID = intval($_POST["id"] ?? 0); $arAccessUsers = CTimeMan::GetAccess(); $bCanEditAll = in_array('*', $arAccessUsers['WRITE']); $bCanReadAll = in_array('*', $arAccessUsers['READ']); $bCanReadUser = (in_array($USER_ID, $arAccessUsers["READ"]) || $bCanReadAll); $bCanWriteUser = (in_array($USER_ID, $arAccessUsers["WRITE"]) || $bCanEditAll); if ($USER_ID > 0 && $bCanWriteUser && ($_POST["approve"] ?? null)) { if ($_POST["approve"] == "Y") { $dbrep = CTimeManReportFull::GetByID($ID); $rep = $dbrep->Fetch(); if (is_array($rep) && $USER_ID == $rep['USER_ID']) { $arFields = [ "MARK" => (in_array($_POST["mark"], ["G", "B", "N"]) !== false) ? $_POST["mark"] : "X", ]; if ($arFields["MARK"] != "X") { $arFields["APPROVER"] = $USER->GetID(); $arFields["APPROVE"] = "Y"; $arFields["APPROVE_DATE"] = ConvertTimeStamp(time(), "FULL"); } else { $arFields["APPROVE"] = "N"; $arFields["APPROVER"] = 0; $arFields["APPROVE_DATE"] = ""; } CTimeManReportFull::Update($ID, $arFields); $CACHE_MANAGER->Clean(CUserReportFull::getInfoCacheId($USER_ID), 'timeman_report_info'); CReportNotifications::MessageUpdate($ID, $rep, $arFields); } } } elseif (($_POST["mode"] ?? null) && $OBJID > 0) { $arFields = [ "UF_REPORT_PERIOD" => $_POST["mode"], "UF_TM_TIME" => (IsAmPmMode() ? convertTimeToMilitary($_POST["time"], 'H:MI T', 'HH:MI') : $_POST["time"]), "UF_TM_REPORT_DATE" => $_POST["date"], "UF_TM_DAY" => $_POST["day"], "ID" => $OBJID, ]; if ($_POST["object"] == "user") { $bCanEdit = ((in_array($OBJID, $arAccessUsers["WRITE"]) && $OBJID != $USER->GetID()) || $bCanEditAll || CTimeMan::IsAdmin() ); if ($bCanEdit) { $arReportUser = new CUserReportFull($OBJID); $res = $arReportUser->SetPeriod($arFields); $CACHE_MANAGER->Clean(CReportSettings::getSettingsCacheId($OBJID), 'timeman_report_settings'); $CACHE_MANAGER->Clean(CUserReportFull::getInfoCacheId($OBJID), 'timeman_report_info'); } } elseif ($_POST["object"] == "dep") { $arSubordination = CIntranetUtils::GetSubordinateDepartments($USER->GetID(), true); $bCanEdit = (in_array($OBJID, $arSubordination) || CTimeMan::IsAdmin()); if ($bCanEdit) { $res = CTimeManReportFull::SetPeriodSection($arFields); $CACHE_MANAGER->CleanDir('timeman_report_settings'); $CACHE_MANAGER->CleanDir('timeman_report_info'); } } } if ($bCanReadUser && !($_POST["mode"] ?? null)) { $dbRes = CUser::GetList( 'ID', 'ASC', ['ID' => $USER_ID], ['SELECT' => ['UF_*']] ); $arUser = $dbRes->GetNext(); $arUser['PHOTO'] = $arUser['PERSONAL_PHOTO'] > 0 ? CIntranetUtils::InitImage($arUser['PERSONAL_PHOTO'], 100, 100, BX_RESIZE_IMAGE_EXACT) : []; $arManagers = CTimeMan::GetUserManagers($USER_ID); $arManagers[] = $USER_ID; if (!is_array($arManagers) || count($arManagers) <= 0) { $arManagers = [$USER_ID]; } $user_url = COption::GetOptionString( 'intranet', 'path_user', '/company/personal/user/#USER_ID#/', ($_REQUEST['site_id'] ?? '') ); $dbManagers = CUser::GetList('ID', 'ASC', ['ID' => implode('|', $arManagers)]); $res["TO"] = []; $res["FROM"] = []; while ($manager = $dbManagers->Fetch()) { $manager['PHOTO'] = $manager['PERSONAL_PHOTO'] > 0 ? CIntranetUtils::InitImage($manager['PERSONAL_PHOTO'], 100, 100, BX_RESIZE_IMAGE_EXACT) : []; $arUserData = [ 'ID' => $manager['ID'], 'LOGIN' => $manager['LOGIN'], 'NAME' => CUser::FormatName(CSite::GetNameFormat(false), $manager, true, true), 'URL' => str_replace(['#ID#', '#USER_ID#'], $manager['ID'], $user_url), 'WORK_POSITION' => $manager['WORK_POSITION'], 'PHOTO' => $manager['PHOTO']['CACHE']['src'] ?? '', ]; if ($USER_ID == $arUserData["ID"]) { $res["FROM"] = $arUserData; } else { $res["TO"][] = $arUserData; } } if (count($res["TO"]) == 0) { $res["TO"][] = $res["FROM"]; } $arFilter = ["ID" => $ID, "USER_ID" => $USER_ID]; $arReportID = []; if ($_POST["empty_slider"] ?? null) { $arFilter = ["ACTIVE" => "Y", "USER_ID" => $USER_ID]; } $dbres = CTimeManReportFull::GetList(["USER_ID" => "asc", "ID" => "asc"], $arFilter); $report = null; while ($rep = $dbres->Fetch()) { $arReportID[] = $rep["ID"]; if ($rep["ID"] == $ID) { $report = $rep; } } $res["REPORT_LIST"] = $arReportID; if (is_array($report)) { $res["INFO"] = $report; if ($res["INFO"]['TASKS_ENABLED'] = (CBXFeatures::IsFeatureEnabled('Tasks') && CModule::IncludeModule('tasks'))) { $res["INFO"]['TASKS'] = unserialize($res["INFO"]['TASKS'] ?? '', ['allowed_classes' => false]); } else { unset($res["INFO"]['TASKS']); } if ($res["INFO"]['CALENDAR_ENABLED'] = CBXFeatures::IsFeatureEnabled('Calendar')) { $res["INFO"]['EVENTS'] = unserialize($res["INFO"]['EVENTS'] ?? '', ['allowed_classes' => false]); } else { unset($res["INFO"]['EVENTS']); } if ($res["INFO"]['FILES']) { $res["INFO"]['FILES'] = unserialize($res["INFO"]['FILES'] ?? '', ['allowed_classes' => false]); } $res["INFO"]['CAN_EDIT'] = ($arUser['ID'] != $USER->GetID()) && ($bCanEditAll || in_array($arUser['ID'], $arAccessUsers['WRITE'])); $res["INFO"]['CAN_EDIT_TEXT'] = ($report["APPROVE"] == "Y") ? "N" : "Y"; $culture = \Bitrix\Main\Application::getInstance()->getContext()->getCulture(); $dayMonthFormat = $culture->getDayMonthFormat(); if ($report["DATE_FROM"] != $report["DATE_TO"]) { $res["INFO"]["TEXT_TITLE"] = FormatDate($dayMonthFormat, MakeTimeStamp($report["DATE_FROM"])) . " - " . FormatDate($dayMonthFormat, MakeTimeStamp($report["DATE_TO"])); } else { $res["INFO"]["TEXT_TITLE"] = FormatDate($dayMonthFormat, MakeTimeStamp($report["DATE_TO"])); } $res["INFO"]["REPORT_STRIP_TAGS"] = strip_tags(nl2br($res["INFO"]["REPORT"])); $res["INFO"]["PLAN_STRIP_TAGS"] = strip_tags(nl2br($res["INFO"]["PLANS"])); $res["INFO"]["APPROVER_INFO"] = []; if (intval($res["INFO"]["APPROVER"]) > 0) { $res["INFO"]["APPROVE_DATE"] = FormatDate($DB->DateFormatToPHP(FORMAT_DATETIME), MakeTimeStamp($res["INFO"]["APPROVE_DATE"])); foreach ($res["TO"] as $manager) { if ($manager["ID"] == intval($res["INFO"]["APPROVER"])) { $res["INFO"]["APPROVER_INFO"] = $manager; break; } } if (!$res["INFO"]["APPROVER_INFO"]) { $dbaprrove = CUser::GetList('ID', 'ASC', ['ID' => intval($res["INFO"]["APPROVER"])]); if ($approver = $dbaprrove->Fetch()) { $approver['PHOTO'] = $approver['PERSONAL_PHOTO'] > 0 ? CIntranetUtils::InitImage($approver['PERSONAL_PHOTO'], 100, 100, BX_RESIZE_IMAGE_EXACT) : []; $res["INFO"]["APPROVER_INFO"] = [ 'ID' => $approver['ID'], 'LOGIN' => $approver['LOGIN'], 'NAME' => CUser::FormatName(CSite::GetNameFormat(false), $approver, true, true), 'URL' => str_replace(['#ID#', '#USER_ID#'], $approver['ID'], $user_url), 'WORK_POSITION' => $approver['WORK_POSITION'], 'PHOTO' => $approver['PHOTO']['CACHE']['src'], ]; } } } ob_start(); $APPLICATION->IncludeComponent( "bitrix:timeman.topic.reviews", "", [ "REPORT_ID" => $ID, ], false ); $res["COMMENTS"] = ob_get_contents(); ob_end_clean(); } } break; //carter
Ниже общий план, как можно добавить в «детальную карточку отчёта» новое поле «оценка загруженности», чтобы руководитель мог его выставлять и смотреть:
Создать поле в базе. Например, если вы хотите хранить оценку загруженности, то в таблице b_timeman_report_full
можете добавить столбец вида:
sqlALTER TABLE b_timeman_report_full ADD UF_WORKLOAD_MARK INT DEFAULT 0;
Или если это текстом: VARCHAR(255)
, но для оценки от 1 до 5 подойдёт INT
.
Убедиться, что модуль умеет «видеть» это поле. В коде Bitrix24 иногда нужно вручную подхватить или указать в CReport::GetList
и т.д. В случае с CTimeManReportFull::GetByID()
– зачастую Bitrix подтягивает все поля таблицы (или «UF_*»), но это надо проверять.
select UF_WORKLOAD_MARK as ...
либо использовать средства для регистрирования «UF_» полей.Bitrix умеет добавлять полям сущностей свои «userfield». Но для этого нужно:
CTimeManReportFull::Update($id, [ "UF_…" => $value ])
.Если вы не хотите заморачиваться с пользовательскими полями Bitrix, то проще пойти по первому пути (добавить своё поле напрямую в таблицу).
В вашем куске кода есть участок:
phpif ($USER_ID > 0 && $bCanWriteUser && ($_POST["approve"] ?? null)) { if ($_POST["approve"] == "Y") { $dbrep = CTimeManReportFull::GetByID($ID); $rep = $dbrep->Fetch(); if (is_array($rep) && $USER_ID == $rep['USER_ID']) { $arFields = [ "MARK" => (in_array($_POST["mark"], ["G", "B", "N"]) !== false) ? $_POST["mark"] : "X", ]; ... CTimeManReportFull::Update($ID, $arFields); } } }
Или другой блок, который что-то обновляет.
Вы можете в этот момент прочитать $_POST["workload"]
, проверить корректность значения (1…5) и сохранить вместе с MARK
. Например:
phpif ($USER_ID > 0 && $bCanWriteUser && ($_POST["approve"] ?? null)) { if ($_POST["approve"] == "Y") { $dbrep = CTimeManReportFull::GetByID($ID); $rep = $dbrep->Fetch(); if (is_array($rep) && $USER_ID == $rep['USER_ID']) { $newMark = (in_array($_POST["mark"], ["G", "B", "N"]) !== false) ? $_POST["mark"] : "X"; // допустим, workload приходит в виде числа от 1 до 5: $workload = (int) $_POST["workload"]; if ($workload < 1 || $workload > 5) { $workload = 0; // или по умолчанию 0, если вышло за диапазон } $arFields = [ "MARK" => $newMark, "UF_WORKLOAD_MARK" => $workload, ]; if ($arFields["MARK"] != "X") { $arFields["APPROVER"] = $USER->GetID(); $arFields["APPROVE"] = "Y"; $arFields["APPROVE_DATE"] = ConvertTimeStamp(time(), "FULL"); } else { $arFields["APPROVE"] = "N"; $arFields["APPROVER"] = 0; $arFields["APPROVE_DATE"] = ""; } CTimeManReportFull::Update($ID, $arFields); $CACHE_MANAGER->Clean(CUserReportFull::getInfoCacheId($USER_ID), 'timeman_report_info'); CReportNotifications::MessageUpdate($ID, $rep, $arFields); } } }
Таким образом, при аппруве (и вообще по нажатию нужной кнопки) у вас запишется UF_WORKLOAD_MARK
в таблице.
Важно: смотрите, где именно вам удобнее обрабатывать поле «оценка загруженности». Может быть, это не в блоке
approve == "Y"
, а в другом блоке или вообще при формировании отчёта. Логика зависит от того, как вы хотите, чтобы поле заполнялось: только при утверждении отчёта или при редактировании и т.п.
Чтобы потом это поле отобразить в «детальной карточке отчёта», надо добавить его в $res["INFO"]
. В коде можно посмотреть, как формируется $res["INFO"] = $report;
, там скорее всего всё содержимое строки из CTimeManReportFull::GetByID($ID)
кладётся. Если Bitrix уже подтягивает это поле, то оно появится в $report["UF_WORKLOAD_MARK"]
. Убедитесь, что оно действительно там:
phpif (is_array($report)) { $res["INFO"] = $report; // ... // здесь где-то можно явно продублировать $res["INFO"]["WORKLOAD_MARK"] = $report["UF_WORKLOAD_MARK"]; // ... }
Если там нет UF_WORKLOAD_MARK
, возможно, нужно либо расширять SELECT, либо зарегистрировать поле как пользовательское, либо просто делать отдельный запрос. Но обычно, если таблица b_timeman_report_full имеет колонку UF_WORKLOAD_MARK
, и CTimeManReportFull::GetByID()
не урезает SELECT, то Bitrix её вытащит.
Вы пишете, что пока не нашли, где правится детальная карточка отчёта. Обычно детальная карточка может выводиться через AJAX (судя по коду) или через включаемый компонент. Нужно в папке bitrix/components/bitrix/timeman.report.weekly/templates/.default/
(или в папке вашего шаблона, если переопределяли) искать файлы типа template.php
, report.php
, ajax.php
и т.п.
Иногда деталка «вшита» в JS + AJAX. Для наглядности можно:
bitrix:timeman.topic.reviews
или bitrix:timeman.report.weekly
, как раз в файлах .default/template.php
и смотреть, куда выводится $res["INFO"]
.Там, где выводится основной отчёт (REPORT
, PLANS
, MARK
), вы можете вписать строчку типа:
phpОценка загруженности: <?=$arResult["INFO"]["UF_WORKLOAD_MARK"]?>
(или как вы там выводите — <span>
, <div>
и т.д.).
Возможно, у вас отдельный шаблон для «admin_report_full» (детального просмотра менеджером). Если его нет в
timeman.report.weekly
, то бывает, что это лежит вbitrix:timeman.report.edit
или в каком-нибудь include-файле. Но поищите по коду фрагменты вродеcase 'admin_report_full': ... $APPLICATION->IncludeComponent("bitrix:timeman.topic.reviews", ...)
, а затем поищитеIncludeComponent("bitrix:timeman.topic.reviews"
в папке/bitrix/components/bitrix/
.
CTimeManReportFull::Update(...)
, дописать UF_WORKLOAD_MARK
(считать из $_POST["workload"]
и записать).$report
). Если нет – настроить/дописать SELECT.Обычно этих шагов достаточно, чтобы сохранить и показывать новую «оценку загруженности».
Чтобы не ломать «коробку», старайтесь переопределять шаблоны компонентов в вашем собственном пространстве (через копирование в /local/components/…
или /local/templates/…
). А в файлах ядра (bitrix/modules/timeman/tools/index.php
) в идеале правки тоже надо делать аккуратно — возможно, через события (OnBeforeReportUpdate / OnAfterReportUpdate), если такие есть. Но если события не предусмотрены, придётся править ядро или заводить свой мини-компонент и т.д.
Просто имейте в виду, что при обновлении коробки могут перезаписаться файлы ядра. Поэтому все «хардкорные» правки надо документировать и проверять при апдейтах.
Надеюсь, этот план поможет вам добавить своё поле «Оценка загруженности (1-5)» и отобразить его в детальной карточке отчёта. Если возникнут вопросы с поиском конкретного шаблона, попробуйте по проекту поискать (grep
/phpstorm
/vs code
) упоминания bitrix:timeman.report.weekly
и bitrix:timeman.topic.reviews
, чтобы понять, где именно выводится детальная часть.
начнем с хотябы шаблока просто его отрисовать свойство. Т.е. без сохронения и пользовательской части..
bitrix:timeman.report.weekly: 0.019 :
<? $APPLICATION->IncludeComponent('bitrix:main.calendar', '', array('SHOW_INPUT' => 'Y', 'SHOW_TIME' => 'N', 'HIDE_TIMEBAR' => 'Y', 'INPUT_NAME' => 'bx_goto_date', 'SILENT'=>'Y'), null, array('HIDE_ICONS' => 'Y')); ?> <script> function jsCalendarInsertDate(value) { if (BX.type.isDate(value)) value = value.valueOf(); else value = parseInt(value); var date = new Date(value); if (date.getMonth() == BXTMREPORT.SETTINGS.DATE_START.getMonth() && date.getYear() == BXTMREPORT.SETTINGS.DATE_START.getYear()) { var cell = BXTMREPORT.PARTS.LAYOUT_COLS.DATA.firstChild.tHead.rows[0].cells[date.getDate()-1]; var q = cell.offsetLeft - BXTMREPORT.PARTS.LAYOUT_COLS.DATA.offsetWidth + Math.ceil(cell.offsetWidth * 1.6); BXTMREPORT.PARTS.LAYOUT_COLS.DATA.scrollLeft = q > 0 ? q : 0; } else { var dm = (date.getFullYear()-BXTMREPORT.SETTINGS.DATE_START.getFullYear()) * 12; dm += date.getMonth() - BXTMREPORT.SETTINGS.DATE_START.getMonth(); BXTMREPORT.changeMonth(dm) } } </script>text<div class="webform-content tm-filter"> <span class="tm-filter-item filter-date tm-filter-item-first" id="tm-filter-date"> <a href="javascript:void(0)" onclick="this.blur();window.BXTMREPORT.changeMonth(-1)" class="filter-date-link filter-date-link-left"></a> <span class="fiter-date-text" id="tm_datefilter_title" onclick="BX.calendar({node: this, field: 'bx_goto_date', bTime: false, callback: jsCalendarInsertDate});"><?= \Bitrix\Main\Localization\Loc::getMessage( 'TMR_REPORT_DATE_FILTER_TITLE', [ '#MONTH#' => GetMessage('TMR_MONTH_'.$cur_date_m), '#YEAR#' => $cur_date_y ] ) ?></span> <input type="hidden" name="bx_goto_date" id="bx_goto_date" value="<?=ConvertTimeStamp()?>" />
<? if (count($arResult['arAccessUsers']['READ']) > 1 || $arResult['arAccessUsers']['READ'][0] == '*'): ?>text<a href="javascript:void(0)" onclick="this.blur();window.BXTMREPORT.changeMonth(1)" class="filter-date-link filter-date-link-right"></a> <a href="javascript:void(0)" class="filter-date-link filter-date-link-calendar" onclick="this.blur();window.BXTMREPORT.setToday(); return false;" title="<?=htmlspecialcharsbx(GetMessage('TMR_SET_TODAY'))?>"></a> </span> <input type="hidden" name="month" value="<?=$cur_date_m;?>" /> <input type="hidden" name="year" value="<?=$cur_date_y;?>" /> <span class="tm-filter-item"> <input type="checkbox" name="stats" id="stats" onclick="window.BXTMREPORT.toggleStats(this.checked);" checked="checked" /><label for="stats"><?=GetMessage('TMR_STATS');?></label> </span>
<? function __tmr_replace($str) { $str = preg_replace( '/<option([^>]*)>'.GetMessage('MAIN_NO').'<\/option>/iu', '<option\\1>'.GetMessage('TMR_FILTER_DEPT_0').'</option>', $str ); $str = preg_replace('/name="department"/i', 'name="department" onchange="window.BXTMREPORT.Filter();"', $str); return $str; } CIntranetUtils::ShowDepartmentFilter($arResult['UF_DEPARTMENT_field'], true, false, '__tmr_replace'); ?> <script> document.forms.REPORT_FILTER.department.value = "<?=$arResult["DEPARTMENT_ID"];?>"; </script>text<?if (count($arResult['arDirectUsers'])>1) $display = "inline-block"; else $display = "none";?> <span class="tm-filter-item inactive" style = "display:<?=$display;?>"> <select name="show_all" class="inactive" onmousedown="BX.removeClass(this.parentNode, 'inactive')" onchange="window.BXTMREPORT.Filter();"> <option value="Y"><?=GetMessage('TMR_FILTER_SHOW_ALL_Y')?></option> <option <?=(($arResult['SHOW_ALL'] == "N")?"selected":"")?> value="N"><?=GetMessage('TMR_FILTER_ONLY_DIRECT')?></option> </select> <a href="javascript:void(0)" class="filter-reset" onclick="this.blur(); document.forms.REPORT_FILTER.show_all.value = 'Y'; window.BXTMREPORT.Filter(); return false;"></a> </span> <span class="tm-filter-item inactive">
<a href="javascript:void(0);" class="filter-reset" onclick="this.blur(); document.forms.REPORT_FILTER.department.value = ''; window.BXTMREPORT.Filter(); return false;"></a>
</span>
<span class="tm-settings-item" id="TMBUTTON" onclick="BXTMREPORT.InitSettingMode(this);">
<span class="tm-settings-l"></span><span class="tm-settings-c"><span class="tm-settings-icon"></span><?=GetMessage("TM_SETTINGS")?></span><span class="tm-settings-r"></span>
</span>
</div> <script> window.BXTMREPORT = new JCTimeManReport('bx_tm_report', { DEPARTMENTS: 'tm_report_conrol_departments', FILTER: 'REPORT_FILTER', START_SHOW_ALL:"<?=$arResult["SHOW_ALL"];?>", START_DEPARTMENT:"<?=$arResult["DEPARTMENT_ID"];?>", DATESELECTOR: 'bx_tm_report_dateselector', MONTHS: [<?for($i=1;$i<13;$i++){echo ($i==1 ? '' : ','),"'",CUtil::JSEscape(GetMessage('TMR_MONTH_'.$i)),"'";}?>], DAYS: [<?for($i=1;$i<8;$i++){echo ($i==1 ? '' : ','),"'",CUtil::JSEscape(GetMessage('TMR_DAY_'.$i)),"'";}?>], LANG: <?=CUtil::PhpToJsObject($arMess)?>, SITE_ID: '<?=SITE_ID?>' })</script> <div style="clear: both;"></div>text</div> <div class="webform-corners-bottom"> <div class="webform-left-corner"></div> <div class="webform-right-corner"></div> </div> </div> </form> </div> <div id="bx-report-overdue" style="display:none" class="bx-tm-overdue"></div>
Ниже короткий пример того, как можно «в лоб» добавить на страницу шаблона вывод свойства «Оценка загруженности». Предположим, что в массиве $$arResult$ (или другом месте) у нас уже есть переменная с названием UF_WORKLOAD_MARK
(или любая своя), которую мы хотим показать.
В вашем шаблоне bitrix:timeman.report.weekly
, после (или до) нужного места верстки просто вставьте, к примеру:
php<?php // Примерно в конце файла template.php (или в подходящем месте внутри верстки) ?> <div style="margin: 10px 0; font-weight: bold;"> Оценка загруженности: <?=htmlspecialcharsbx($arResult["INFO"]["UF_WORKLOAD_MARK"] ?? "—")?> </div>
$arResult["INFO"]["UF_WORKLOAD_MARK"]
— это лишь пример того, где может лежать ваша «оценка загруженности». Если у вас другая переменная или другой ключ массива — подставьте свой.
Если данных пока нет, вы можете временно вывести просто статический текст:
php<div>Оценка загруженности: 4 (пример)</div>
А затем уже задействовать реальные данные, когда будете дорабатывать логику.
Так как этот шаблон, судя по коду, в основном отвечает за общий список/сводку отчётов за неделю (а не за «детальную карточку»), то есть вероятность, что детальная часть (где показывается «Карточка отчёта») рендерится другим компонентом или через AJAX. Однако если вам нужно «просто попробовать» вывести свойство – это хороший старт.
При желании можно оформить через языковые фразы (например, создать фразу в /lang/ru/component.php
и вызвать её GetMessage('TMR_MY_WORKLOAD')
), но для начала достаточно статичного текста «Оценка загруженности: ...».
Если вдруг в
$arResult
ещё нет поляUF_WORKLOAD_MARK
(или другого для «оценки»), то при нынешней логике вы всё равно не увидите ничего. Но так вы хотя бы будете уверены, что шаблон «выводит» то, что ему пришло.
У меня вопрос именно как мне найти место ... т.е. где этот шаблон сам детального отчета сотрдника. может ты продумаеш путь как мы найдем место, а уже далее разберемся
/bitrix/tools/timeman.php?action=admin_report_full&site_id=s1
в ответе вижу:
'COMMENTS':'\n
<div class="timeman-comment">
\n
<span class="timeman-comment-avatar" style="background:url('/upload/resize_cache/main/9d8/b7so5x2j1ahx658q7usoymn534sqs71m/100_100_2/bird_black.png') no-repeat center center; background-size: cover;">
</span>
\n
<span class="timeman-comment-body">
\n
т.е. сразу постраение
В «коробочном» Битриксе для детализации отчёта (когда открывается карточка конкретного сотрудника и его отчёта) часто нет отдельного «.php-шаблона» компонента в классическом понимании, как в bitrix:timeman.report.weekly/templates/.default/template.php
. Чаще эта детальная карточка:
Формируется на сервере (через case 'admin_report_full'
в /bitrix/tools/timeman.php
), где собирается массив $res
( INFO
, COMMENTS
, и т.д. ), а затем отдается фронтенду в виде JSON или «HTML-кусков в JSON» (как вы видите по 'COMMENTS' => '<div class="...">'
и проч.).
На клиенте (в JavaScript) происходит AJAX-запрос к timeman.php?action=admin_report_full
, и затем полученные данные вставляются в DOM (или всплывающее окно, или слайдер). То есть в большинстве случаев детальная карточка отчёта отрисовывается именно в JS. Поэтому классического template.php
для детальной карточки вы можете и не найти – всё формируется «на лету» на клиентской стороне.
Посмотрите, кто отправляет AJAX-запрос на timeman.php?action=admin_report_full
.
/bitrix/tools/timeman.php?action=admin_report_full
.timeman.report.js
или script.js
внутри bitrix:timeman.report.weekly
.admin_report_full
.Посмотрите, как обрабатывается ответ. В коде JavaScript это будет примерно так:
jsBX.ajax({ url: '/bitrix/tools/timeman.php?action=admin_report_full', data: { /* ... */ }, method: 'POST', onsuccess: function(resJson) { var res = BX.parseJSON(resJson); // тут часто бывает log(res) или сразу вставка: document.getElementById('...').innerHTML = res.COMMENTS; // либо формируется верстка из res.INFO, res.TO, res.FROM и т.д. } });
Именно в этом месте идёт сборка верстки детальной карточки: заголовок (REPORT_STRIP_TAGS
, PLAN_STRIP_TAGS
), поля «Оценка» (MARK
), «Одобрил» и т.п.
Если нужен именно блок комментариев (COMMENTS
), то он вызывается через:
phpob_start(); $APPLICATION->IncludeComponent("bitrix:timeman.topic.reviews", "", [ "REPORT_ID" => $ID ], false); $res["COMMENTS"] = ob_get_clean();
Это стандартный компонент:
/bitrix/components/bitrix/timeman.topic.reviews/
И там же, в его templates/.default/template.php
, формируется HTML комментариев.
Для остальной части отчёта (сам текст, планы, оценки и т.д.) в текущем коде timeman.php
мы не видим включения другого компонента. Скорее всего, верстка либо формируется «на лету» в JS, либо это какая-то старая логика CJSCore::Init(...)
. Поэтому искать стоит по ключам наподобие "res.INFO", "res.COMMENTS", "admin_report_full", "REPORT_STRIP_TAGS", "APPROVE_DATE"
и т.п.`
Если всё-таки у Bitrix есть заготовленный PHP-шаблон (переопределённый, например, в /local/components/...
), вы найдёте его по тому, что в коде будет $APPLICATION->IncludeComponent("bitrix:timeman.report.edit", ...)
или что-то похожее для «admin_report_full». Но из вашего фрагмента видно, что единственный $APPLICATION->IncludeComponent()
― это bitrix:timeman.topic.reviews
(комментарии). Значит, остальная часть формируется вручную (через JSON + JS).
admin_report_full
.action: 'admin_report_full'
. Там будет коллбэк onsuccess: function(res){...}
, внутри которого и есть отрисовка.res.INFO["UF_WORKLOAD_MARK"]
или как вы его назовёте).Таким образом, «детальный шаблон» сводится к тому JavaScript-куску, где вы распаковываете объект ответа и динамически вставляете его в HTML. Именно там отображаются все поля, которые вернулись с сервера.
Вот тут формируется ответ:
case 'admin_report_full':
$bReturnRes = true;
text$ID = intval($_POST["report_id"] ?? 0); $USER_ID = intval($_POST["user_id"] ?? 0); $OBJID = intval($_POST["id"] ?? 0); $arAccessUsers = CTimeMan::GetAccess(); $bCanEditAll = in_array('*', $arAccessUsers['WRITE']); $bCanReadAll = in_array('*', $arAccessUsers['READ']); $bCanReadUser = (in_array($USER_ID, $arAccessUsers["READ"]) || $bCanReadAll); $bCanWriteUser = (in_array($USER_ID, $arAccessUsers["WRITE"]) || $bCanEditAll); if ($USER_ID > 0 && $bCanWriteUser && ($_POST["approve"] ?? null)) { if ($_POST["approve"] == "Y") { $dbrep = CTimeManReportFull::GetByID($ID); $rep = $dbrep->Fetch(); if (is_array($rep) && $USER_ID == $rep['USER_ID']) { $arFields = [ "MARK" => (in_array($_POST["mark"], ["G", "B", "N"]) !== false) ? $_POST["mark"] : "X", ]; if ($arFields["MARK"] != "X") { $arFields["APPROVER"] = $USER->GetID(); $arFields["APPROVE"] = "Y"; $arFields["APPROVE_DATE"] = ConvertTimeStamp(time(), "FULL"); } else { $arFields["APPROVE"] = "N"; $arFields["APPROVER"] = 0; $arFields["APPROVE_DATE"] = ""; } CTimeManReportFull::Update($ID, $arFields); $CACHE_MANAGER->Clean(CUserReportFull::getInfoCacheId($USER_ID), 'timeman_report_info'); CReportNotifications::MessageUpdate($ID, $rep, $arFields); } } } elseif (($_POST["mode"] ?? null) && $OBJID > 0) { $arFields = [ "UF_REPORT_PERIOD" => $_POST["mode"], "UF_TM_TIME" => (IsAmPmMode() ? convertTimeToMilitary($_POST["time"], 'H:MI T', 'HH:MI') : $_POST["time"]), "UF_TM_REPORT_DATE" => $_POST["date"], "UF_TM_DAY" => $_POST["day"], "ID" => $OBJID, ]; if ($_POST["object"] == "user") { $bCanEdit = ((in_array($OBJID, $arAccessUsers["WRITE"]) && $OBJID != $USER->GetID()) || $bCanEditAll || CTimeMan::IsAdmin() ); if ($bCanEdit) { $arReportUser = new CUserReportFull($OBJID); $res = $arReportUser->SetPeriod($arFields); $CACHE_MANAGER->Clean(CReportSettings::getSettingsCacheId($OBJID), 'timeman_report_settings'); $CACHE_MANAGER->Clean(CUserReportFull::getInfoCacheId($OBJID), 'timeman_report_info'); } } elseif ($_POST["object"] == "dep") { $arSubordination = CIntranetUtils::GetSubordinateDepartments($USER->GetID(), true); $bCanEdit = (in_array($OBJID, $arSubordination) || CTimeMan::IsAdmin()); if ($bCanEdit) { $res = CTimeManReportFull::SetPeriodSection($arFields); $CACHE_MANAGER->CleanDir('timeman_report_settings'); $CACHE_MANAGER->CleanDir('timeman_report_info'); } } } if ($bCanReadUser && !($_POST["mode"] ?? null)) { $dbRes = CUser::GetList( 'ID', 'ASC', ['ID' => $USER_ID], ['SELECT' => ['UF_*']] ); $arUser = $dbRes->GetNext(); $arUser['PHOTO'] = $arUser['PERSONAL_PHOTO'] > 0 ? CIntranetUtils::InitImage($arUser['PERSONAL_PHOTO'], 100, 100, BX_RESIZE_IMAGE_EXACT) : []; $arManagers = CTimeMan::GetUserManagers($USER_ID); $arManagers[] = $USER_ID; if (!is_array($arManagers) || count($arManagers) <= 0) { $arManagers = [$USER_ID]; } $user_url = COption::GetOptionString( 'intranet', 'path_user', '/company/personal/user/#USER_ID#/', ($_REQUEST['site_id'] ?? '') ); $dbManagers = CUser::GetList('ID', 'ASC', ['ID' => implode('|', $arManagers)]); $res["TO"] = []; $res["FROM"] = []; while ($manager = $dbManagers->Fetch()) { $manager['PHOTO'] = $manager['PERSONAL_PHOTO'] > 0 ? CIntranetUtils::InitImage($manager['PERSONAL_PHOTO'], 100, 100, BX_RESIZE_IMAGE_EXACT) : []; $arUserData = [ 'ID' => $manager['ID'], 'LOGIN' => $manager['LOGIN'], 'NAME' => CUser::FormatName(CSite::GetNameFormat(false), $manager, true, true), 'URL' => str_replace(['#ID#', '#USER_ID#'], $manager['ID'], $user_url), 'WORK_POSITION' => $manager['WORK_POSITION'], 'PHOTO' => $manager['PHOTO']['CACHE']['src'] ?? '', ]; if ($USER_ID == $arUserData["ID"]) { $res["FROM"] = $arUserData; } else { $res["TO"][] = $arUserData; } } if (count($res["TO"]) == 0) { $res["TO"][] = $res["FROM"]; } $arFilter = ["ID" => $ID, "USER_ID" => $USER_ID]; $arReportID = []; if ($_POST["empty_slider"] ?? null) { $arFilter = ["ACTIVE" => "Y", "USER_ID" => $USER_ID]; } $dbres = CTimeManReportFull::GetList(["USER_ID" => "asc", "ID" => "asc"], $arFilter); $report = null; while ($rep = $dbres->Fetch()) { $arReportID[] = $rep["ID"]; if ($rep["ID"] == $ID) { $report = $rep; } } $res["REPORT_LIST"] = $arReportID; if (is_array($report)) { $res["INFO"] = $report; if ($res["INFO"]['TASKS_ENABLED'] = (CBXFeatures::IsFeatureEnabled('Tasks') && CModule::IncludeModule('tasks'))) { $res["INFO"]['TASKS'] = unserialize($res["INFO"]['TASKS'] ?? '', ['allowed_classes' => false]); } else { unset($res["INFO"]['TASKS']); } if ($res["INFO"]['CALENDAR_ENABLED'] = CBXFeatures::IsFeatureEnabled('Calendar')) { $res["INFO"]['EVENTS'] = unserialize($res["INFO"]['EVENTS'] ?? '', ['allowed_classes' => false]); } else { unset($res["INFO"]['EVENTS']); } if ($res["INFO"]['FILES']) { $res["INFO"]['FILES'] = unserialize($res["INFO"]['FILES'] ?? '', ['allowed_classes' => false]); } $res["INFO"]['CAN_EDIT'] = ($arUser['ID'] != $USER->GetID()) && ($bCanEditAll || in_array($arUser['ID'], $arAccessUsers['WRITE'])); $res["INFO"]['CAN_EDIT_TEXT'] = ($report["APPROVE"] == "Y") ? "N" : "Y"; $culture = \Bitrix\Main\Application::getInstance()->getContext()->getCulture(); $dayMonthFormat = $culture->getDayMonthFormat(); if ($report["DATE_FROM"] != $report["DATE_TO"]) { $res["INFO"]["TEXT_TITLE"] = FormatDate($dayMonthFormat, MakeTimeStamp($report["DATE_FROM"])) . " - " . FormatDate($dayMonthFormat, MakeTimeStamp($report["DATE_TO"])); } else { $res["INFO"]["TEXT_TITLE"] = FormatDate($dayMonthFormat, MakeTimeStamp($report["DATE_TO"])); } $res["INFO"]["REPORT_STRIP_TAGS"] = strip_tags(nl2br($res["INFO"]["REPORT"])); $res["INFO"]["PLAN_STRIP_TAGS"] = strip_tags(nl2br($res["INFO"]["PLANS"])); $res["INFO"]["APPROVER_INFO"] = []; if (intval($res["INFO"]["APPROVER"]) > 0) { $res["INFO"]["APPROVE_DATE"] = FormatDate($DB->DateFormatToPHP(FORMAT_DATETIME), MakeTimeStamp($res["INFO"]["APPROVE_DATE"])); foreach ($res["TO"] as $manager) { if ($manager["ID"] == intval($res["INFO"]["APPROVER"])) { $res["INFO"]["APPROVER_INFO"] = $manager; break; } } if (!$res["INFO"]["APPROVER_INFO"]) { $dbaprrove = CUser::GetList('ID', 'ASC', ['ID' => intval($res["INFO"]["APPROVER"])]); if ($approver = $dbaprrove->Fetch()) { $approver['PHOTO'] = $approver['PERSONAL_PHOTO'] > 0 ? CIntranetUtils::InitImage($approver['PERSONAL_PHOTO'], 100, 100, BX_RESIZE_IMAGE_EXACT) : []; $res["INFO"]["APPROVER_INFO"] = [ 'ID' => $approver['ID'], 'LOGIN' => $approver['LOGIN'], 'NAME' => CUser::FormatName(CSite::GetNameFormat(false), $approver, true, true), 'URL' => str_replace(['#ID#', '#USER_ID#'], $approver['ID'], $user_url), 'WORK_POSITION' => $approver['WORK_POSITION'], 'PHOTO' => $approver['PHOTO']['CACHE']['src'], ]; } } } ob_start(); $APPLICATION->IncludeComponent( "bitrix:timeman.topic.reviews", "", [ "REPORT_ID" => $ID, ], false ); $res["COMMENTS"] = ob_get_contents(); ob_end_clean(); } } break; //carter
тут мы можем прост одобавить свойство? я так понимаю нет? если про визуальную часть
Короткий ответ:
В этом месте (case 'admin_report_full'
) вы действительно можете добавить новое поле (например, $res["INFO"]["WORKLOAD_MARK"]
) в итоговый массив, который уходит в ответ на AJAX-запрос. Но это ещё не означает, что поле автоматически отрисуется на экране. После этого PHP-кода включается JavaScript-логика, которая парсит полученный объект res
(т.е. res["INFO"]
, res["COMMENTS"]
и т.д.) и вставляет в DOM нужные куски HTML.
Чтобы «визуально» показать новое свойство, у вас есть два пути:
res["COMMENTS"]
(или ещё какой-нибудь ключ ответа)Вы можете «прицепить» нужный фрагмент HTML к $res["COMMENTS"]
, так как он потом вставляется на страницу. Например, перед:
phpob_start(); $APPLICATION->IncludeComponent( "bitrix:timeman.topic.reviews", "", [ "REPORT_ID" => $ID, ], false ); $res["COMMENTS"] = ob_get_contents(); ob_end_clean();
Можно сделать:
phpob_start(); ?> <div style="font-weight: bold; margin-top: 10px;"> Оценка загруженности: <?=htmlspecialcharsbx($report["UF_WORKLOAD_MARK"] ?? "—")?> </div> <?php $htmlWorkload = ob_get_contents(); ob_end_clean(); // теперь прикрепим это к COMMENTS ob_start(); $APPLICATION->IncludeComponent(...); $res["COMMENTS"] = ob_get_contents(); ob_end_clean(); $res["COMMENTS"] .= $htmlWorkload;
Тогда в итоговом JSON-ответе ключ COMMENTS
уже будет содержать и HTML с вашим новым свойством. На клиенте после AJAX-а этот HTML вставится на страницу (в том же месте, где вставляются комментарии).
Плюсы: Быстро и наглядно.
Минусы: Немного «хак» — приделываем верстку к блоку комментариев, что не всегда красиво.
res["INFO"]
и самому добавить вывод (лучший метод)В большинстве случаев вся «детальная карточка отчёта» формируется так:
res["INFO"]
, res["COMMENTS"]
, res["TO"]
и т. д.).echo json_encode($res)
или чем-то подобным).jsBX.ajax({ url: '/bitrix/tools/timeman.php?action=admin_report_full', method: 'POST', data: {...}, onsuccess: function(resText) { var res = BX.parseJSON(resText); // ... тут формируется интерфейс детального отчёта // res.INFO содержит все поля: REPORT, PLANS, MARK, APPROVE_DATE... } });
и так далее.jsdocument.querySelector('#someBlock').innerHTML = res.INFO.REPORT_STRIP_TAGS; document.querySelector('#anotherBlock').innerHTML = res.COMMENTS; ...
Чтобы «визуально» вывести UF_WORKLOAD_MARK
, вы должны:
case 'admin_report_full'
) добавить в $res["INFO"]["WORKLOAD_MARK"]
нужное значение (например, из $report["UF_WORKLOAD_MARK"]
).res
) вставить это значение в нужное место DOM, например:
или как в вашем коде устроено.jsdocument.querySelector('#bx-tm-report-detail .workload-mark').textContent = res.INFO.WORKLOAD_MARK;
Так что надо (а) найти этот JS-файл (или inline-скрипт), где после AJAX вызывается admin_report_full
. Чаще всего он где-то рядом с timeman.report.js
, report.js
, или в том же timeman.php
(выше/ниже) есть <script>…</script>
.
Совет: сделайте поиском по проекту
admin_report_full
и смотрите, где есть JavaScript, который делаетBX.ajax
наaction=admin_report_full
. Именно там происходит отрисовка полученных данных.
Добавляем в PHP:
phpif (is_array($report)) { $res["INFO"] = $report; // ... // вот здесь: $res["INFO"]["WORKLOAD_MARK"] = $report["UF_WORKLOAD_MARK"]; // ... }
На клиенте (JavaScript), там где выводятся поля отчёта, добавить логику, чтобы показать res.INFO.WORKLOAD_MARK
. Например:
jsvar container = document.getElementById('tm-report-full'); container.innerHTML += '<div>Оценка загруженности: ' + res.INFO.WORKLOAD_MARK + '</div>';
Или другой метод, в зависимости от верстки.
Если вы не находите код, который формирует HTML на клиенте (или это слишком сложно), можете «в лоб» сгенерировать HTML на PHP-стороне (как вариант 1) и впихнуть его в $res["COMMENTS"]
или создать другой $res["DETAIL_HTML"]
, а на клиенте просто вставить res.DETAIL_HTML
.
Таким образом, визуальную часть вы «рисуете» либо полностью в серверном рендере (через ob_start()
и $res["COMMENTS"]
), либо в клиентском JS, используя значение $res["INFO"]["WORKLOAD_MARK"]
.
Без изменения клиентского кода (JS) или без «прицепа» к $res["COMMENTS"]
ваше поле просто останется в JSON-ответе, но не будет показано на странице.
как найти где у нас формируется запрос? BX.ajax
Чтобы найти, где формируется запрос BX.ajax
в вашем проекте, нужно выполнить несколько шагов для поиска в коде и отладки с использованием браузерных инструментов. Вот подробный план:
BX.ajax
в JavaScriptПоиск по коду проекта:
В вашем проекте выполните поиск по ключевым словам BX.ajax
или части запроса, например:
jsBX.ajax({ url: '/bitrix/tools/timeman.php?action=admin_report_full', });
Это поможет вам найти место, где происходит вызов AJAX-запроса. Обычно запрос на timeman.php?action=admin_report_full
будет в одном из JavaScript файлов, возможно, в компоненте или его шаблоне.
Проверка с использованием инструментов разработчика: Если не удается найти запрос через поиск, можно использовать браузерные инструменты разработчика для отслеживания запросов:
timeman.php?action=admin_report_full
отправится, он отобразится в списке запросов на вкладке Network.Если вы уже видите запрос в инструментах разработчика, то посмотрите Request URL. Например, запрос будет выглядеть так:
/bitrix/tools/timeman.php?action=admin_report_full
Используйте этот URL в поиске по проекту. Например, можно выполнить поиск по файлу или по всему проекту:
phpaction=admin_report_full
Это поможет вам найти, где именно в коде выполняется этот запрос. Находя это место, вы сможете понять, как передаются параметры в запросе (например, report_id
, user_id
, и другие).
После того как вы нашли код, который отправляет запрос, вам нужно будет понять, как обрабатывается ответ и как происходит рендеринг данных на странице.
Найдите код, который обрабатывает ответ от BX.ajax
. Это будет выглядеть примерно так:
jsBX.ajax({ url: '/bitrix/tools/timeman.php?action=admin_report_full', data: { report_id: reportId, user_id: userId, approve: approveValue }, method: 'POST', onsuccess: function(response) { // Здесь будет обработка ответа от сервера var result = BX.parseJSON(response); // Например: document.getElementById('someElement').innerHTML = result.COMMENTS; } });
Посмотрите, как данные вставляются в DOM:
Ответ от сервера, скорее всего, будет содержать HTML (например, комментарии) или другие данные (INFO
, REPORT
, APPROVE
, и т.д.). Обычно эти данные вставляются в нужный элемент на странице. Найдите этот участок кода и добавьте логику для вывода вашего поля «оценка загруженности».
BX.ajax
, и когда он будет вызван, вы сможете увидеть стек вызовов и точку в коде, где это происходит.BX.ajax
, чтобы найти точку вызова в JavaScript.Когда найдете место, где формируется и отправляется запрос BX.ajax
, вы сможете точно понять, как обрабатываются данные и где внести изменения для отображения нового поля.
так а давай ты напишешь JS которыйй попробует перехватить или собрать нужную информацию
Ниже приведён пример «хака» на чистом JavaScript, который позволяет перехватить любой вызов BX.ajax
и, в частности, подглядеть (и при желании модифицировать) запрос/ответ, связанный с admin_report_full
. Это может пригодиться, когда вы не находите точное место в коде, но хотите поймать событие AJAX на лету.
Внимание: это именно «обёртка» (монки-патч) вокруг
BX.ajax
. Код не является «правильной» практикой для промышленного использования, потому что вмешивается во все AJAX-запросыBX.ajax
. Но может помочь на этапе отладки и исследования.
</body>
, либо через консоль браузера, либо через пользовательский скрипт).console.log
.BX.ajax
, наш код «подменит» функцию и сможет вывести в консоль данные запроса и ответа.html<script> (function() { // Сохраняем оригинальную функцию BX.ajax var originalBxAjax = BX.ajax; // Переопределяем BX.ajax своей функцией-обёрткой BX.ajax = function(config) { // Проверяем, подходит ли нам URL (например, action=admin_report_full) var isTimemanRequest = false; if (config.url && config.url.indexOf("admin_report_full") !== -1) { isTimemanRequest = true; console.log("[BX.ajax] Перехвачена отправка на admin_report_full:", config); } // Сохраняем оригинальный коллбэк 'onsuccess' (если он есть) var originalSuccess = config.onsuccess; // Создаём новую функцию, которая будет вызываться вместо оригинальной config.onsuccess = function(response) { if (isTimemanRequest) { console.log("[BX.ajax] Перехвачена ОТВЕТ для admin_report_full:", response); // Если ответ — это JSON-строка, можно сразу распарсить var data; try { data = BX.parseJSON(response); console.log("[BX.ajax] Распарсенные данные:", data); // Здесь вы можете, например, дополнительно вывести данные // или сохранить их, или модифицировать (если очень надо). // Пример: вывести в консоль оценку загруженности: if (data && data.INFO && data.INFO.WORKLOAD_MARK) { console.log("Оценка загруженности (WORKLOAD_MARK) =", data.INFO.WORKLOAD_MARK); } } catch (e) { console.warn("[BX.ajax] Ошибка парсинга JSON:", e); } } // Вызываем оригинальный onsuccess (чтобы логика Битрикс не сломалась) if (typeof originalSuccess === "function") { originalSuccess.call(this, response); } }; // Наконец, вызываем оригинальный BX.ajax с нашей «подправленной» конфигурацией return originalBxAjax.call(BX, config); }; })(); </script>
BX.ajax
в переменную originalBxAjax
.BX.ajax
на функцию, которая:
url
используется.url
есть admin_report_full
) выводит информацию в консоль.onsuccess
так, чтобы мы могли посмотреть/распарсить ответ.originalBxAjax
) функцию, чтобы не ломать внутренний функционал Bitrix.BX.ajax(...)
(включая запросы к admin_report_full
) будут проходить через нашу обёртку.BX.ajax({ url: '…admin_report_full…' })
, вы сможете перейти в тот участок кода (в JS-файле или в скрипте) и там точечно доработать логику вывода нового свойства.innerHTML
или appendChild
, чем полагаться на глобальный перехват. Но для отладки / промежуточного этапа такой метод с «монки-патчем» довольно удобен.Uncaught TypeError: t.ajax.prepareData is not a function
at t.timeman_query (core_timeman.js:43:20)
at t.JSTimeManReportFullForm.Click (core_timeman.js:6735:6)
at t.ReportSlider.ShowReport (core_timeman.js:7399:19)
at t.StartSlider (core_timeman.js:7276:24)
at HTMLTableCellElement.<anonymous> (script.js:880:7)
Ошибка вида:
textUncaught TypeError: t.ajax.prepareData is not a function at t.timeman_query (core_timeman.js:43:20) ...
чаще всего возникает, когда вы переопределяете или «монки-патчите» (monkey-patch) BX.ajax
(или другой объект), но при этом затираете некоторые методы/свойства, которые Bitrix ожидает от объекта BX.ajax
.
В core_timeman.js
(или другом JS-файле Bitrix) есть вызов типа:
jst.ajax.prepareData(...)
или
jsBX.ajax.prepareData(...)
Если после вашей переопределённой функции больше нет метода prepareData
, выбрасывается ошибка ... is not a function
.
В стандартном Bitrix BX.ajax
— это не просто функция, а объект со своими методами, например:
BX.ajax(...);
BX.ajax.prepareData(...)
;BX.ajax.submitAjax(...)
;Если вы написали код вроде:
js(function() { var originalBxAjax = BX.ajax; BX.ajax = function(config) { // ... // ваш код // ... return originalBxAjax.call(BX, config); }; })();
Вы тем самым превратили BX.ajax
в «чистую» функцию, у которой нет дополнительных методов (prepareData
, submitAjax
, и т.д.), которые были у старого объекта BX.ajax
.
Как результат, когда Bitrix (или ваш код) делает BX.ajax.prepareData(...)
, вы получаете TypeError.
BX.ajax
, не потеряв методы?Ниже пример кода, который сохраняет все статические методы и свойства BX.ajax
, но позволяет вам «подменить» основной вызов для целей логирования или отладки.
html<script> (function() { // 1) Сохраняем старый объект (не просто функцию, а весь объект со свойствами) var oldBxAjax = BX.ajax; // 2) Создаём обёртку — функцию, которую мы будем вызывать, когда кто-то вызывает BX.ajax(...) function newAjaxWrapper(config) { // Здесь можно «посмотреть» url, data, method и т.д. if (config && config.url && config.url.indexOf('admin_report_full') !== -1) { console.log('[newAjaxWrapper] Перехваченный config:', config); } // Сохраняем исходный onsuccess var originalSuccess = config.onsuccess; config.onsuccess = function(response) { // Тоже можем логировать ответ console.log('[newAjaxWrapper] Ответ от сервера:', response); // Вызываем старый коллбэк, чтобы ничего не ломать if (typeof originalSuccess === 'function') { originalSuccess.call(this, response); } }; // Возвращаем результат старой функции BX.ajax (которая тоже может быть функцией) return oldBxAjax.call(BX, config); } // 3) Копируем **все** свойства со старого объекта BX.ajax на нашу новую функцию, // чтобы методы типа prepareData, submitAjax и прочие не пропали. for (var key in oldBxAjax) { if (Object.prototype.hasOwnProperty.call(oldBxAjax, key)) { newAjaxWrapper[key] = oldBxAjax[key]; } } // 4) Теперь подменяем BX.ajax на нашу обёртку BX.ajax = newAjaxWrapper; })(); </script>
BX.ajax
(у которого есть методы).newAjaxWrapper
, которая перехватывает вызовы BX.ajax(config)
.config.url
, модифицируем onsuccess
и т.п.).oldBxAjax.call(...)
, чтобы не сломать исходную логику.for in
) со старого BX.ajax
на нашу новую функцию (или объект). Иначе BX.ajax.prepareData
и другие методы будут утеряны.BX.ajax = newAjaxWrapper;
. Теперь вызовы BX.ajax(...)
идут через нашу обёртку, но BX.ajax.prepareData
и т.д. по-прежнему существуют.Таким образом, код типа core_timeman.js: t.ajax.prepareData(...)
не сломается, потому что newAjaxWrapper.prepareData
= oldBxAjax.prepareData
.
BX.ajax
, а t.ajax
Иногда в коде Bitrix (особенно в «коробочной» части для timeman) используется объект вида:
jst = { ajax: BX.ajax, ... };
или даже собственный метод t.timeman_query = function(...) { ... }
, который внутри всё равно вызывает BX.ajax(...)
. Если у вас конкретно в коде t.ajax.prepareData
(а не BX.ajax.prepareData
), убедитесь, что t.ajax = BX.ajax
после вашей подстановки.
Если t.ajax
сохранялся в переменную до вашего переопределения BX.ajax
, то, возможно, t.ajax
ссылается на «уже испорченную» функцию.
В таком случае придётся либо:
t.ajax
аналогичным способом.t.ajax
всегда берёт актуальный BX.ajax
.Чтобы не словить TypeError: ... is not a function
, нужно не терять исходные методы BX.ajax
. Правильный подход – это аккуратная «обёртка» (wrapper), где вы:
BX.ajax
.oldBxAjax.call(...)
.prepareData
) на вашу новую обёртку.BX.ajax = newAjaxWrapper;
.Тогда ваш патч будет работать, не ломая стандартный функционал Bitrix.
[newAjaxWrapper] Перехваченный config: {method: 'POST', dataType: 'json', url: '/bitrix/tools/timeman.php?action=admin_report_full…ite_id=s1&sessid=609982bd84c499969d59fd8234ef1c7b', data: 'report_id=5&user_id=1&empty_slider=Y&device=browser', onsuccess: ƒ, …}
VM16044:18 [newAjaxWrapper] Ответ от сервера: {TO: Array(1), FROM: {…}, REPORT_LIST: Array(1), INFO: {…}, COMMENTS: '\n\t\t\t\t\t\t\t<div class="timeman-comment">\n\t\t\t\t<span cl…\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t\t'}
Из логов в консоли видно, что обёртка для BX.ajax
успешно перехватывает запрос и выводит ответ. То есть теперь вы видите в консоли:
admin_report_full
.TO
, FROM
, REPORT_LIST
, INFO
, COMMENTS
и т.д.).Пример лога у вас такой:
text[newAjaxWrapper] Перехваченный config: {...} [newAjaxWrapper] Ответ от сервера: { TO: [...], FROM: {...}, REPORT_LIST: [...], INFO: {...}, COMMENTS: '...HTML-строка...' }
Это означает, что вы уже можете исследовать, какие конкретно поля приходят в res.INFO
(или res.COMMENTS
) для нужного отчёта.
Если вы на серверной стороне (в timeman.php
, блок admin_report_full
) добавили, например:
php$res["INFO"]["WORKLOAD_MARK"] = $report["UF_WORKLOAD_MARK"];
или какое-то другое пользовательское поле, теперь вы можете убедиться, что оно реально приходит в ответе.
В консоли вы увидите res.INFO.WORKLOAD_MARK: 3
(или то значение, что есть в БД).
Если поля нет — значит, нужно доработать серверную часть (т.е. создать/заполнить $res["INFO"]["WORKLOAD_MARK"]
).
Сейчас в консоли вы видите ответ, но на страницу оно не выводится, так как клиентский код Bitrix (или ваш) не знает, что нужно где-то «нарисовать» это поле.
Есть два варианта, где «подключиться» к выводу:
В вашей обёртке ( newAjaxWrapper
) – вы можете «дополнить» onsuccess
, после того как получили res
. Например:
js// Ваш обёрнутый onsuccess config.onsuccess = function(response) { var data = BX.parseJSON(response); // Например, выводим в консоль console.log('[newAjaxWrapper] data:', data); if (data && data.INFO && data.INFO.WORKLOAD_MARK) { // Ищем какой-то элемент на странице, куда вставить var container = document.getElementById('myCustomContainer'); if (container) { container.innerHTML = "Оценка загруженности: " + data.INFO.WORKLOAD_MARK; } } // Не забываем вызвать исходный коллбэк, чтобы Битрикс не сломался if (typeof originalSuccess === 'function') { originalSuccess.call(this, response); } };
В «родном» JS-коде, который обрабатывает ответ от admin_report_full
.
BX.ajax({...onsuccess: function(resText) {...}})
без вашей обёртки.jsdocument.querySelector("#somePlace").innerHTML = "Моя оценка: " + res.INFO.WORKLOAD_MARK;
Если посмотрите, как Bitrix вставляет res.COMMENTS
, то увидите что-то вроде:
jsdocument.getElementById('timeman-comments-container').innerHTML = res.COMMENTS;
или аналог. В таком случае, чтобы прямо вместе с комментариями появился ваш блок, можно:
Сформировать HTML на стороне PHP и «приделать» его к $res["COMMENTS"]
.
Например:
phpob_start(); ?> <div class="my-extra-field"> Оценка загруженности: <?=htmlspecialcharsbx($report["UF_WORKLOAD_MARK"])?> </div> <?php $htmlExtra = ob_get_clean(); $res["COMMENTS"] = $res["COMMENTS"] . $htmlExtra;
Тогда в ответе COMMENTS
уже будет содержать и блок «Оценка загруженности», и на клиенте оно автоматически впечатается в страницу вместе с комментариями.
Или перехватить res.COMMENTS
в onsuccess
и самому конкатенировать:
jsdata.COMMENTS += '<div class="my-extra-field">Оценка: ' + data.INFO.WORKLOAD_MARK + '</div>'; document.getElementById('timeman-comments-container').innerHTML = data.COMMENTS;
В итоге вы получаете нужное поле прямо в том месте, где рендерятся комментарии.
$res["COMMENTS"]
на сервере и тем самым встроить ваш новый блок туда, где Bitrix уже вставляет COMMENTS
.Либо искать «родной» код JavaScript, где обрабатывается res.INFO
и дописывать туда innerHTML
.
Главное: теперь, благодаря «newAjaxWrapper», вы точно знаете, что запрос улетает, что приходит, и можете отследить все поля для детальной карточки отчёта.
окей ID,
],
false
);
report["UF_WORKLOAD_MARK"])?>
</div>
<?php
res["COMMENTS"] = htmlExtra;
ob_end_clean();
}
}
вариант стработал. давай только его разовем сделаем тут выподающий список использую UI битрикса с оценкой от 1 до 5
Чтобы добавить выпадающий список с оценкой от 1 до 5, используя UI компоненты Битрикса, нам нужно будет использовать компонент bitrix:ui.select
или похожий компонент, который предоставляет интерфейс для выбора значений в виде списка.
Вот шаги по расширению текущего кода с выпадающим списком для оценки загруженности.
COMMENTS
, чтобы отобразить в интерфейсе.Вот пример, как можно это реализовать:
php$APPLICATION->IncludeComponent( "bitrix:timeman.topic.reviews", "", [ "REPORT_ID" => $ID, ], false ); // Получаем текущую оценку, если она уже есть $currentWorkloadMark = $report["UF_WORKLOAD_MARK"] ?? 0; // Формируем HTML для выпадающего списка с оценкой ?> <div class="my-extra-field"> <label for="workload_mark"><?= GetMessage("TMR_WORKLOAD_MARK_LABEL") ?>:</label> <select id="workload_mark" name="UF_WORKLOAD_MARK"> <option value="1" <?= ($currentWorkloadMark == 1) ? 'selected' : '' ?>>1</option> <option value="2" <?= ($currentWorkloadMark == 2) ? 'selected' : '' ?>>2</option> <option value="3" <?= ($currentWorkloadMark == 3) ? 'selected' : '' ?>>3</option> <option value="4" <?= ($currentWorkloadMark == 4) ? 'selected' : '' ?>>4</option> <option value="5" <?= ($currentWorkloadMark == 5) ? 'selected' : '' ?>>5</option> </select> </div> <?php $htmlExtra = ob_get_clean(); // Добавляем новый блок с оценкой к комментариям $res["COMMENTS"] = $res["COMMENTS"] . $htmlExtra; ob_end_clean();
<select>
с оценками от 1 до 5: Мы создаём выпадающий список с пятью значениями. Если в $report["UF_WORKLOAD_MARK"]
уже есть значение (например, 3), оно будет установлено как выбранное. Для этого используется конструкция <?= ($currentWorkloadMark == 3) ? 'selected' : '' ?>
.GetMessage("TMR_WORKLOAD_MARK_LABEL")
: Это пример локализованной строки для метки. Замените на вашу локализацию, например: Оценка загруженности
.$res["COMMENTS"]
, чтобы он был отображен вместе с комментариями отчёта.Чтобы сохранить выбор, необходимо добавить обработку, когда пользователь выбрал оценку и отправил форму. В вашем коде, после того как форма будет отправлена, можно будет обработать поле UF_WORKLOAD_MARK
и сохранить его в БД.
Пример:
phpif ($_POST["UF_WORKLOAD_MARK"]) { $workloadMark = intval($_POST["UF_WORKLOAD_MARK"]); // Проверяем, что это значение от 1 до 5 if ($workloadMark >= 1 && $workloadMark <= 5) { // Сохраняем в БД $arFields = [ "UF_WORKLOAD_MARK" => $workloadMark, ]; CTimeManReportFull::Update($ID, $arFields); } }
Для улучшенного интерфейса можно использовать UI компоненты Bitrix, например, компонент bitrix:ui.select
, который позволяет стилизовать выпадающий список и даёт дополнительные возможности для работы с компонентами.
Вот как это может выглядеть:
php// Используем UI компонент Bitrix для выбора оценки use Bitrix\Main\UI\Extension; Extension::load(["ui.select"]); ?> <div class="my-extra-field"> <label for="workload_mark"><?= GetMessage("TMR_WORKLOAD_MARK_LABEL") ?>:</label> <select id="workload_mark" name="UF_WORKLOAD_MARK" class="ui-select"> <option value="1" <?= ($currentWorkloadMark == 1) ? 'selected' : '' ?>>1</option> <option value="2" <?= ($currentWorkloadMark == 2) ? 'selected' : '' ?>>2</option> <option value="3" <?= ($currentWorkloadMark == 3) ? 'selected' : '' ?>>3</option> <option value="4" <?= ($currentWorkloadMark == 4) ? 'selected' : '' ?>>4</option> <option value="5" <?= ($currentWorkloadMark == 5) ? 'selected' : '' ?>>5</option> </select> </div> <?php $htmlExtra = ob_get_clean(); $res["COMMENTS"] = $res["COMMENTS"] . $htmlExtra; ob_end_clean();
ui-select
из UI библиотеки Bitrix, что добавляет стилизацию для вашего выпадающего списка.Extension::load
.Этот подход позволит вам эффективно собирать и отображать оценки загруженности для отчётов сотрудников.
давай сделаем с кнопкой "Оценить" (пока без обработчика)
Ниже пример, как можно вывести выпадающий список с оценками (1–5) и кнопку «Оценить», не сохраняя пока результат в БД. Мы просто вставляем эту верстку (вместе со скриптом) в конец $res["COMMENTS"]
, чтобы она появилась в детальной карточке (там же, где выводятся комментарии).
php// Предположим, что мы находимся внутри if (is_array($report)) { ... } и уже делаем ob_start(); ob_start(); // Получим текущую (если есть) оценку из отчёта. Если нет, будет 0 или пусто. $currentWorkloadMark = (int)($report["UF_WORKLOAD_MARK"] ?? 0); ?> <div class="my-extra-field" style="margin-top: 10px;"> <label for="workload_mark" style="display:inline-block; width:140px;"> <?= htmlspecialcharsbx("Оценка загруженности:") ?> </label> <select id="workload_mark" name="UF_WORKLOAD_MARK"> <option value="1" <?= ($currentWorkloadMark === 1 ? 'selected' : '') ?>>1</option> <option value="2" <?= ($currentWorkloadMark === 2 ? 'selected' : '') ?>>2</option> <option value="3" <?= ($currentWorkloadMark === 3 ? 'selected' : '') ?>>3</option> <option value="4" <?= ($currentWorkloadMark === 4 ? 'selected' : '') ?>>4</option> <option value="5" <?= ($currentWorkloadMark === 5 ? 'selected' : '') ?>>5</option> </select> <button type="button" id="btnEvaluate" style="margin-left: 10px;"> <?= htmlspecialcharsbx("Оценить") ?> </button> </div> <script> // Простой скрипт для демонстрации – пока просто выводим выбранное значение в консоль document.addEventListener('DOMContentLoaded', function() { var btn = document.getElementById('btnEvaluate'); btn.addEventListener('click', function() { var select = document.getElementById('workload_mark'); var mark = select.value; console.log("Нажали 'Оценить'. Выбранная оценка:", mark); // Здесь можно добавить AJAX или другую логику сохранения }); }); </script> <?php // Получим HTML, сформированный выше $htmlExtra = ob_get_clean(); // Теперь добавляем его к существующим $res["COMMENTS"] $res["COMMENTS"] .= $htmlExtra; // ob_end_clean() уже есть в вашем коде после этого
ob_start() / ob_get_clean()
: собирает HTML в буфер, чтобы потом дописать к $res["COMMENTS"]
.<select>
) с оценками от 1
до 5
.
UF_WORKLOAD_MARK
(например, 3
), мы отмечаем его selected
.type="button"
) не вызывает перезагрузку, а просто срабатывает скрипт.click
по кнопке, получает текущее значение из списка и выводит его в консоль.
case 'admin_report_full'
), которая будет принимать $_POST["UF_WORKLOAD_MARK"]
и вызывать CTimeManReportFull::Update($ID, ["UF_WORKLOAD_MARK" => $value])
.onchange
/onclick
JS и отправлять запрос через BX.ajax
.<form> ... </form>
с method="POST"
(или onsubmit
-JS).