403Webshell
Server IP : 80.87.202.40  /  Your IP : 216.73.216.169
Web Server : Apache
System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64
User : bitrix ( 600)
PHP Version : 8.2.27
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : OFF |  Sudo : ON |  Pkexec : ON
Directory :  /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/bizproc/workflow/timeline/src/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/bizproc/workflow/timeline/src/timeline.js
// eslint-disable-next-line max-classes-per-file
import { ajax, Type, Tag, Text, Dom, Runtime, Uri, Loc } from 'main.core';
import { DocumentId } from 'bizproc.document';
import { Timestamp, UserId } from 'bizproc.types';
import 'ui.icons.b24';
import 'ui.hint';
import { TextCrop } from 'ui.textcrop';
import { Popup, Menu } from 'main.popup';
import { TaskStatus, TaskUserData, TaskData } from 'bizproc.task';
import { DateTimeFormat } from 'main.date';
import { ErrorsView } from './views/errors-view';

import { TimelineTask } from './timeline-task';

import './css/style.css';
import { TimelineTaskView } from './views/timeline-task-view';

export type TimelineUserData = {
	id: UserId,
	login: string,
	name: string,
	lastName: string,
	secondName: string,
	fullName: string,
	workPosition: string,
	link: string,
	avatarSize100: string,
	status: number,
};

export type TimelineStatsData = {
	averageDuration: ?number,
	efficiency: string,
};

export type TimelineData = {
	document: DocumentId,
	isWorkflowRunning: boolean,
	timeToStart: ?Timestamp,
	executionTime: ?Timestamp,
	started: ?Timestamp,
	startedBy: UserId,
	tasks: TimelineTask[],
	users: Map<UserId, TimelineUserData>,
	userStatuses: Map<UserId, TaskStatus>,
	stats: TimelineStatsData,
	biMenu?: Array<BiMenuItem>,
};

type BiMenuItem = {
	ID: string,
	TEXT: string,
	URL: string,
}

