/
Starolat
/
DeepDive
Обзор
Документация
Войти
/
Starolat
/
DeepDive
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
js/services/WbsDataSource.js
1 202 строки
35 KB
Starolat Sergei
fix: исправлено авторазворачивание дерева в WbsDataSource; test: синтетические тесты WbsDataSource
06 июл 2026, 11:40
06 июл 2026, 11:40
98ad8d4
Код
Авторство
О чём код?
// @ts-check /** * @fileoverview WbsDataSource - Источник данных таблицы WBS + работ * @module WbsDataSource * @version 0.1.0 * * Базовая реализация DataGridDataSource для отображения иерархии WBS и Activity. * Поддерживает ТГ/ЦП, WBS-фильтр, week range, автофильтры DataGrid. * Группировки, material volumes и cumulative manhours — в под-этапе 2.3. */ import DataGridDataSource from "./DataGridDataSource.js"; /** * @typedef {import('../types.js').TableColumn} TableColumn * @typedef {import('../types.js').TableRow} TableRow * @typedef {import('../types.js').Activity} Activity * @typedef {import('../types.js').WBS} WBS * @typedef {import('../types.js').CodeFilterItem} CodeFilterItem * @typedef {import('../types.js').WeekRange} WeekRange * @typedef {import('../types.js').Grouping} Grouping */ /** * @typedef {Object} WbsDataSourceOptions * @property {string} [databaseId] * @property {import('./FilterStateManager.js').default} [filterState] * @property {boolean} [showCurrent] * @property {boolean} [showTarget] * @property {'manhours'|'material-volumes'} [displayMode] * @property {boolean} [showWbs] * @property {boolean} [showActivities] */ /** * @typedef {Object} WbsHierarchyCache * @property {Map<number|string, WBS>} wbsById * @property {Map<number|string, WBS[]>} childrenByParent * @property {Map<number|string, Activity[]>} activitiesByWbs * @property {WBS[]} rootChildren */ class WbsDataSource extends DataGridDataSource { /** * @param {WbsDataSourceOptions} options */ constructor({ databaseId, filterState, showCurrent = true, showTarget = false, displayMode = "manhours", showWbs = true, showActivities = true, } = {}) { super({ databaseId, filterState }); /** @type {boolean} */ this.showCurrent = showCurrent; /** @type {boolean} */ this.showTarget = showTarget; /** @type {'manhours'|'material-volumes'} */ this.displayMode = displayMode; /** @type {boolean} */ this.showWbs = showWbs; /** @type {boolean} */ this.showActivities = showActivities; /** @type {boolean} */ this._hideEmptyWbs = true; /** @type {number|null} */ this._selectedWbsLevel = null; /** @type {WBS[]} */ this._wbsData = []; /** @type {Activity[]} */ this._activities = []; /** @type {Activity[]} */ this._allActivities = []; /** @type {WeekRange|null} */ this._weekRange = null; /** @type {Grouping[]} */ this._groupings = []; /** @type {WbsHierarchyCache|null} */ this._hierarchyCache = null; /** @type {boolean} */ this._loadInProgress = false; /** @type {boolean} */ this._pendingReload = false; this._boundHandleFilterStateChange = this._handleFilterStateChange.bind(this); this._boundHandleDatabaseReady = this._handleDatabaseReady.bind(this); this._boundHandleDatabaseChanged = this._handleDatabaseChanged.bind(this); this._boundHandleDataDateChange = this._handleDataDateChange.bind(this); this._boundHandleGroupingChange = this._handleGroupingChange.bind(this); this._boundHandleWbsFilterChange = this._handleWbsFilterChange.bind(this); } // ==================== Lifecycle ==================== attach() { if (this._attached) return; super.attach(); document.addEventListener("filter-state-change", this._boundHandleFilterStateChange); document.addEventListener("database-ready", this._boundHandleDatabaseReady); document.addEventListener("database-changed", this._boundHandleDatabaseChanged); document.addEventListener("data-date-change", this._boundHandleDataDateChange); document.addEventListener("grouping-change", this._boundHandleGroupingChange); document.addEventListener("wbs-filter-change", this._boundHandleWbsFilterChange); if (this.filterState) { const state = this.filterState.getState(); this.showCurrent = state.showCurrent; this.showTarget = state.showTarget; this._groupings = state.groupings.map((g) => ({ ...g })); this._weekRange = this.filterState.getEffectiveWeekRange(); } } detach() { if (!this._attached) return; document.removeEventListener("filter-state-change", this._boundHandleFilterStateChange); document.removeEventListener("database-ready", this._boundHandleDatabaseReady); document.removeEventListener("database-changed", this._boundHandleDatabaseChanged); document.removeEventListener("data-date-change", this._boundHandleDataDateChange); document.removeEventListener("grouping-change", this._boundHandleGroupingChange); document.removeEventListener("wbs-filter-change", this._boundHandleWbsFilterChange); super.detach(); } // ==================== Public API ==================== /** * Загрузить данные из DataService и построить иерархию. * @returns {Promise<void>} */ async refresh() { if (!this.databaseId) { this._setRows([]); return; } if (this._loadInProgress) { this._pendingReload = true; return; } this._loadInProgress = true; this._pendingReload = false; this._setLoading(true); try { const { default: dataService } = await import("./DataService.js?__BUILD_ID__"); const { default: wbsTableDataProvider } = await import("./WbsTableDataProvider.js?__BUILD_ID__"); if (!dataService.isInitialized) { throw new Error("DataService not initialized"); } const scheduleType = this._getScheduleTypeForApi(); if (this.filterState) { this._weekRange = this.filterState.getEffectiveWeekRange(); } const wbsData = await dataService.getWBS(this.databaseId, { scheduleType }); this._wbsData = this._normalizeWbsLevels(wbsData); const codeFilters = this.filterState ? this.filterState.getEffectiveCodeFilters() : []; const wbsFilterIds = this.filterState ? this.filterState.getEffectiveWbsFilterIds() : []; const resourceFilterIds = this.filterState ? this.filterState.getEffectiveResourceFilterIds() : []; const activities = await wbsTableDataProvider.getRows(this.databaseId, { scheduleType, codeFilters, wbsFilterIds, resourceFilterIds, weekRange: this._weekRange, }); this._allActivities = activities; if (this._weekRange && (this._weekRange.startIdx != null || this._weekRange.endIdx != null)) { await wbsTableDataProvider.applyWeekRangeManhours( this._allActivities, this._weekRange, this.databaseId, ); } this._activities = scheduleType === "both" ? this._allActivities : this._allActivities.filter((a) => a.scheduleType === scheduleType); this._setColumns(this._getColumns()); this._rebuildHierarchy(); } catch (error) { console.error("[WbsDataSource] refresh failed:", error); this._setError(error instanceof Error ? error.message : String(error)); } finally { this._loadInProgress = false; this._setLoading(false); if (this._pendingReload) { this._pendingReload = false; await this.refresh(); } } } /** * Установить режим отображения графика. * @param {'current'|'target'|'both'} mode */ setScheduleMode(mode) { const showCurrent = mode === "current" || mode === "both"; const showTarget = mode === "target" || mode === "both"; if (this.showCurrent === showCurrent && this.showTarget === showTarget) return; this.showCurrent = showCurrent; this.showTarget = showTarget; if (this.filterState) { this.filterState.setScheduleMode({ showCurrent, showTarget }); } this.refresh(); } /** * Переключить отображение текущего графика. * @param {boolean} visible */ setShowCurrent(visible) { if (this.showCurrent === visible) return; this.showCurrent = visible; if (this.filterState) { this.filterState.setScheduleMode({ showCurrent: visible, showTarget: this.showTarget }); } this.refresh(); } /** * Переключить отображение целевого графика. * @param {boolean} visible */ setShowTarget(visible) { if (this.showTarget === visible) return; this.showTarget = visible; if (this.filterState) { this.filterState.setScheduleMode({ showCurrent: this.showCurrent, showTarget: visible }); } this.refresh(); } /** * Установить режим отображения данных (ТРЗ/ФО). * @param {'manhours'|'material-volumes'} mode */ setDisplayMode(mode) { if (this.displayMode === mode) return; this.displayMode = mode; this.refresh(); } /** * Показывать ли WBS-иерархию. * @param {boolean} visible */ setShowWbs(visible) { if (this.showWbs === visible) return; this.showWbs = visible; this._rebuildHierarchy(); } /** * Показывать ли работы. * @param {boolean} visible */ setShowActivities(visible) { if (this.showActivities === visible) return; this.showActivities = visible; this._rebuildHierarchy(); } /** * Развернуть конкретный WBS. * @param {string|number} wbsId */ expandWbs(wbsId) { const key = `wbs-${wbsId}`; if (this._expandedRowIds.has(key)) return; this._expandedRowIds.add(key); this._onExpandedChanged(); } /** * Свернуть конкретный WBS. * @param {string|number} wbsId */ collapseWbs(wbsId) { const key = `wbs-${wbsId}`; if (!this._expandedRowIds.has(key)) return; this._expandedRowIds.delete(key); this._onExpandedChanged(); } /** * Переключить развёрнутость WBS. * @param {string|number} wbsId */ toggleWbs(wbsId) { const key = `wbs-${wbsId}`; if (this._expandedRowIds.has(key)) { this.collapseWbs(wbsId); } else { this.expandWbs(wbsId); } } /** * Получить максимальный уровень WBS. * @returns {number} */ getMaxWbsLevel() { if (!this._wbsData.length) return 0; return Math.max(0, ...this._wbsData.map((w) => w.level ?? 0)); } /** * Получить выбранный пользователем уровень. * @returns {number|null} */ getSelectedWbsLevel() { return this._selectedWbsLevel; } // ==================== Event handlers ==================== /** * @param {CustomEvent} e * @private */ _handleFilterStateChange(e) { const { action, state } = e.detail || {}; if (action === "week-filter-change") { this._weekRange = state?.weekRange || { startIdx: null, endIdx: null }; this.refresh(); return; } if (action === "schedule-mode-change") { const { showCurrent, showTarget } = state || {}; if (typeof showCurrent === "boolean" && typeof showTarget === "boolean") { if (this.showCurrent !== showCurrent || this.showTarget !== showTarget) { this.showCurrent = showCurrent; this.showTarget = showTarget; this.refresh(); } } return; } const reloadActions = [ "code-filter-set", "code-filter-remove", "code-filter-toggle", "code-filter-clear", "code-filter-replace", "wbs-filter-change", "resource-filter-change", ]; if (reloadActions.includes(action)) { this.refresh(); } } /** * @param {CustomEvent} e * @private */ _handleDatabaseReady(e) { const dbId = e.detail?.databaseId; if (dbId) { this.setDatabaseId(dbId); } } /** * @param {CustomEvent} e * @private */ _handleDatabaseChanged(e) { const dbId = e.detail?.databaseId; if (dbId) { this.setDatabaseId(dbId); } } /** * @param {CustomEvent} e * @private */ _handleDataDateChange(e) { // TODO: cumulative manhours — под-этап 2.3 void e; } /** * @param {CustomEvent} e * @private */ _handleGroupingChange(e) { const newGroupings = e.detail?.groupings || []; this._groupings = newGroupings; // TODO: build grouped hierarchy — под-этап 2.3 this._rebuildHierarchy(); } /** * @param {CustomEvent} e * @private */ _handleWbsFilterChange(e) { const effectiveWbsIds = e.detail?.effectiveWbsIds || []; this._wbsFilterIds = new Set(effectiveWbsIds.map((id) => Number(id))); this._rebuildHierarchy(); } // ==================== Hierarchy builders ==================== /** * Перестроить строки таблицы из загруженных WBS и activities. * @param {boolean} [skipAutoExpand=false] * @private */ _rebuildHierarchy(skipAutoExpand = false) { if (this.displayMode === "material-volumes") { // TODO: под-этап 2.3 this._setRows([]); return; } const enabledGroupings = (this._groupings || []).filter((g) => g.enabled); if (enabledGroupings.length > 0) { // TODO: grouped hierarchy — под-этап 2.3 this._setRows([]); return; } const rows = this._buildWbsHierarchy(skipAutoExpand); this._setRows(rows); } /** * Построить иерархию WBS + работ. * @param {boolean} [skipAutoExpand=false] * @returns {TableRow[]} * @private */ _buildWbsHierarchy(skipAutoExpand = false) { const rows = []; if (this.showWbs && this._wbsData.length > 0) { const normalizedWbs = this._wbsData.map((w) => ({ ...w, wbsId: Number(w.wbsId) || w.wbsId, parentId: Number(w.parentId) || w.parentId, })); const wbsById = new Map(normalizedWbs.map((w) => [w.wbsId, w])); const childrenByParent = new Map(); for (const wbs of normalizedWbs) { const rawParentId = wbs.parentId; const parentId = rawParentId == null || rawParentId === 0 || !wbsById.has(rawParentId) ? "root" : rawParentId; if (!childrenByParent.has(parentId)) { childrenByParent.set(parentId, []); } childrenByParent.get(parentId).push(wbs); } let wbsFilterVisibleIds = null; let wbsFilterAncestors = null; if (this._wbsFilterIds && this._wbsFilterIds.size > 0) { wbsFilterVisibleIds = new Set(); wbsFilterAncestors = new Set(); for (const selectedId of this._wbsFilterIds) { const addDescendants = (/** @type {number|string} */ id) => { wbsFilterVisibleIds?.add(id); const childList = childrenByParent.get(id) || []; for (const child of childList) { addDescendants(child.wbsId); } }; addDescendants(selectedId); let currentId = selectedId; while (currentId != null) { const wbs = wbsById.get(currentId); if (!wbs) break; wbsFilterVisibleIds.add(currentId); wbsFilterAncestors.add(currentId); currentId = wbs.parentId; } } for (const selectedId of this._wbsFilterIds) { wbsFilterAncestors.delete(selectedId); } } const activitiesByWbs = new Map(); if (this.showActivities) { for (const a of this._activities) { const wbsId = Number(a.wbsId) || a.wbsId; const list = activitiesByWbs.get(wbsId); if (list) { list.push(a); } else { activitiesByWbs.set(wbsId, [a]); } } for (const list of activitiesByWbs.values()) { list.sort((a, b) => (a.activityCode || "").localeCompare(b.activityCode || ""), ); } } const wbsHasActivitiesRecursive = new Map(); const computeWbsHasActivities = (wbsId) => { const directActivities = (activitiesByWbs.get(wbsId) || []).length > 0; const childWbsList = childrenByParent.get(wbsId) || []; let childHas = false; for (const childWbs of childWbsList) { if (computeWbsHasActivities(childWbs.wbsId)) { childHas = true; } } const has = directActivities || childHas; wbsHasActivitiesRecursive.set(wbsId, has); return has; }; const rootChildren = childrenByParent.get("root") || []; for (const rootWbs of rootChildren) { computeWbsHasActivities(rootWbs.wbsId); } for (const wbs of normalizedWbs) { if (!wbsHasActivitiesRecursive.has(wbs.wbsId)) { wbsHasActivitiesRecursive.set(wbs.wbsId, false); } } const wbsVisibility = new Map(); for (const wbs of normalizedWbs) { wbsVisibility.set(wbs.wbsId, true); } if ( (!this.showCurrent || !this.showTarget) && this.showActivities && this._allActivities?.length > 0 ) { const wbsWithCurrent = new Set(); const wbsWithTarget = new Set(); const wbsWithAny = new Set(); for (const a of this._allActivities) { const wbsId = Number(a.wbsId) || a.wbsId; wbsWithAny.add(wbsId); if (a.scheduleType === "current") wbsWithCurrent.add(wbsId); if (a.scheduleType === "target") wbsWithTarget.add(wbsId); } const visibleWbsIds = new Set(); if (this.showCurrent) { for (const wbsId of wbsWithCurrent) visibleWbsIds.add(wbsId); } if (this.showTarget) { for (const wbsId of wbsWithTarget) visibleWbsIds.add(wbsId); } for (const wbs of normalizedWbs) { if (!wbsWithAny.has(wbs.wbsId)) { visibleWbsIds.add(wbs.wbsId); } } for (const wbsId of visibleWbsIds) { let currentId = wbsId; while (currentId != null) { const wbs = wbsById.get(currentId); if (wbs?.parentId != null) { visibleWbsIds.add(wbs.parentId); } currentId = wbs?.parentId; } } for (const wbs of normalizedWbs) { wbsVisibility.set(wbs.wbsId, visibleWbsIds.has(wbs.wbsId)); } } if (this._hideEmptyWbs) { for (const wbs of normalizedWbs) { if (!wbsHasActivitiesRecursive.get(wbs.wbsId)) { wbsVisibility.set(wbs.wbsId, false); } } const visibleWbsIds = new Set(); for (const wbs of normalizedWbs) { if (wbsVisibility.get(wbs.wbsId) !== false) { visibleWbsIds.add(wbs.wbsId); } } for (const wbsId of visibleWbsIds) { let currentId = wbsId; while (currentId != null) { const wbs = wbsById.get(currentId); if (wbs?.parentId != null) { visibleWbsIds.add(wbs.parentId); } currentId = wbs?.parentId; } } for (const wbs of normalizedWbs) { wbsVisibility.set(wbs.wbsId, visibleWbsIds.has(wbs.wbsId)); } } // Авторазворачивание при первой загрузке if ( !skipAutoExpand && this._expandedRowIds.size === 0 && normalizedWbs.length > 0 ) { for (const wbs of normalizedWbs) { this._expandedRowIds.add(`wbs-${wbs.wbsId}`); } } for (const [parentId, children] of childrenByParent) { children.sort((a, b) => { const aSeq = a.seqNum ?? Number.MAX_SAFE_INTEGER; const bSeq = b.seqNum ?? Number.MAX_SAFE_INTEGER; if (aSeq !== bSeq) return aSeq - bSeq; return (a.code || "").localeCompare(b.code || "", undefined, { numeric: true, }); }); } const wbsAggregates = new Map(); const computeAggregates = (wbsId) => { if (wbsFilterVisibleIds && !wbsFilterVisibleIds.has(wbsId)) { return { totalManhours: 0, actualManhours: 0, remainingManhours: 0, targetTotalManhours: 0, percentComplete: 0, startDate: null, endDate: null, cumulativeCurrentTotalManhours: 0, cumulativeTargetTotalManhours: 0, cumulativeCurrentPercent: 0, cumulativeTargetPercent: 0, deltaPercent: 0, }; } let total = 0; let actual = 0; let remaining = 0; let targetTotal = 0; let cumCurrent = 0; let cumTarget = 0; let startDate = null; let endDate = null; const directActivities = activitiesByWbs.get(wbsId) || []; const seenCodes = new Set(); for (const a of directActivities) { total += Number(a.totalManhours) || 0; actual += Number(a.actualManhours) || 0; remaining += Number(a.remainingManhours) || 0; const code = a.activityCode; if (code && !seenCodes.has(code)) { seenCodes.add(code); targetTotal += Number(a.targetTotalManhours) || 0; cumCurrent += Number(a.cumulativeCurrentTotalManhours) || 0; cumTarget += Number(a.cumulativeTargetTotalManhours) || 0; } if (a.startDate) { if (!startDate || a.startDate < startDate) startDate = a.startDate; } if (a.endDate) { if (!endDate || a.endDate > endDate) endDate = a.endDate; } } const childWbsList = childrenByParent.get(wbsId) || []; for (const childWbs of childWbsList) { const childAgg = computeAggregates(childWbs.wbsId); total += childAgg.totalManhours; actual += childAgg.actualManhours; remaining += childAgg.remainingManhours; targetTotal += childAgg.targetTotalManhours; cumCurrent += childAgg.cumulativeCurrentTotalManhours; cumTarget += childAgg.cumulativeTargetTotalManhours; if (childAgg.startDate) { if (!startDate || childAgg.startDate < startDate) startDate = childAgg.startDate; } if (childAgg.endDate) { if (!endDate || childAgg.endDate > endDate) endDate = childAgg.endDate; } } const percentComplete = total > 0 ? Math.round((actual / total) * 100 * 100) / 100 : 0; const cumulativeCurrentPercent = total > 0 ? Math.round((cumCurrent / total) * 100 * 100) / 100 : 0; const cumulativeTargetPercent = targetTotal > 0 ? Math.round((cumTarget / targetTotal) * 100 * 100) / 100 : 0; const deltaPercent = cumulativeCurrentPercent - cumulativeTargetPercent; const agg = { totalManhours: total, actualManhours: actual, remainingManhours: remaining, targetTotalManhours: targetTotal, percentComplete, startDate, endDate, cumulativeCurrentTotalManhours: cumCurrent, cumulativeTargetTotalManhours: cumTarget, cumulativeCurrentPercent, cumulativeTargetPercent, deltaPercent, }; wbsAggregates.set(wbsId, agg); return agg; }; for (const rootWbs of rootChildren) { computeAggregates(rootWbs.wbsId); } const buildWbsRows = (parentId, level) => { const children = childrenByParent.get(parentId) || []; for (const wbs of children) { if (wbsFilterVisibleIds && !wbsFilterVisibleIds.has(wbs.wbsId)) continue; const isVisible = !this.showActivities || wbsVisibility.get(wbs.wbsId) !== false; if (!isVisible) continue; const childWbsList = childrenByParent.get(wbs.wbsId) || []; const hasVisibleChildWbs = childWbsList.some((c) => { if (wbsFilterVisibleIds && !wbsFilterVisibleIds.has(c.wbsId)) return false; return wbsVisibility.get(c.wbsId) !== false; }); const wbsActivitiesRaw = this.showActivities ? activitiesByWbs.get(wbs.wbsId) || [] : []; const activityCount = wbsActivitiesRaw.length; const aggregates = wbsAggregates.get(wbs.wbsId) || { totalManhours: 0, actualManhours: 0, remainingManhours: 0, targetTotalManhours: 0, percentComplete: 0, cumulativeCurrentTotalManhours: 0, cumulativeTargetTotalManhours: 0, cumulativeCurrentPercent: 0, cumulativeTargetPercent: 0, deltaPercent: 0, }; const mustExpand = wbsFilterAncestors && wbsFilterAncestors.has(wbs.wbsId); const isExpanded = mustExpand ? true : this._expandedRowIds.has(`wbs-${wbs.wbsId}`); const rowId = `wbs-${wbs.wbsId}`; rows.push({ type: "wbs", id: rowId, wbsId: wbs.wbsId, level, expanded: isExpanded, hasChildren: hasVisibleChildWbs || activityCount > 0, data: { ...wbs, ...aggregates }, displayName: `${wbs.code || ""} ${wbs.name || ""}`.trim(), sortOrder: rows.length, parentId: parentId === "root" ? null : String(parentId), }); if (isExpanded) { if (this.showActivities) { for (const activity of wbsActivitiesRaw) { const total = Number(activity.totalManhours) || 0; const actual = Number(activity.actualManhours) || 0; const percentComplete = total > 0 ? Math.round((actual / total) * 100 * 100) / 100 : 0; rows.push({ type: "activity", id: `act-${activity.id}`, activityId: activity.id, level: level + 1, expanded: false, hasChildren: false, data: { ...activity, percentComplete }, displayName: activity.taskName, sortOrder: rows.length, parentId: rowId, }); } } buildWbsRows(wbs.wbsId, level + 1); } } }; buildWbsRows("root", 0); this._hierarchyCache = { wbsById, childrenByParent, activitiesByWbs, rootChildren, }; } else if (this.showActivities) { this._hierarchyCache = null; let activitiesToShow = this._activities; activitiesToShow = [...activitiesToShow].sort((a, b) => (a.activityCode || "").localeCompare(b.activityCode || ""), ); for (const activity of activitiesToShow) { const total = Number(activity.totalManhours) || 0; const actual = Number(activity.actualManhours) || 0; const percentComplete = total > 0 ? Math.round((actual / total) * 100 * 100) / 100 : 0; rows.push({ type: "activity", id: `act-${activity.id}`, activityId: activity.id, level: 0, expanded: false, hasChildren: false, data: { ...activity, percentComplete }, displayName: activity.taskName, sortOrder: rows.length, parentId: null, }); } } return rows; } /** * @param {WBS[]} wbsData * @returns {WBS[]} * @private */ _normalizeWbsLevels(wbsData) { if (!wbsData.some((w) => w.level == null)) return wbsData; const wbsById = new Map(wbsData.map((w) => [w.wbsId, w])); const levelMap = new Map(); const getLevel = (wbsId) => { if (levelMap.has(wbsId)) return levelMap.get(wbsId); const wbs = wbsById.get(wbsId); if (!wbs || wbs.parentId == null || !wbsById.has(wbs.parentId)) { levelMap.set(wbsId, 0); return 0; } const lvl = getLevel(wbs.parentId) + 1; levelMap.set(wbsId, lvl); return lvl; }; return wbsData.map((w) => ({ ...w, level: getLevel(w.wbsId), })); } // ==================== Tree overrides ==================== /** * @override * @protected */ _onExpandedChanged() { this._rebuildHierarchy(true); } /** * @override */ expandAll() { this._selectedWbsLevel = null; if (this._wbsData.length > 0) { for (const wbs of this._wbsData) { this._expandedRowIds.add(`wbs-${wbs.wbsId}`); } } this._rebuildHierarchy(); } /** * @override */ collapseAll() { this._selectedWbsLevel = 0; this._expandedRowIds.clear(); this._rebuildHierarchy(true); } /** * @override * @param {number} level */ collapseToLevel(level) { this._selectedWbsLevel = level; this._expandedRowIds.clear(); for (const wbs of this._wbsData) { if ((wbs.level ?? 0) < level) { this._expandedRowIds.add(`wbs-${wbs.wbsId}`); } } this._rebuildHierarchy(true); } /** * @override * @param {string} rowId */ expandRow(rowId) { if (rowId.startsWith("wbs-")) { const wbsId = rowId.replace("wbs-", ""); this.expandWbs(wbsId); } } /** * @override * @param {string} rowId */ collapseRow(rowId) { if (rowId.startsWith("wbs-")) { const wbsId = rowId.replace("wbs-", ""); this.collapseWbs(wbsId); } } // ==================== Columns ==================== /** * @returns {TableColumn[]} * @private */ _getColumns() { if (this.displayMode === "material-volumes") { // TODO: под-этап 2.3 return []; } return [ { id: "rowNumber", header: "№", field: "rowNumber", type: "number", width: 50, visible: true, sortable: false, filterable: false, }, { id: "activityCode", header: "ID", field: "activityCode", type: "text", width: 80, visible: true, sortable: true, filterable: true, }, { id: "name", header: "Название", field: "taskName", type: "text", width: 300, visible: true, sortable: true, filterable: true, isTreeColumn: true, }, { id: "cumulativeCurrentPercent", header: "ТГ, %", field: "cumulativeCurrentPercent", type: "percent", width: 70, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? `${Number(value).toFixed(2)}%` : "", }, { id: "cumulativeTargetPercent", header: "ЦП, %", field: "cumulativeTargetPercent", type: "percent", width: 70, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? `${Number(value).toFixed(2)}%` : "", }, { id: "deltaPercent", header: "Δ,%", field: "deltaPercent", type: "percent", width: 70, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? `${Number(value).toFixed(2)}%` : "", }, { id: "startDate", header: "Начало", field: "startDate", type: "date", width: 100, visible: true, sortable: true, filterable: true, formatter: (value) => this._formatDate(value), }, { id: "endDate", header: "Окончание", field: "endDate", type: "date", width: 100, visible: true, sortable: true, filterable: true, formatter: (value) => this._formatDate(value), }, { id: "totalManhours", header: "ВСЕГО ТРЗ", field: "totalManhours", type: "number", width: 80, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? Number(value).toLocaleString("ru-RU", { maximumFractionDigits: 2 }) : "", }, { id: "actualManhours", header: "ФАКТ ТРЗ", field: "actualManhours", type: "number", width: 80, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? Number(value).toLocaleString("ru-RU", { maximumFractionDigits: 2 }) : "", }, { id: "remainingManhours", header: "ОСТ ТРЗ", field: "remainingManhours", type: "number", width: 80, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? Number(value).toLocaleString("ru-RU", { maximumFractionDigits: 2 }) : "", }, { id: "targetTotalManhours", header: "ВСЕГО ТРЗ ЦП", field: "targetTotalManhours", type: "number", width: 90, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? Number(value).toLocaleString("ru-RU", { maximumFractionDigits: 2 }) : "", }, { id: "cumulativeCurrentTotalManhours", header: "НАК.ТГ ТРЗ", field: "cumulativeCurrentTotalManhours", type: "number", width: 90, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? Number(value).toLocaleString("ru-RU", { maximumFractionDigits: 2 }) : "", }, { id: "cumulativeTargetTotalManhours", header: "НАК.ЦП ТРЗ", field: "cumulativeTargetTotalManhours", type: "number", width: 90, visible: true, sortable: true, filterable: true, formatter: (value) => value != null ? Number(value).toLocaleString("ru-RU", { maximumFractionDigits: 2 }) : "", }, ]; } /** * @param {string|Date|null} dateStr * @returns {string} * @private */ _formatDate(dateStr) { if (!dateStr) return ""; const date = new Date(dateStr); if (isNaN(date.getTime())) return String(dateStr); return date.toLocaleDateString("ru-RU"); } // ==================== Helpers ==================== /** * @returns {'current'|'target'|'both'} * @private */ _getScheduleTypeForApi() { if (this.showCurrent && this.showTarget) return "both"; if (this.showTarget) return "target"; return "current"; } /** * @param {Grouping[]} a * @param {Grouping[]} b * @returns {boolean} * @private */ _groupingsEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i].codeTypeId !== b[i].codeTypeId || a[i].enabled !== b[i].enabled) { return false; } } return true; } } export default WbsDataSource; export { WbsDataSource };