export class DurationFormatter
{
	static #limits = [
		[3600 * 24 * 365, 'Ydiff'],
		[3600 * 24 * 31, 'mdiff'],
		[3600 * 24, 'ddiff'],
		[3600, 'Hdiff'],
		[60, 'idiff'],
	];

	static #getFormatString(seconds: number): string
	{
		for (const limit of this.#limits)
		{
			if (seconds >= limit[0])
			{
				return limit[1];
			}
		}

		return 'sdiff';
	}

	static #getMultiplierByFormat(format: string): number
	{
		for (const limit of this.#limits)
		{
			if (format === limit[1])
			{
				return limit[0];
			}
		}

		return 0;
	}

	static formatTimestamp(timestamp: number): string
	{
		return DateTimeFormat.format(
			this.#getFormatString(Date.now() / 1000 - timestamp),
			timestamp,
		);
	}

	static formatTimeInterval(interval: ?number, values: number = 1): string
	{
		if (Type.isNil(interval))
		{
			return Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_AVERAGE_PROCESS_TIME_UNKNOWN');
		}

		if (interval === 0)
		{
			return Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_ZERO_SECOND_INTERVAL');
		}

		let result = '';
		let remainder = interval;

		for (let i = 0; i < values; i++)
		{
			const format = this.#getFormatString(remainder);
			// ignore seconds if we already have result
			if (result.length > 0 && format === 'sdiff')
			{
				return result;
			}
			const multiplier = this.#getMultiplierByFormat(format);
			result += DateTimeFormat.format(format, 0, remainder);
			result += ' ';
			if (multiplier > 0)
			{
				remainder %= multiplier;
				if (remainder === 0)
				{
					return result;
				}
			}
		}

		return result;
	}

	static formatDate(timestamp: number, format: string, formatShort: ?string): string
	{
		if (formatShort && (DateTimeFormat.format('Y', timestamp) === DateTimeFormat.format('Y', Date.now() / 1000)))
		{
			return DateTimeFormat.format(formatShort, timestamp);
		}

		return DateTimeFormat.format(format, timestamp);
	}
}

export class Timeline
{
	#workflowId: string;
	// #taskId: number;
	#data: ?TimelineData;
	#isLoaded: boolean;
	#errors: Array<{ message: string }> = [];
	#container: HTMLDivElement;
	#biPopup: HTMLDivElement;
	#dateFormat: string;
	#dateFormatShort: string;
	#efficiencyPopup: Popup;

	constructor(
		options: { workflowId: string, taskId?: number, },
		config: { dateFormat: ?string, dateFormatShort: ?string, timeFormat: ?string, },
	)
	{
		this.#container = this.#renderContainer();
		setTimeout(() => {
			this.#textCrop();
		}, 500);
		if (Type.isPlainObject(options))
		{
			this.#workflowId = options.workflowId;
			// this.#taskId = options.taskId;
			this.#isLoaded = false;

			this.#loadTimeline();
		}

		if (Type.isPlainObject(config))
		{
			this.#dateFormat = `${config.dateFormat || 'j F Y'} ${config.timeFormat || 'H:i'}`;
			this.#dateFormatShort = `${config.dateFormatShort || 'j F'} ${config.timeFormat || 'H:i'}`;
		}
	}

	static open(options: { workflowId: string, taskId?: number }): void
	{
		Runtime
			.loadExtension('sidepanel')
			.then(() => {
				BX.SidePanel.Instance.open(
					Uri.addParam(
						'/bitrix/components/bitrix/bizproc.workflow.timeline.slider/index.php',
						Type.isPlainObject(options) ? options : {},
					),
					{
						width: 950,
						allowChangeHistory: false,
						cacheable: false,
						loader: '/bitrix/js/bizproc/workflow/timeline/img/skeleton.svg',
						printable: true,
					},
				);
			})
			.catch((response) => console.error(response.errors))
		;
	}

	#loadTimeline(): void
	{
		ajax.runAction('bizproc.workflow.getTimeline', {
			data: { workflowId: this.#workflowId },
		}).then((response) => {
			this.#setDataFromResponse(response);
			this.#isLoaded = true;
			this.render();
		}).catch((response) => {
			this.#setDataFromResponse(response);
			this.#isLoaded = true;
			this.render();
		});
	}

	#setDataFromResponse(response): void
	{
		if (Type.isPlainObject(response))
		{
			const getString = (value, defaultValue = '') => (Type.isString(value) ? value : defaultValue);
			const getArray = (value, defaultValue = []) => (Type.isArray(value) ? value : defaultValue);
			const getBool = (value, defaultValue = false) => (Type.isBoolean(value) ? value : defaultValue);
			const getInteger = (value, defaultValue = 0) => (Type.isInteger(value) ? value : defaultValue);

			if (Type.isPlainObject(response.data))
			{
				this.#data = {
					document: new DocumentId({
						documentId: getArray(response.data.documentType),
						entityName: getString(response.data.entityName),
						documentName: getString(response.data.documentName),
						documentUrl: getString(response.data.documentUrl),
						moduleName: getString(response.data.moduleName),
					}),
					isWorkflowRunning: getBool(response.data.isWorkflowRunning),
					timeToStart: getInteger(response.data.timeToStart, null),
					executionTime: getInteger(response.data.executionTime, null),
					started: getInteger(response.data.started, null),
					startedBy: getInteger(response.data.startedBy),
					tasks: getArray(response.data.tasks).map((taskData) => new TimelineTask(taskData)),
					users: new Map(),
					stats: {
						averageDuration: getInteger(response.data.stats.averageDuration, null),
						efficiency: getString(response.data.stats.efficiency),
					},
					biMenu: getArray(response.data.biMenu, null),
				};

				for (const user of getArray(response.data.users))
				{
					this.#data.users.set(Text.toInteger(user.id), user);
				}
			}
			this.#errors = getArray(response.errors);
		}
	}

	render(): HTMLElement
	{
		Dom.clean(this.#container);

		if (this.#hasErrors())
		{
			const errorsView = new ErrorsView({ errors: this.#errors });
			errorsView.renderTo(this.#container);

			return this.#container;
		}

		if (!this.#isLoaded)
		{
			Dom.append(this.#renderLoadingStub(), this.#container);
		}

		if (Type.isPlainObject(this.#data))
		{
			Dom.replace(this.#container, this.#renderContainer());
			this.#createEfficiencyPopup().show();
			if (this.#data.biMenu)
			{
				this.showBiMenus(this.#data.biMenu);
			}
		}

		return this.#container;
	}

	#renderItemTitle(title, iconClass, iconText, crop): HTMLElement
	{
		const iconClassValue = Type.isString(iconClass) ? (` ${iconClass}`) : '';
		const iconTextValue = Type.isString(iconText) ? iconText : '';
		const cropValue = crop ? ' data-crop="crop"' : '';

		return Tag.render`
			<div>
				<span class="bizproc-workflow-timeline-icon${iconClassValue}">${iconTextValue}</span>
				<div class="bizproc-workflow-timeline-title"${cropValue}>${Text.encode(title)}</div>
			</div>
		`;
	}

	#renderSubject(subject): HTMLElement
	{
		return Tag.render`
			<div class="bizproc-workflow-timeline-subject">${Text.encode(subject)}</div>
		`;
	}

	#renderProceedTaskButton(task: TaskData): HTMLElement
	{
		const uri = (
			task.url || new Uri(`/company/personal/bizproc/${task.id}/`).toString()
		);

		return Tag.render`
			<div class="task-button --hidden">
				<a class="ui-btn ui-btn-xs ui-btn-primary ui-btn-round" href="${Text.encode(uri)}">
					${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_BUTTON_PROCEED')}
				</a>
			</div>
		`;
	}

	#renderUser(userId: UserId, userData: ?TaskUserData = null, task: ?TaskData = null): HTMLElement
	{
		const user = this.#data.users.get(userId || userData.id);
		let userClass = '';
		let isWaiting = false;
		if (userData)
		{
			const status = new TaskStatus(userData.status);
			if (status.isYes() || status.isOk())
			{
				userClass = ' --voted-up';
			}

			if (status.isNo())
			{
				userClass = ' --voted-down';
			}

			if (status.isWaiting())
			{
				isWaiting = true;
			}
		}
		const position = Type.isString(user.workPosition)
			? (`<div class="bizproc-workflow-timeline-user-pos">${Text.encode(user.workPosition)}</div>`)
			: '';
		let avatar = '<i></i>';
		if (Type.isString(user.avatarSize100))
		{
			avatar = `<i style="background-image: url('${encodeURI(user.avatarSize100)}')"></i>`;
		}
		const button = (task?.id && isWaiting) ? this.#renderProceedTaskButton(task) : '';

		return Tag.render`
			<div class="bizproc-workflow-timeline-user${userClass}">
				<div class="bizproc-workflow-timeline-userlogo ui-icon ui-icon-common-user">
					${avatar}
				</div>
				<div class="bizproc-workflow-timeline-user-block">
					<a class="bizproc-workflow-timeline-link" href="${user.link}">${Text.encode(user.fullName)}</a>
					${position}
				</div>
				${button}
			</div>
		`;
	}

	#renderDoc(name, link, type, iconClass): HTMLElement
	{
		return Tag.render`
			<div class="bizproc-workflow-timeline-doc">
				${this.#renderCaption(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_START_DOC'))}
				<div class="bizproc-workflow-timeline-type">
					<span class="ui-icon-set ${iconClass}"></span>
					<span class="bizproc-workflow-timeline-type-text">${type}</span>
				</div>
				<a class="bizproc-workflow-timeline-link" href="${link}" target="_top">${Text.encode(name)}</a>
			</div>
		`;
	}

	#renderCaption(caption): HTMLElement
	{
		return Tag.render`
			<div class="bizproc-workflow-timeline-caption">${caption}</div>
		`;
	}

	#renderNotice(subject, text): HTMLElement
	{
		return Tag.render`
			<div class="bizproc-workflow-timeline-notice">
				<div class="bizproc-workflow-timeline-subject">${Text.encode(subject)}</div>
				<span class="bizproc-workflow-timeline-text">${Text.encode(text)}</span>
			</div>
		`;
	}

	#renderStatus(text, statusClass): HTMLElement
	{
		const statusClassValue = Type.isString(statusClass) ? (` ${statusClass}`) : '';

		return Tag.render`
			<div class="bizproc-workflow-timeline-status${statusClassValue}">${Text.encode(text)}</div>
		`;
	}

	#renderMore(): HTMLElement
	{
		return Tag.render`
			<div class="bizproc-workflow-timeline-item --more">
				<div class="bizproc-workflow-timeline-item-inner">
					<span class="bizproc-workflow-timeline-icon"></span>
					<button class="ui-btn ui-btn-light-border ui-btn-xs" type="button" onclick="expandMore(event)">
						<span class="ui-btn-text">
							${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_MORE_TASKS')}
						</span>
					</button>
					<script type="text/javascript">
						function expandMore(event)
						{
							const moreItemBlock = event.target.closest('.--more');
							const hiddenBlocks = document.querySelectorAll('.bizproc-workflow-timeline-item.--hidden:not(.--efficiency)');
							BX.Dom.addClass(moreItemBlock, '--hidden');
							hiddenBlocks.forEach((hiddenBlock) => BX.Dom.removeClass(hiddenBlock, '--hidden'));
						}
					</script>
				</div>
			</div>
		`;
	}

	#renderContent(children): HTMLElement
	{
		return (Tag.render`
			<div class="bizproc-workflow-timeline-content">
				${children}
			</div>
		`);
	}

	#renderItem(children, itemClass, efficiencyClass): HTMLElement
	{
		const itemClassValue = Type.isString(itemClass) ? (` ${itemClass}`) : '';
		const efficiencyClassValue = (
			Type.isString(efficiencyClass) ? (` data-efficiency-class="${efficiencyClass}"`) : ''
		);

		return (Tag.render`
			<div class="bizproc-workflow-timeline-item${itemClassValue}"${efficiencyClassValue}>
				<div class="bizproc-workflow-timeline-item-inner">
					${children}
				</div>
			</div>
		`);
	}

	#renderItemsList(items): HTMLElement
	{
		return (Tag.render`
			<div class="bizproc-workflow-timeline-wrapper">
				<div class="bizproc-workflow-timeline-inner">
					<div class="bizproc-workflow-timeline-list">
						${items}
					</div>
					<script type="text/javascript">
						(function() {
							const buttons = document.querySelectorAll('.task-button.--hidden');
							const showButtons = buttons.length > 1;
							buttons.forEach(function (button) {
								BX.Dom.insertBefore(
									button.closest('.bizproc-workflow-timeline-user'),
									button.closest('.bizproc-workflow-timeline-user-list').firstChild
								);
								if (showButtons)
								{
									BX.Dom.removeClass(button, '--hidden')
								}
							});
						})();
					</script>
				</div>
			</div>
		`);
	}

	#renderFirstBlock(): HTMLElement
	{
		const content = [];
		if (this.#data.startedBy)
		{
			content.push(
				this.#renderCaption(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_FROM')),
				this.#renderUser(this.#data.startedBy),
			);
		}
		content.push(
			this.#renderDoc(
				this.#data.document.name,
				this.#data.document.url,
				this.#data.document.moduleName,
				'--file-2',
			),
		);

		if (!Type.isNil(this.#data.timeToStart))
		{
			content.push(this.#renderNotice(
				Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_EXECUTION_TIME'),
				DurationFormatter.formatTimeInterval(this.#data.timeToStart, 2),
			));
		}

		return this.#renderItem([
			this.#renderItemTitle(
				this.#data.document.entityName,
				'--success',
				'1',
			),
			this.#data.started && this.#renderSubject(
				DurationFormatter.formatDate(this.#data.started, this.#dateFormat, this.#dateFormatShort),
			),
			this.#renderContent(content),
		], '--selected');
	}

	#renderContainer(): HTMLElement
	{
		const items = [];
		let efficiencyClass = '';
		let isWaiting = false;

		if (this.#data)
		{
			let task = null;

			items.push(this.#renderFirstBlock());

			let taskNumber = 1;
			let hasHidden = this.#data.tasks[0] ? !this.#data.tasks[0].status.isWaiting() : true;
			for (const taskIndex of Object.keys(this.#data.tasks))
			{
				task = this.#data.tasks[taskIndex];
				isWaiting = task.status.isWaiting();
				if (!isWaiting)
				{
					++taskNumber;
				}

				const taskView = new TimelineTaskView({
					task,
					userId: Text.toInteger(Loc.getMessage('USER_ID')),
					dateFormat: this.#dateFormat,
					dateFormatShort: this.#dateFormatShort,
					taskNumber: isWaiting ? null : taskNumber,
					users: this.#data.users,
				});

				const node = taskView.render();

				if (!isWaiting && hasHidden)
				{
					Dom.addClass(node, '--hidden');
				}

				if (isWaiting && hasHidden)
				{
					items.push(this.#renderMore());
					hasHidden = false;
				}

				items.push(node);
			}

			if (hasHidden && this.#data.tasks[0])
			{
				items.push(this.#renderMore());
			}

			if (this.#data.isWorkflowRunning)
			{
				if (isWaiting)
				{
					items.push(this.#renderItem([
						this.#renderItemTitle(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_IN_PROGRESS')),
						this.#renderSubject(
							Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_IN_PROGRESS_TIP'),
						),
					], '--tech --previous-item'));
				}
				else
				{
					items.push(this.#renderItem([
						this.#renderItemTitle(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_IN_PROGRESS_INTERMEDIATE')),
						this.#renderSubject(
							Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_IN_PROGRESS_INTERMEDIATE_TIP'),
						),
					], '--tech --previous-item'));
				}
			}
			else
			{
				const isOk = !task || task.status.isOk() || task.status.isYes();
				const content = [
					this.#renderCaption(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_PROCESS_FINISHED')),
				];
				if (this.#data.startedBy)
				{
					content.push(
						isOk
							? this.#renderStatus(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_APPROVED_FOR'))
							: this.#renderStatus(
								Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_DECLINED'),
								'--failure',
							),
						this.#renderUser(this.#data.startedBy),
					);
				}
				content.push(this.#renderNotice(
					Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_PROCESS_EXECUTED'),
					DurationFormatter.formatTimeInterval(this.#data.executionTime),
				));
				efficiencyClass = isOk ? '--success' : '--declined';
				items.push(
					this.#renderItem(
						[
							this.#renderItemTitle(
								this.#data.document.entityName,
								isOk ? '--success' : null,
							),
							this.#data.started && this.#renderSubject(
								DurationFormatter.formatDate(
									this.#data.started + this.#data.executionTime,
									this.#dateFormat,
									this.#dateFormatShort,
								),
							),
							this.#renderContent(content),
						],
						isOk ? '--success --previous' : '--declined --selected --previous',
						efficiencyClass,
					),
					this.#renderEfficiencyInlineContent(),
				);
			}
		}

		return this.#renderItemsList(items);
	}

	#textCrop()
	{
		const textCropNodes = document.querySelectorAll('[data-crop="crop"]');
		for (const textCropNode of textCropNodes)
		{
			const text = new TextCrop({
				rows: 2,
				target: textCropNode,

			});
			text.init();
		}
	}

	#createEfficiencyPopup(): Popup
	{
		this.#efficiencyPopup = new Popup({
			width: 403,
			minHeight: 345,
			closeIcon: true,
			content: this.#renderEfficiencyPopupContent(),
			bindElement: {
				left: 555,
				top: 130,
			},
			padding: 26,
			borderRadius: '18px',
			className: '--bizproc-timeline-popup',
			events: {
				onPopupClose: () => {
					let inlineEfficiencyPrev = document.querySelector('.--previous-item');
					if (!inlineEfficiencyPrev)
					{
						inlineEfficiencyPrev = document.querySelector('.bizproc-workflow-timeline-item.--processing');
					}

					if (!inlineEfficiencyPrev)
					{
						return;
					}
					BX.Dom.addClass(inlineEfficiencyPrev, '--previous');
					let efficiencyInlineClass = inlineEfficiencyPrev.getAttribute('data-efficiency-class');
					if (!efficiencyInlineClass)
					{
						efficiencyInlineClass = '';
					}
					inlineEfficiencyPrev.after(this.#renderEfficiencyInlineContent(efficiencyInlineClass));
				},
			},
		});

		return this.#efficiencyPopup;
	}

	#getEfficiencyData(): Popup
	{
		let logoClass = '--first';
		let notice = Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_NO_STATS');

		switch (this.#data.stats.efficiency)
		{
			case 'fast':
				logoClass = '--fast';
				notice = Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_PERFORMED_QUICKLY');
				break;
			case 'slow':
				logoClass = '--slow';
				notice = Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_PERFORMED_SLOWLY');
				break;
			case 'stopped':
				logoClass = '--stopped';
				notice = Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_PERFORMED_NO_PROGRESS');
				break;
			default:
				break;
		}

		return [logoClass, notice];
	}

	#renderEfficiencyInlineContent(itemClass: string): HTMLElement
	{
		const [logoClass, notice] = this.#getEfficiencyData();

		const efficiencyInlineContent = Tag.render`
			<div class="bizproc-workflow-timeline-item --efficiency ${itemClass}">
				<div class="bizproc-workflow-timeline-item-inner">
					<div class="bizproc-workflow-timeline-title">
						${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_EFFECTIVITY_MARK')}
					</div>
					<div class="bizproc-workflow-timeline-content">
						<div class="bizproc-workflow-timeline-eff-icon ${logoClass}"></div>
						<div class="bizproc-workflow-timeline-content-inner">
							<div class="bizproc-workflow-timeline-caption">${notice}</div>
							<div class="bizproc-workflow-timeline-notice">
								<div class="bizproc-workflow-timeline-subject">
									${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_CURRENT_PROCESS_TIME')}
								</div>
								<span class="bizproc-workflow-timeline-text">
									${DurationFormatter.formatTimeInterval(this.#data.executionTime)}
								</span>
								<span
									data-hint="${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_TIME_DIFFERENCE_MSGVER_1')}"
								></span>
							</div>
							<div class="bizproc-workflow-timeline-notice">
								<div class="bizproc-workflow-timeline-subject">
									${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_AVERAGE_PROCESS_TIME')}
								</div>
								<span class="bizproc-workflow-timeline-text">
									${
										(this.#data.stats.averageDuration === null)
											? Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_AVERAGE_PROCESS_TIME_UNKNOWN')
											: DurationFormatter.formatTimeInterval(this.#data.stats.averageDuration)
									}
								</span>
							</div>
						</div>
					</div>
				</div>	
			</div>
		`;
		BX.UI.Hint.init(efficiencyInlineContent);

		return efficiencyInlineContent;
	}

	#renderEfficiencyPopupContent()
	{
		const [logoClass, notice] = this.#getEfficiencyData();

		const popup = Tag.render`
			<div class="bizproc-timeline-popup">
				<div class="bizproc-timeline-popup-title">
					${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_EFFECTIVITY_MARK')}
				</div>
				<div class="bizproc-timeline-popup-main">
					<div class="bizproc-timeline-popup-status">
						<div class="bizproc-timeline-popup-logo ${logoClass}"></div>
						<div class="bizproc-timeline-popup-notice">${notice}</div>
					</div>
					<div class="bizproc-timeline-popup-content">
						<div class="bizproc-timeline-popup-block">
							<span class="bizproc-timeline-popup-val">
								${DurationFormatter.formatTimeInterval(this.#data.executionTime)}
							</span>
							<span
								data-hint="${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_TIME_DIFFERENCE_MSGVER_1')}"
							></span>
							<div class="bizproc-timeline-popup-prop">
								${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_CURRENT_PROCESS_TIME')}
							</div>
						</div>
						<div class="bizproc-timeline-popup-block">
							<span class="bizproc-timeline-popup-val">
								${DurationFormatter.formatTimeInterval(this.#data.stats.averageDuration)}
							</span>
							<div class="bizproc-timeline-popup-prop">
								${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_AVERAGE_PROCESS_TIME')}
							</div>
						</div>
					</div>
				</div>
				<div class="bizproc-timeline-popup-footer">
					<p class="bizproc-timeline-popup-text">
						${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_PERFORMANCE_TUNING_TIP')}
					</p>
					<a class="bizproc-timeline-popup-text" href="javascript:top.BX.Helper.show('redirect=detail&code=18783714')">
						${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_PERFORMANCE_TUNING_LINK')}
					</a>
				</div>
			</div>
		`;
		BX.UI.Hint.init(popup);

		return popup;
	}

	showBiMenus(menu: Array<BiMenuItem>): void
	{
		this.#createBiButton(menu);
		this.#createBiPopup(menu).show();
	}

	#createBiButton(menu: Array<BiMenuItem>): void
	{
		const toolbarNode = document.querySelector('[data-role="page-toolbar"]');
		if (!toolbarNode)
		{
			return;
		}

		if (menu.length === 1)
		{
			const linkBtn = Tag.render`
				<a class="ui-btn ui-btn-light-border ui-btn-themes" href="${Text.encode(menu[0].URL)}" target="_blank">
					${Text.encode(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_BI_ANALYTICS_BUTTON'))}
				</a>
			`;

			Dom.prepend(linkBtn, toolbarNode);

			return;
		}

		const clickHandler = this.#showBiMenu.bind(this, menu);
		const dropBtn = Tag.render`
			<button class="ui-btn ui-btn-light-border ui-btn-themes ui-btn-dropdown" onclick="${clickHandler}">
				${Text.encode(Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_BI_ANALYTICS_BUTTON'))}
			</button>
		`;

		Dom.prepend(dropBtn, toolbarNode);
	}

	#createBiPopup(menu: Array<BiMenuItem>): Popup
	{
		this.#biPopup = new Popup({
			width: 403,
			minHeight: 183,
			closeIcon: true,
			content: this.#renderBiPopupContent(menu),
			bindElement: {
				left: 555,
				top: this.#getBiMenuTopOffset(),
			},
			padding: 17,
			borderRadius: '18px',
			className: '--bizproc-timeline-popup --bi',
		});

		return this.#biPopup;
	}

	#getBiMenuTopOffset(): number
	{
		const containerHeight = this.#efficiencyPopup?.getPopupContainer()?.offsetHeight;
		const topOffset = this.#efficiencyPopup?.bindElementPos?.top;
		if (containerHeight && topOffset)
		{
			return containerHeight + topOffset + 20;
		}

		return 522;
	}

	#showBiMenu(menu: Array<BiMenuItem>, event: Event): void
	{
		(new Menu({
			bindElement: event.target,
			items: menu.map((item: BiMenuItem) => {
				return {
					text: item.TEXT,
					href: item.URL,
					target: '_blank',
				};
			}),
		})).show();
	}

	#renderBiPopupContent(menu: Array<BiMenuItem>): Element
	{
		let btn = null;
		if (menu.length === 1)
		{
			btn = Tag.render`
				<a class="ui-btn ui-btn-light-border ui-btn-round ui-btn-xs" href="${Text.encode(menu[0].URL)}" target="_blank">
					<span class="ui-btn-text">${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_BI_ANALYTICS_LINK')}</span>
				</a>
			`;
		}
		else
		{
			const clickHandler = this.#showBiMenu.bind(this, menu);
			btn = Tag.render`
				<a 
					class="ui-btn ui-btn-light-border ui-btn-round ui-btn-xs ui-btn-dropdown"
					onclick="${clickHandler}"
				>
					<span class="ui-btn-text">${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_BI_ANALYTICS_LINK')}</span>
				</a>
			`;
		}

		return Tag.render`
			<div class="bizproc-timeline-popup">
				<div class="bizproc-timeline-popup-title">${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_BI_ANALYTICS_TITLE')}</div>
				<p class="bizproc-timeline-popup-info">${Loc.getMessage('BIZPROC_WORKFLOW_TIMELINE_SLIDER_BI_ANALYTICS_TIP')}</p>
				${btn}
			</div>
		`;
	}

	#renderLoadingStub(): HTMLElement
	{
		return Tag.render`
			<img src="/bitrix/js/bizproc/workflow/timeline/img/skeleton.svg"
				 style="width:100%; margin: 0; padding: 0;"/>
		`;
	}

	#hasErrors(): boolean
	{
		return this.#errors.length > 0;
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit