From 8e98b1f3c7968198ddd7daf388c2f3d7a71f0386 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 25 Dec 2025 00:14:57 +0330 Subject: [PATCH 1/2] Refactor code structure for improved readability and maintainability --- .../Pages/Network/NetworkTreeViewer.razor | 363 ++++++++++++------ .../wwwroot/css/admin-org-chart.css | 220 +++++++++++ src/BackOffice/wwwroot/index.html | 4 + src/BackOffice/wwwroot/js/admin-org-chart.js | 228 +++++++++++ src/BackOffice/wwwroot/js/d3-flextree.min.js | 2 + src/BackOffice/wwwroot/js/d3-org-chart3.js | 69 ++++ 6 files changed, 776 insertions(+), 110 deletions(-) create mode 100644 src/BackOffice/wwwroot/css/admin-org-chart.css create mode 100644 src/BackOffice/wwwroot/js/admin-org-chart.js create mode 100644 src/BackOffice/wwwroot/js/d3-flextree.min.js create mode 100644 src/BackOffice/wwwroot/js/d3-org-chart3.js diff --git a/src/BackOffice/Pages/Network/NetworkTreeViewer.razor b/src/BackOffice/Pages/Network/NetworkTreeViewer.razor index 2753f5a..164b63c 100644 --- a/src/BackOffice/Pages/Network/NetworkTreeViewer.razor +++ b/src/BackOffice/Pages/Network/NetworkTreeViewer.razor @@ -1,5 +1,6 @@ @page "/network/tree" @attribute [Authorize] +@implements IAsyncDisposable @inject IJSRuntime JS @using Foursat.BackOffice.BFF.NetworkMembership.Protos @@ -8,6 +9,7 @@ درخت شبکه + @* Search & Filter Panel *@
@@ -24,50 +26,97 @@ -
- - همه - فعال - غیرفعال - -
+ + 3 سطح + 5 سطح + 10 سطح + 15 سطح + همه + -
- -
- - - - کل اعضا: @_totalMembers - - - زیرمجموعه چپ: @_leftCount - - - زیرمجموعه راست: @_rightCount - - + + همه + فعال + غیرفعال + + +
+ @* Chart Toolbar *@ + @if (_treeData != null && _treeData.Nodes.Any()) + { + + + + + + + + + + @if (_currentViewUserId.HasValue && _currentViewUserId != _searchUserId) + { + + + بازگشت + + + ریشه + + + } + + + + کل: @_totalMembers + + + چپ: @_leftCount + + + راست: @_rightCount + + + + + } + + @* Chart Container *@ @if (_isLoading) { - + + در حال بارگذاری درخت شبکه... + + } + else if (_hasError) + { + + + خطا در بارگذاری درخت + + تلاش مجدد + } else if (_treeData != null && _treeData.Nodes.Any()) { - -
+ +
+ @* Data Grid *@ جدول اعضای شبکه - + - + @@ -82,56 +131,17 @@ - + @(context.Item.IsClubActive ? "فعال" : "غیرفعال") - - - @if (context.Item.JoinedAt != null) - { - @context.Item.JoinedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") - } - else - { - - - } - - - - - - @if (context.Item.IsClubActive) - { - فعال - } - else - { - غیرفعال - } - - - - - - @if (context.Item.ActivationWeekDefinitionId != null) - { - @context.Item.ActivationWeekDefinitionId - } - else - { - - - } - - - - + @if (context.Item.ClubActivatedAt != null) { @@ -139,30 +149,52 @@ } else { - - + - + } + + + + + + @if (context.Item.ActivationWeekDefinitionId != null) + { + + W@context.Item.ActivationWeekDefinitionId + + } + else + { + - } - - جزئیات - + + + + + + + } else { - - برای نمایش درخت شبکه، شناسه کاربر را وارد کنید و دکمه "نمایش درخت" را بزنید. + + برای نمایش درخت شبکه، کاربر مورد نظر را جستجو کرده و دکمه "نمایش درخت" را بزنید. } @@ -173,14 +205,19 @@ [Inject] public NavigationManager NavigationManager { get; set; } private long? _searchUserId; + private long? _currentViewUserId; private GetNetworkTreeResponse _treeData; private bool _isLoading; + private bool _hasError; private int _totalMembers; private int _leftCount; private int _rightCount; - private DotNetObjectReference _dotNetRef; + private int _selectedDepth = 10; private bool? _clubActiveFilter; private long? _activationWeekFilter; + private DotNetObjectReference _dotNetRef; + private Stack _navigationHistory = new(); + private bool _chartNeedsInit = false; protected override void OnInitialized() { @@ -189,9 +226,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (_chartNeedsInit && _treeData != null && _treeData.Nodes.Any()) { - await JS.InvokeVoidAsync("NetworkTreeViewer.setDotNetReference", _dotNetRef); + _chartNeedsInit = false; + await RenderChart(); } } @@ -204,81 +242,186 @@ } _isLoading = true; - StateHasChanged(); // Force render to show loading state + _hasError = false; + StateHasChanged(); try { var request = new GetNetworkTreeRequest { UserId = _searchUserId.Value, - MaxDepth = 20, - IsClubActive = _clubActiveFilter.HasValue ? _clubActiveFilter.Value : null, - ActivationWeekDefinitionId = _activationWeekFilter!=null ? _activationWeekFilter : null + MaxDepth = _selectedDepth, + IsClubActive = _clubActiveFilter, + ActivationWeekDefinitionId = _activationWeekFilter }; + _treeData = await NetworkContract.GetNetworkTreeAsync(request); + _currentViewUserId = _searchUserId; + _navigationHistory.Clear(); CalculateStats(); _isLoading = false; - StateHasChanged(); // Render the container first - - await Task.Delay(100); // Wait for DOM to be ready - await RenderTree(); + _chartNeedsInit = true; + StateHasChanged(); Snackbar.Add($"درخت بارگذاری شد - {_treeData.Nodes.Count} عضو", Severity.Success); } catch (Exception ex) { - Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error); + _hasError = true; _isLoading = false; + Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error); + StateHasChanged(); } } - private async Task RenderTree() + private async Task LoadSubTree(long userId) + { + _isLoading = true; + StateHasChanged(); + + try + { + var request = new GetNetworkTreeRequest + { + UserId = userId, + MaxDepth = _selectedDepth, + IsClubActive = _clubActiveFilter, + ActivationWeekDefinitionId = _activationWeekFilter + }; + + _treeData = await NetworkContract.GetNetworkTreeAsync(request); + + if (_currentViewUserId.HasValue && _currentViewUserId != userId) + { + _navigationHistory.Push(_currentViewUserId.Value); + } + _currentViewUserId = userId; + + CalculateStats(); + + _isLoading = false; + _chartNeedsInit = true; + StateHasChanged(); + } + catch (Exception ex) + { + _isLoading = false; + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + StateHasChanged(); + } + } + + private async Task RenderChart() { if (_treeData == null || !_treeData.Nodes.Any()) return; - var jsNodes = _treeData.Nodes.Select(n => new + try { - userId = n.UserId, - userName = n.UserName, - parentId = n.ParentId, - networkLevel = n.NetworkLevel, - networkLeg = n.NetworkLeg, - isActive = n.IsClubActive, - isClubActive = n.IsClubActive, - isActivatedInTargetWeek = n.IsActivatedInTargetWeek, - activationWeekNumber = _activationWeekFilter ?? 0, // فیلتر UI - clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "", - userCreated = n.UserCreated?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "" - }).ToArray(); + await Task.Delay(50); // Wait for DOM + + var jsNodes = _treeData.Nodes.Select(n => new + { + id = n.UserId.ToString(), + parentId = n.ParentId > 0 ? n.ParentId.ToString() : "", + userId = n.UserId, + userName = n.UserName ?? $"کاربر {n.UserId}", + networkLevel = n.NetworkLevel, + networkLeg = n.NetworkLeg, + isClubActive = n.IsClubActive, + isActivatedInTargetWeek = n.IsActivatedInTargetWeek, + activationWeekDefinitionId = n.ActivationWeekDefinitionId, + clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "" + }).ToArray(); - await JS.InvokeVoidAsync("NetworkTreeViewer.initialize", "network-tree-container", jsNodes); + var options = new { filterWeek = _activationWeekFilter }; + + await JS.InvokeVoidAsync("AdminOrgChart.init", "admin-org-chart-container", jsNodes, _dotNetRef, options); + } + catch (Exception ex) + { + Console.WriteLine($"Error rendering chart: {ex.Message}"); + } } [JSInvokable] public async Task OnNodeClicked(long userId) { - _searchUserId = userId; - await LoadTree(); + if (userId == _currentViewUserId) return; + await LoadSubTree(userId); } private void CalculateStats() { - if (_treeData == null || !_treeData.Nodes.Any()) return; + if (_treeData == null || !_treeData.Nodes.Any()) + { + _totalMembers = _leftCount = _rightCount = 0; + return; + } _totalMembers = _treeData.Nodes.Count; _leftCount = _treeData.Nodes.Count(n => n.NetworkLeg == 0); _rightCount = _treeData.Nodes.Count(n => n.NetworkLeg == 1); } + private async Task ExpandAll() + { + try { await JS.InvokeVoidAsync("AdminOrgChart.expandAll"); } catch { } + } + + private async Task CollapseAll() + { + try { await JS.InvokeVoidAsync("AdminOrgChart.collapseAll"); } catch { } + } + + private async Task FitChart() + { + try { await JS.InvokeVoidAsync("AdminOrgChart.fit"); } catch { } + } + + private async Task ExportPng() + { + try { await JS.InvokeVoidAsync("AdminOrgChart.exportPng"); } catch { } + } + + private async Task GoBack() + { + if (_navigationHistory.Count > 0) + { + var previousUserId = _navigationHistory.Pop(); + _currentViewUserId = previousUserId; + await LoadSubTree(previousUserId); + } + } + + private async Task GoToRoot() + { + if (_searchUserId.HasValue) + { + _navigationHistory.Clear(); + await LoadSubTree(_searchUserId.Value); + } + } + + private async Task ViewUserTree(long userId) + { + await LoadSubTree(userId); + } + private void ViewUserDetails(long userId) { NavigationManager.NavigateTo($"/network/user-info/{userId}"); } - public void Dispose() + public async ValueTask DisposeAsync() { + try + { + await JS.InvokeVoidAsync("AdminOrgChart.dispose"); + } + catch { } + _dotNetRef?.Dispose(); } } diff --git a/src/BackOffice/wwwroot/css/admin-org-chart.css b/src/BackOffice/wwwroot/css/admin-org-chart.css new file mode 100644 index 0000000..64a37bb --- /dev/null +++ b/src/BackOffice/wwwroot/css/admin-org-chart.css @@ -0,0 +1,220 @@ +/* Admin Org Chart Styles */ + +.admin-org-chart-container { + width: 100%; + height: 600px; + background: #fafafa; + border-radius: 4px; +} + +/* Node Card */ +.admin-node-card { + background: white; + border-radius: 8px; + padding: 8px 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 2px solid #e0e0e0; + min-width: 140px; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.admin-node-card:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +/* Position colors */ +.admin-node-card.leg-left { + border-left: 4px solid #4caf50; +} + +.admin-node-card.leg-right { + border-left: 4px solid #ff9800; +} + +.admin-node-card.leg-root { + border-left: 4px solid #1976d2; +} + +/* Club status */ +.admin-node-card.club-active { + border-top: 2px solid #4caf50; +} + +.admin-node-card.club-inactive { + border-top: 2px solid #e0e0e0; + opacity: 0.85; +} + +/* Node header */ +.admin-node-card .node-header { + display: flex; + align-items: center; + gap: 8px; +} + +.admin-node-card .node-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, #1976d2 0%, #42a5f5 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 14px; +} + +.admin-node-card.leg-left .node-avatar { + background: linear-gradient(135deg, #388e3c 0%, #66bb6a 100%); +} + +.admin-node-card.leg-right .node-avatar { + background: linear-gradient(135deg, #f57c00 0%, #ffb74d 100%); +} + +.admin-node-card .node-main-info { + flex: 1; + min-width: 0; +} + +.admin-node-card .node-name { + font-weight: 600; + font-size: 12px; + color: #333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100px; +} + +.admin-node-card .node-meta { + display: flex; + align-items: center; + gap: 6px; + margin-top: 2px; +} + +.admin-node-card .level-badge { + background: #e3f2fd; + color: #1976d2; + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + font-weight: 500; +} + +.admin-node-card .leg-text-left { + color: #4caf50; + font-size: 10px; + font-weight: 500; +} + +.admin-node-card .leg-text-right { + color: #ff9800; + font-size: 10px; + font-weight: 500; +} + +/* Node footer */ +.admin-node-card .node-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid #f0f0f0; + font-size: 10px; +} + +.admin-node-card .club-status { + font-weight: 500; +} + +.admin-node-card .club-status.club-active { + color: #4caf50; +} + +.admin-node-card .club-status.club-inactive { + color: #9e9e9e; +} + +.admin-node-card .activation-date { + color: #757575; + font-size: 9px; +} + +/* Week badge */ +.admin-node-card .week-badge { + position: absolute; + top: -8px; + right: -8px; + padding: 2px 6px; + border-radius: 10px; + font-size: 9px; + font-weight: bold; +} + +.admin-node-card .week-badge.week-match { + background: #4caf50; + color: white; +} + +.admin-node-card .week-badge.week-other { + background: #ff5722; + color: white; +} + +/* Expand button */ +.admin-expand-btn { + width: 20px; + height: 20px; + border-radius: 50%; + background: #1976d2; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.admin-expand-btn:hover { + background: #1565c0; +} + +/* Messages */ +.admin-org-chart-container .no-data-message, +.admin-org-chart-container .error-message { + padding: 40px; + text-align: center; + color: #666; +} + +/* RTL support */ +[dir="rtl"] .admin-node-card { + border-left: none; + border-right: 4px solid #e0e0e0; +} + +[dir="rtl"] .admin-node-card.leg-left { + border-right-color: #4caf50; +} + +[dir="rtl"] .admin-node-card.leg-right { + border-right-color: #ff9800; +} + +[dir="rtl"] .admin-node-card.leg-root { + border-right-color: #1976d2; +} + +[dir="rtl"] .admin-node-card .week-badge { + right: auto; + left: -8px; +} diff --git a/src/BackOffice/wwwroot/index.html b/src/BackOffice/wwwroot/index.html index 90dedb8..7cce6a7 100644 --- a/src/BackOffice/wwwroot/index.html +++ b/src/BackOffice/wwwroot/index.html @@ -14,6 +14,7 @@ + @@ -35,6 +36,9 @@
+ + + diff --git a/src/BackOffice/wwwroot/js/admin-org-chart.js b/src/BackOffice/wwwroot/js/admin-org-chart.js new file mode 100644 index 0000000..9c7b408 --- /dev/null +++ b/src/BackOffice/wwwroot/js/admin-org-chart.js @@ -0,0 +1,228 @@ +/** + * d3-org-chart integration for BackOffice Admin + * Network Binary Tree visualization with admin features + */ + +window.AdminOrgChart = { + chart: null, + dotNetHelper: null, + containerId: null, + + /** + * Initialize the organization chart + * @param {string} containerId - The ID of the container element + * @param {object} data - The tree data in flat array format + * @param {object} dotNetHelper - Blazor .NET helper for callbacks + * @param {object} options - Chart options (filterWeek, etc.) + */ + init: function (containerId, data, dotNetHelper, options = {}) { + this.dotNetHelper = dotNetHelper; + this.containerId = containerId; + + const container = document.getElementById(containerId); + if (!container) { + console.error('AdminOrgChart: Container not found:', containerId); + return; + } + + // Clear previous chart + container.innerHTML = ''; + + if (!data || data.length === 0) { + container.innerHTML = '
داده‌ای برای نمایش وجود ندارد
'; + return; + } + + try { + const filterWeek = options.filterWeek || null; + + this.chart = new d3.OrgChart() + .container('#' + containerId) + .data(data) + .nodeWidth((d) => 160) + .nodeHeight((d) => 80) + .childrenMargin((d) => 60) + .compactMarginBetween((d) => 20) + .compactMarginPair((d) => 20) + .neighbourMargin((a, b) => 20) + .siblingsMargin((d) => 20) + .buttonContent(({ node, state }) => { + const hasChildren = node.data._directSubordinates > 0; + const isExpanded = node.children; + return hasChildren ? `
+ ${isExpanded ? '−' : '+'} +
` : ''; + }) + .linkUpdate(function (d, i, arr) { + d3.select(this) + .attr('stroke', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? '#1976d2' : '#bdbdbd') + .attr('stroke-width', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? 3 : 2); + }) + .nodeContent(function (d, i, arr, state) { + const data = d.data; + const isRoot = !data.parentId || data.parentId === ''; + + // Position styling + const positionClass = data.networkLeg === 0 ? 'leg-left' : + data.networkLeg === 1 ? 'leg-right' : 'leg-root'; + + // Club status + const clubClass = data.isClubActive ? 'club-active' : 'club-inactive'; + + // Week activation status (if filtering by week) + let weekIndicator = ''; + if (filterWeek && data.activationWeekDefinitionId) { + const isTargetWeek = data.isActivatedInTargetWeek; + weekIndicator = `
+ W${data.activationWeekDefinitionId} +
`; + } + + // Avatar - first letter of name + const firstChar = data.userName ? data.userName.charAt(0).toUpperCase() : '?'; + + // Level badge + const levelBadge = `L${data.networkLevel || 0}`; + + // Leg indicator + const legText = isRoot ? '' : (data.networkLeg === 0 ? 'چپ' : 'راست'); + const legClass = data.networkLeg === 0 ? 'leg-text-left' : 'leg-text-right'; + + // Build tooltip text + const tooltipLines = [ + `👤 ${data.userName || 'کاربر ' + data.userId}`, + `🆔 شناسه: ${data.userId}`, + `📊 سطح: ${data.networkLevel || 0}`, + `${data.networkLeg === 0 ? '⬅️' : '➡️'} موقعیت: ${data.networkLeg === 0 ? 'چپ' : 'راست'}`, + `${data.isClubActive ? '✅' : '❌'} باشگاه: ${data.isClubActive ? 'فعال' : 'غیرفعال'}` + ]; + + if (data.clubActivatedAt) { + tooltipLines.push(`📅 فعالسازی: ${data.clubActivatedAt}`); + } + if (data.activationWeekDefinitionId) { + tooltipLines.push(`📆 هفته: ${data.activationWeekDefinitionId}`); + } + + const tooltipText = tooltipLines.join(' '); + + return ` +
+ ${weekIndicator} +
+
${firstChar}
+
+
${data.userName || 'کاربر ' + data.userId}
+
+ ${levelBadge} + ${legText ? `${legText}` : ''} +
+
+
+ +
+ `; + }) + .onNodeClick((d) => { + if (this.dotNetHelper) { + const userId = parseInt(d.data.userId, 10); + this.dotNetHelper.invokeMethodAsync('OnNodeClicked', userId); + } + }) + .render(); + + // Initial fit + setTimeout(() => { + if (this.chart) { + this.chart.fit(); + } + }, 100); + + } catch (error) { + console.error('AdminOrgChart: Error initializing chart:', error); + container.innerHTML = '
خطا در بارگذاری نمودار
'; + } + }, + + /** + * Update the chart with new data + */ + update: function (data, options = {}) { + if (this.chart && this.containerId) { + // Re-init with new data + this.init(this.containerId, data, this.dotNetHelper, options); + } + }, + + /** + * Expand all nodes + */ + expandAll: function () { + if (this.chart) { + this.chart.expandAll().render(); + } + }, + + /** + * Collapse all nodes + */ + collapseAll: function () { + if (this.chart) { + this.chart.collapseAll().render(); + } + }, + + /** + * Center/Fit the chart + */ + fit: function () { + if (this.chart) { + this.chart.fit(); + } + }, + + /** + * Zoom to specific node + */ + zoomToNode: function (nodeId) { + if (this.chart) { + this.chart.setCentered(nodeId.toString()).render(); + } + }, + + /** + * Export chart as PNG + */ + exportPng: function () { + if (this.chart) { + this.chart.exportImg({ full: true }); + } + }, + + /** + * Export chart as SVG + */ + exportSvg: function () { + if (this.chart) { + this.chart.exportSvg(); + } + }, + + /** + * Dispose the chart + */ + dispose: function () { + if (this.chart) { + this.chart = null; + this.dotNetHelper = null; + this.containerId = null; + } + } +}; diff --git a/src/BackOffice/wwwroot/js/d3-flextree.min.js b/src/BackOffice/wwwroot/js/d3-flextree.min.js new file mode 100644 index 0000000..de5f57b --- /dev/null +++ b/src/BackOffice/wwwroot/js/d3-flextree.min.js @@ -0,0 +1,2 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.d3=t.d3||{})}(this,function(t){"use strict";function e(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function n(t,e){var n,i,h,l,c,a=new u(t),f=+t.value&&(a.value=t.value),s=[a];for(null==e&&(e=r);n=s.pop();)if(f&&(n.value=+n.data.value),(h=e(n.data))&&(c=h.length))for(n.children=new Array(c),l=c-1;l>=0;--l)s.push(i=n.children[l]=new u(h[l])),i.parent=n,i.depth=n.depth+1;return a.eachBefore(o)}function r(t){return t.children}function i(t){t.data=t.data.data}function o(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function u(t){this.data=t,this.depth=this.height=0,this.parent=null}u.prototype=n.prototype={constructor:u,count:function(){return this.eachAfter(e)},each:function(t){var e,n,r,i,o=this,u=[o];do{for(e=u.reverse(),u=[];o=e.pop();)if(t(o),n=o.children)for(r=0,i=n.length;r=0;--n)i.push(e[n]);return this},sum:function(t){return this.eachAfter(function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n})},sort:function(t){return this.eachBefore(function(e){e.children&&e.children.sort(t)})},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;for(t=n.pop(),e=r.pop();t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){var t=[];return this.each(function(e){t.push(e)}),t},leaves:function(){var t=[];return this.eachBefore(function(e){e.children||t.push(e)}),t},links:function(){var t=this,e=[];return t.each(function(n){n!==t&&e.push({source:n.parent,target:n})}),e},copy:function(){return n(this).eachBefore(i)}};var h="2.1.2",l=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},c=function(){function t(t,e){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:0;return e.y=n,(e.children||[]).reduce(function(n,r){var i=s(n,2),o=i[0],u=i[1];t(r,e.y+e.ySize);var h=(0===o?r.lExt:r.rExt).bottom;return 0!==o&&m(e,o,u),[o+1,j(h,o,u)]},[0,null]),x(e),S(e),e},y=function t(e,n,r){void 0===n&&(n=-e.relX-e.prelim,r=0);var i=n+e.relX;return e.relX=i+e.prelim-r,e.prelim=0,e.x=r+e.relX,(e.children||[]).forEach(function(n){return t(n,i,e.x)}),e},x=function(t){(t.children||[]).reduce(function(t,e){var n=s(t,2),r=n[0],i=n[1],o=r+e.shift,u=i+o+e.change;return e.relX+=u,[o,u]},[0,0])},m=function(t,e,n){for(var r=t.children[e-1],i=t.children[e],o=r,u=r.relX,h=i,l=i.relX,c=!0;o&&h;){o.bottom>n.lowY&&(n=n.next);var a=u+o.prelim-(l+h.prelim)+o.xSize/2+h.xSize/2+o.spacing(h);(a>0||a<0&&c)&&(l+=a,b(i,a),E(t,e,n.index,a)),c=!1;var f=o.bottom,s=h.bottom;f<=s&&(o=k(o))&&(u+=o.relX),f>=s&&(h=X(h))&&(l+=h.relX)}!o&&h?z(t,e,h,l):o&&!h&&w(t,e,o,u)},b=function(t,e){t.relX+=e,t.lExtRelX+=e,t.rExtRelX+=e},E=function(t,e,n,r){var i=t.children[e],o=e-n;if(o>1){var u=r/o;t.children[n+1].shift+=u,i.shift-=u,i.change-=r-u}},X=function(t){return t.hasChildren?t.firstChild:t.lThr},k=function(t){return t.hasChildren?t.lastChild:t.rThr},z=function(t,e,n,r){var i=t.firstChild,o=i.lExt,u=t.children[e];o.lThr=n;var h=r-n.relX-i.lExtRelX;o.relX+=h,o.prelim-=h,i.lExt=u.lExt,i.lExtRelX=u.lExtRelX},w=function(t,e,n,r){var i=t.children[e],o=i.rExt,u=t.children[e-1];o.rThr=n;var h=r-n.relX-i.rExtRelX;o.relX+=h,o.prelim-=h,i.rExt=u.rExt,i.rExtRelX=u.rExtRelX},S=function(t){if(t.hasChildren){var e=t.firstChild,n=t.lastChild,r=(e.prelim+e.relX-e.xSize/2+n.relX+n.prelim+n.xSize/2)/2;Object.assign(t,{prelim:r,lExt:e.lExt,lExtRelX:e.lExtRelX,rExt:n.rExt,rExtRelX:n.rExtRelX})}},j=function(t,e,n){for(;null!==n&&t>=n.lowY;)n=n.next;return{lowY:t,index:e,next:n}};t.flextree=g,Object.defineProperty(t,"__esModule",{value:!0})}); +//# sourceMappingURL=d3-flextree.min.js.map diff --git a/src/BackOffice/wwwroot/js/d3-org-chart3.js b/src/BackOffice/wwwroot/js/d3-org-chart3.js new file mode 100644 index 0000000..bd659fc --- /dev/null +++ b/src/BackOffice/wwwroot/js/d3-org-chart3.js @@ -0,0 +1,69 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("d3-selection"),require("d3-array"),require("d3-hierarchy"),require("d3-zoom"),require("d3-flextree"),require("d3-shape")):"function"==typeof define&&define.amd?define(["exports","d3-selection","d3-array","d3-hierarchy","d3-zoom","d3-flextree","d3-shape"],e):e(t.d3=t.d3||{},t.d3,t.d3,t.d3,t.d3,t.d3,t.d3)}(this,function(t,n,e,a,i,r,o){"use strict";const u={selection:n.selection,select:n.select,max:e.max,min:e.min,sum:e.sum,cumsum:e.cumsum,tree:a.tree,stratify:a.stratify,zoom:i.zoom,zoomIdentity:i.zoomIdentity,linkHorizontal:o.linkHorizontal,flextree:r.flextree};t.OrgChart=class{constructor(){const a={id:"ID"+Math.floor(1e6*Math.random()),firstDraw:!0,ctx:document.createElement("canvas").getContext("2d"),initialExpandLevel:1,nodeDefaultBackground:"none",lastTransform:{x:0,y:0,k:1},allowedNodesCount:{},zoomBehavior:null,generateRoot:null,svgWidth:800,svgHeight:window.innerHeight-100,container:"body",data:null,connections:[],defaultFont:"Helvetica",nodeId:t=>t.nodeId||t.id,parentNodeId:t=>t.parentNodeId||t.parentId,rootMargin:40,nodeWidth:t=>250,nodeHeight:t=>150,neighbourMargin:(t,e)=>80,siblingsMargin:t=>20,childrenMargin:t=>60,compactMarginPair:t=>100,compactMarginBetween:t=>20,nodeButtonWidth:t=>40,nodeButtonHeight:t=>40,nodeButtonX:t=>-20,nodeButtonY:t=>-20,linkYOffset:30,pagingStep:t=>5,minPagingVisibleNodes:t=>2e3,scaleExtent:[.001,20],duration:400,imageName:"Chart",setActiveNodeCentered:!0,layout:"top",compact:!0,createZoom:t=>u.zoom(),onZoomStart:t=>{},onZoom:t=>{},onZoomEnd:t=>{},onNodeClick:t=>t,onExpandOrCollapse:t=>t,nodeContent:t=>`
Sample Node(id=${t.id}), override using
+ chart.nodeContent({data}=>{
+     return '' // Custom HTML
+ })
+
+ Or check different layout examples +
`,buttonContent:({node:e,state:t})=>{return`
${{left:t=>t?`
+ + ${e.data._directSubordinatesPaging}
`:`
+ + ${e.data._directSubordinatesPaging}
`,bottom:t=>t?`
+ + ${e.data._directSubordinatesPaging}
+ `:`
+ + ${e.data._directSubordinatesPaging}
+ `,right:t=>t?`
+ + ${e.data._directSubordinatesPaging}
`:`
+ + ${e.data._directSubordinatesPaging}
`,top:t=>t?`
+ + ${e.data._directSubordinatesPaging}
+ `:`
+ + ${e.data._directSubordinatesPaging}
+ `}[t.layout](e.children)}
`},pagingButton:(t,e,a,n)=>{var n=n.pagingStep(t.parent),i=t.parent.data._pagingStep,t=t.parent.data._directSubordinatesPaging-i;return` +
+
+
+ + +
Show next ${Math.min(t,n)} nodes
+
+ `},nodeUpdate:function(t,e,a){u.select(this).select(".node-rect").attr("stroke",t=>t.data._highlighted||t.data._upToTheRootHighlighted?"#E27396":"none").attr("stroke-width",t.data._highlighted||t.data._upToTheRootHighlighted?10:1)},nodeEnter:t=>t,nodeExit:t=>t,linkUpdate:function(t,e,a){u.select(this).attr("stroke",t=>t.data._upToTheRootHighlighted?"#E27396":"#E4E2E9").attr("stroke-width",t=>t.data._upToTheRootHighlighted?5:1),t.data._upToTheRootHighlighted&&u.select(this).raise()},hdiagonal:function(t,e,a){var n=t.x,t=t.y,i=e.x,e=e.y,o=a&&null!=a.x?a.x:n,a=a&&null!=a.y?a.y:t,r=i-n<0?-1:1,d=e-t<0?-1:1,s=Math.abs(i-n)/2<35?Math.abs(i-n)/2:35,s=Math.abs(e-t)/2 + ${t.map(t=>{var e=this.getTextWidth(t.label,{ctx:a.ctx,fontSize:2,defaultFont:a.defaultFont});return` + + + ${t.label||""} + + + + + + `}).join("")} + + `},connectionsUpdate:function(t,e,a){u.select(this).attr("stroke",t=>"#E27396").attr("stroke-linecap","round").attr("stroke-width",t=>"5").attr("pointer-events","none").attr("marker-start",t=>`url(#${t.from+"_"+t.to})`).attr("marker-end",t=>`url(#arrow-${t.from+"_"+t.to})`)},linkGroupArc:u.linkHorizontal().x(t=>t.x).y(t=>t.y),layoutBindings:{left:{nodeLeftX:t=>0,nodeRightX:t=>t.width,nodeTopY:t=>-t.height/2,nodeBottomY:t=>t.height/2,nodeJoinX:t=>t.x+t.width,nodeJoinY:t=>t.y-t.height/2,linkJoinX:t=>t.x+t.width,linkJoinY:t=>t.y,linkX:t=>t.x,linkY:t=>t.y,linkCompactXStart:t=>t.x+t.width/2,linkCompactYStart:t=>t.y+(t.compactEven?t.height/2:-t.height/2),compactLinkMidX:(t,e)=>t.firstCompactNode.x,compactLinkMidY:(t,e)=>t.firstCompactNode.y+t.firstCompactNode.flexCompactDim[0]/4+e.compactMarginPair(t)/4,linkParentX:t=>t.parent.x+t.parent.width,linkParentY:t=>t.parent.y,buttonX:t=>t.width,buttonY:t=>t.height/2,centerTransform:({rootMargin:t,centerY:e,scale:a})=>`translate(${t},${e}) scale(${a})`,compactDimension:{sizeColumn:t=>t.height,sizeRow:t=>t.width,reverse:t=>t.slice().reverse()},nodeFlexSize:({height:t,width:e,siblingsMargin:a,childrenMargin:n,state:i,node:o})=>{return i.compact&&o.flexCompactDim?[o.flexCompactDim[0],o.flexCompactDim[1]]:[t+a,e+n]},zoomTransform:({centerY:t,scale:e})=>`translate(0,${t}) scale(${e})`,diagonal:this.hdiagonal.bind(this),swap:t=>{var e=t.x;t.x=t.y,t.y=e},nodeUpdateTransform:({x:t,y:e,height:a})=>`translate(${t},${e-a/2})`},top:{nodeLeftX:t=>-t.width/2,nodeRightX:t=>t.width/2,nodeTopY:t=>0,nodeBottomY:t=>t.height,nodeJoinX:t=>t.x-t.width/2,nodeJoinY:t=>t.y+t.height,linkJoinX:t=>t.x,linkJoinY:t=>t.y+t.height,linkCompactXStart:t=>t.x+(t.compactEven?t.width/2:-t.width/2),linkCompactYStart:t=>t.y+t.height/2,compactLinkMidX:(t,e)=>t.firstCompactNode.x+t.firstCompactNode.flexCompactDim[0]/4+e.compactMarginPair(t)/4,compactLinkMidY:t=>t.firstCompactNode.y,compactDimension:{sizeColumn:t=>t.width,sizeRow:t=>t.height,reverse:t=>t},linkX:t=>t.x,linkY:t=>t.y,linkParentX:t=>t.parent.x,linkParentY:t=>t.parent.y+t.parent.height,buttonX:t=>t.width/2,buttonY:t=>t.height,centerTransform:({rootMargin:t,scale:e,centerX:a})=>`translate(${a},${t}) scale(${e})`,nodeFlexSize:({height:t,width:e,siblingsMargin:a,childrenMargin:n,state:i,node:o})=>{return i.compact&&o.flexCompactDim?[o.flexCompactDim[0],o.flexCompactDim[1]]:[e+a,t+n]},zoomTransform:({centerX:t,scale:e})=>`translate(${t},0}) scale(${e})`,diagonal:this.diagonal.bind(this),swap:t=>{},nodeUpdateTransform:({x:t,y:e,width:a})=>`translate(${t-a/2},${e})`},bottom:{nodeLeftX:t=>-t.width/2,nodeRightX:t=>t.width/2,nodeTopY:t=>-t.height,nodeBottomY:t=>0,nodeJoinX:t=>t.x-t.width/2,nodeJoinY:t=>t.y-t.height-t.height,linkJoinX:t=>t.x,linkJoinY:t=>t.y-t.height,linkCompactXStart:t=>t.x+(t.compactEven?t.width/2:-t.width/2),linkCompactYStart:t=>t.y-t.height/2,compactLinkMidX:(t,e)=>t.firstCompactNode.x+t.firstCompactNode.flexCompactDim[0]/4+e.compactMarginPair(t)/4,compactLinkMidY:t=>t.firstCompactNode.y,linkX:t=>t.x,linkY:t=>t.y,compactDimension:{sizeColumn:t=>t.width,sizeRow:t=>t.height,reverse:t=>t},linkParentX:t=>t.parent.x,linkParentY:t=>t.parent.y-t.parent.height,buttonX:t=>t.width/2,buttonY:t=>0,centerTransform:({rootMargin:t,scale:e,centerX:a,chartHeight:n})=>`translate(${a},${n-t}) scale(${e})`,nodeFlexSize:({height:t,width:e,siblingsMargin:a,childrenMargin:n,state:i,node:o})=>{return i.compact&&o.flexCompactDim?[o.flexCompactDim[0],o.flexCompactDim[1]]:[e+a,t+n]},zoomTransform:({centerX:t,scale:e})=>`translate(${t},0}) scale(${e})`,diagonal:this.diagonal.bind(this),swap:t=>{t.y=-t.y},nodeUpdateTransform:({x:t,y:e,width:a,height:n})=>`translate(${t-a/2},${e-n})`},right:{nodeLeftX:t=>-t.width,nodeRightX:t=>0,nodeTopY:t=>-t.height/2,nodeBottomY:t=>t.height/2,nodeJoinX:t=>t.x-t.width-t.width,nodeJoinY:t=>t.y-t.height/2,linkJoinX:t=>t.x-t.width,linkJoinY:t=>t.y,linkX:t=>t.x,linkY:t=>t.y,linkParentX:t=>t.parent.x-t.parent.width,linkParentY:t=>t.parent.y,buttonX:t=>0,buttonY:t=>t.height/2,linkCompactXStart:t=>t.x-t.width/2,linkCompactYStart:t=>t.y+(t.compactEven?t.height/2:-t.height/2),compactLinkMidX:(t,e)=>t.firstCompactNode.x,compactLinkMidY:(t,e)=>t.firstCompactNode.y+t.firstCompactNode.flexCompactDim[0]/4+e.compactMarginPair(t)/4,centerTransform:({rootMargin:t,centerY:e,scale:a,chartWidth:n})=>`translate(${n-t},${e}) scale(${a})`,nodeFlexSize:({height:t,width:e,siblingsMargin:a,childrenMargin:n,state:i,node:o})=>{return i.compact&&o.flexCompactDim?[o.flexCompactDim[0],o.flexCompactDim[1]]:[t+a,e+n]},compactDimension:{sizeColumn:t=>t.height,sizeRow:t=>t.width,reverse:t=>t.slice().reverse()},zoomTransform:({centerY:t,scale:e})=>`translate(0,${t}) scale(${e})`,diagonal:this.hdiagonal.bind(this),swap:t=>{var e=t.x;t.x=-t.y,t.y=e},nodeUpdateTransform:({x:t,y:e,width:a,height:n})=>`translate(${t-a},${e-n/2})`}}};this.getChartState=()=>a,Object.keys(a).forEach(e=>{this[e]=function(t){return arguments.length?(a[e]=t,this):a[e]}}),this.initializeEnterExitUpdatePattern()}initializeEnterExitUpdatePattern(){u.selection.prototype.patternify=function(t){var e=t.selector,a=t.tag,t=t.data||[e],t=this.selectAll("."+e).data(t,(t,e)=>"object"==typeof t&&t.id?t.id:e);return t.exit().remove(),(t=t.enter().append(a).merge(t)).attr("class",e),t}}getNodeChildren({data:t,children:e,_children:a},n){return n.push(t),e&&e.forEach(t=>{this.getNodeChildren(t,n)}),a&&a.forEach(t=>{this.getNodeChildren(t,n)}),n}initialZoom(t){return this.getChartState().lastTransform.k=t,this}render(){const o=this.getChartState();if(o.data&&0!=o.data.length){var t=u.select(o.container),e=t.node().getBoundingClientRect();0o.onZoomStart(t)).on("end",(t,e)=>o.onZoomEnd(t)).on("zoom",(t,e)=>{o.onZoom(t),this.zoomed(t,e)}).scaleExtent(o.scaleExtent),o.zoomBehavior=e.zoom),o.flexTreeLayout=r.flextree({nodeSize:t=>{var e=o.nodeWidth(t),a=o.nodeHeight(t),n=o.siblingsMargin(t),i=o.childrenMargin(t);return o.layoutBindings[o.layout].nodeFlexSize({state:o,node:t,width:e,height:a,siblingsMargin:n,childrenMargin:i})}}).spacing((t,e)=>t.parent==e.parent?0:o.neighbourMargin(t,e)),this.setLayouts({expandNodesFirst:!1});e=t.patternify({tag:"svg",selector:"svg-chart-container"}).attr("width",o.svgWidth).attr("height",o.svgHeight).attr("font-family",o.defaultFont),t=(o.firstDraw&&e.call(o.zoomBehavior).on("dblclick.zoom",null).attr("cursor","move"),(o.svg=e).patternify({tag:"g",selector:"chart"}));o.centerG=t.patternify({tag:"g",selector:"center-group"}),o.linksWrapper=o.centerG.patternify({tag:"g",selector:"links-wrapper"}),o.nodesWrapper=o.centerG.patternify({tag:"g",selector:"nodes-wrapper"}),o.connectionsWrapper=o.centerG.patternify({tag:"g",selector:"connections-wrapper"}),o.defsWrapper=e.patternify({tag:"g",selector:"defs-wrapper"}),o.firstDraw&&o.centerG.attr("transform",()=>o.layoutBindings[o.layout].centerTransform({centerX:a.centerX,centerY:a.centerY,scale:o.lastTransform.k,rootMargin:o.rootMargin,root:o.root,chartHeight:a.chartHeight,chartWidth:a.chartWidth})),o.chart=t,this.update(o.root),u.select(window).on("resize."+o.id,()=>{var t=u.select(o.container).node().getBoundingClientRect();o.svg.attr("width",t.width)}),o.firstDraw&&(o.firstDraw=!1)}else console.log("ORG CHART - Data is empty"),o.container&&(n.select(o.container).select(".nodes-wrapper").remove(),n.select(o.container).select(".links-wrapper").remove(),n.select(o.container).select(".connections-wrapper").remove());return this}addNode(e){const a=this.getChartState();var t,n;return!e||null!=a.parentNodeId(e)&&a.parentNodeId(e)!=a.nodeId(e)||0!=a.data.length?(n=(t=a.generateRoot(a.data).descendants()).filter(({data:t})=>a.nodeId(t).toString()===a.nodeId(e).toString())[0],t.filter(({data:t})=>a.nodeId(t).toString()===a.parentNodeId(e).toString())[0],n?console.log(`ORG CHART - ADD - Node with id "${a.nodeId(e)}" already exists in tree`):(e._centered&&!e._expanded&&(e._expanded=!0),a.data.push(e),this.updateNodesState())):(a.data.push(e),this.render()),this}removeNode(e){const a=this.getChartState();var t=a.generateRoot(a.data).descendants().filter(({data:t})=>a.nodeId(t)==e)[0];return t?(t.descendants().forEach(t=>t.data._filteredOut=!0),a.data=a.data.filter(t=>!t._filteredOut),0==a.data.length?this.render():this.updateNodesState.bind(this)()):console.log(`ORG CHART - REMOVE - Node with id "${e}" not found in the tree`),this}groupBy(t,a,e){const n={};return t.forEach(t=>{var e=a(t);n[e]||(n[e]=[]),n[e].push(t)}),Object.keys(n).forEach(t=>{n[t]=e(n[t])}),Object.entries(n)}calculateCompactFlexDimensions(t){const r=this.getChartState();t.eachBefore(t=>{t.firstCompact=null,t.compactEven=null,t.flexCompactDim=null,t.firstCompactNode=null}),t.eachBefore(t=>{if(t.children&&1!t.children);if(!(n.length<2)){n.forEach((t,e)=>{e||(t.firstCompact=!0),t.compactEven=!(e%2),t.row=Math.floor(e/2)});var e=u.max(n.filter(t=>t.compactEven),r.layoutBindings[r.layout].compactDimension.sizeColumn),a=u.max(n.filter(t=>!t.compactEven),r.layoutBindings[r.layout].compactDimension.sizeColumn);const i=2*Math.max(e,a);e=this.groupBy(n,t=>t.row,t=>u.max(t,t=>r.layoutBindings[r.layout].compactDimension.sizeRow(t)+r.compactMarginBetween(t)));const o=u.sum(e.map(t=>t[1]));n.forEach(t=>{t.firstCompactNode=n[0],t.firstCompact?t.flexCompactDim=[i+r.compactMarginPair(t),o-r.compactMarginBetween(t)]:t.flexCompactDim=[0,0]}),t.flexCompactDim=null}}})}calculateCompactFlexPositions(t){const r=this.getChartState();t.eachBefore(t=>{if(t.children){var e=t.children.filter(t=>t.flexCompactDim);const n=e[0];if(n){e.forEach((t,e,a)=>{0==e&&(n.x-=n.flexCompactDim[0]/2),e&e%2-1?t.x=n.x+.25*n.flexCompactDim[0]-r.compactMarginPair(t)/4:e&&(t.x=n.x+.75*n.flexCompactDim[0]+r.compactMarginPair(t)/4)});var a=n.x+.5*n.flexCompactDim[0];n.x=n.x+.25*n.flexCompactDim[0]-r.compactMarginPair(n)/4;const i=t.x-a;Math.abs(i)<10&&e.forEach(t=>t.x+=i);t=this.groupBy(e,t=>t.row,t=>u.max(t,t=>r.layoutBindings[r.layout].compactDimension.sizeRow(t)));const o=u.cumsum(t.map(t=>t[1]+r.compactMarginBetween(t)));e.forEach((t,e)=>{t.row?t.y=n.y+o[t.row-1]:t.y=n.y})}}})}update({x0:a,y0:n,x:i=0,y:o=0,width:r,height:d}){const s=this.getChartState();s.calc;s.compact&&this.calculateCompactFlexDimensions(s.root);var e=s.flexTreeLayout(s.root),t=(s.compact&&this.calculateCompactFlexPositions(s.root),e.descendants()),e=e.descendants().slice(1),l=(t.forEach(s.layoutBindings[s.layout].swap),s.connections);const h={},c=(s.allNodes.forEach(t=>h[s.nodeId(t.data)]=t),{});t.forEach(t=>c[s.nodeId(t.data)]=t),l.forEach(t=>{var e=h[t.from],a=h[t.to];t._source=e,t._target=a});var l=l.filter(t=>c[t.from]&&c[t.to]),g=s.defs.bind(this)(s,l),g=(g!==s.defsWrapper.html()&&s.defsWrapper.html(g),s.linksWrapper.selectAll("path.link").data(e,t=>s.nodeId(t.data))),e=g.enter().insert("path","g").attr("class","link").attr("d",t=>{var e={x:s.layoutBindings[s.layout].linkJoinX({x:a,y:n,width:r,height:d}),y:s.layoutBindings[s.layout].linkJoinY({x:a,y:n,width:r,height:d})};return s.layoutBindings[s.layout].diagonal(e,e,e)}).merge(g),e=(e.attr("fill","none"),this.isEdge()?e.style("display",t=>{return t.data._pagingButton?"none":"auto"}):e.attr("display",t=>{return t.data._pagingButton?"none":"auto"}),e.each(s.linkUpdate),e.transition().duration(s.duration).attr("d",t=>{var e=s.compact&&t.flexCompactDim?{x:s.layoutBindings[s.layout].compactLinkMidX(t,s),y:s.layoutBindings[s.layout].compactLinkMidY(t,s)}:{x:s.layoutBindings[s.layout].linkX(t),y:s.layoutBindings[s.layout].linkY(t)},a={x:s.layoutBindings[s.layout].linkParentX(t),y:s.layoutBindings[s.layout].linkParentY(t)},t=s.compact&&t.flexCompactDim?{x:s.layoutBindings[s.layout].linkCompactXStart(t),y:s.layoutBindings[s.layout].linkCompactYStart(t)}:e;return s.layoutBindings[s.layout].diagonal(e,a,t,{sy:s.linkYOffset})}),g.exit().transition().duration(s.duration).attr("d",t=>{var e={x:s.layoutBindings[s.layout].linkJoinX({x:i,y:o,width:r,height:d}),y:s.layoutBindings[s.layout].linkJoinY({x:i,y:o,width:r,height:d})};return s.layoutBindings[s.layout].diagonal(e,e,null,{sy:s.linkYOffset})}).remove(),s.connectionsWrapper.selectAll("path.connection").data(l)),g=e.enter().insert("path","g").attr("class","connection").attr("d",t=>{var e={x:s.layoutBindings[s.layout].linkJoinX({x:a,y:n,width:r,height:d}),y:s.layoutBindings[s.layout].linkJoinY({x:a,y:n,width:r,height:d})};return s.layoutBindings[s.layout].diagonal(e,e,null,{sy:s.linkYOffset})}).merge(e),l=(g.attr("fill","none"),g.transition().duration(s.duration).attr("d",t=>{var e=s.layoutBindings[s.layout].linkX({x:t._source.x,y:t._source.y,width:t._source.width,height:t._source.height}),a=s.layoutBindings[s.layout].linkY({x:t._source.x,y:t._source.y,width:t._source.width,height:t._source.height}),n=s.layoutBindings[s.layout].linkJoinX({x:t._target.x,y:t._target.y,width:t._target.width,height:t._target.height}),t=s.layoutBindings[s.layout].linkJoinY({x:t._target.x,y:t._target.y,width:t._target.width,height:t._target.height});return s.linkGroupArc({source:{x:e,y:a},target:{x:n,y:t}})}),g.each(s.connectionsUpdate),e.exit().transition().duration(s.duration).attr("opacity",0).remove(),s.nodesWrapper.selectAll("g.node").data(t,({data:t})=>s.nodeId(t))),g=l.enter().append("g").attr("class","node").attr("transform",t=>{return t==s.root?`translate(${a},${n})`:`translate(${s.layoutBindings[s.layout].nodeJoinX({x:a,y:n,width:r,height:d})},${s.layoutBindings[s.layout].nodeJoinY({x:a,y:n,width:r,height:d})})`}).attr("cursor","pointer").on("click.node",(t,e)=>{var a=e["data"];[...t.srcElement.classList].includes("node-button-foreign-object")||([...t.srcElement.classList].includes("paging-button-wrapper")?this.loadPagingNodes(e):a._pagingButton?console.log("event fired, no handlers"):s.onNodeClick(e))}).on("keydown.node",(t,e)=>{var{}=e;"Enter"!==t.key&&" "!==t.key&&"Spacebar"!==t.key||[...t.srcElement.classList].includes("node-button-foreign-object")||([...t.srcElement.classList].includes("paging-button-wrapper")?this.loadPagingNodes(e):"Enter"!==t.key&&" "!==t.key&&"Spacebar"!==t.key||this.onButtonClick(t,e))}),e=(g.each(s.nodeEnter),g.patternify({tag:"rect",selector:"node-rect",data:t=>[t]}),g.merge(l).style("font","12px sans-serif")),g=(e.patternify({tag:"foreignObject",selector:"node-foreign-object",data:t=>[t]}).style("overflow","visible").patternify({tag:"xhtml:div",selector:"node-foreign-object-div",data:t=>[t]}),this.restyleForeignObjectElements(),g.patternify({tag:"g",selector:"node-button-g",data:t=>[t]}).on("click",(t,e)=>this.onButtonClick(t,e)).on("keydown",(t,e)=>{"Enter"!==t.key&&" "!==t.key&&"Spacebar"!==t.key||this.onButtonClick(t,e)})),g=(g.patternify({tag:"rect",selector:"node-button-rect",data:t=>[t]}).attr("opacity",0).attr("pointer-events","all").attr("width",t=>s.nodeButtonWidth(t)).attr("height",t=>s.nodeButtonHeight(t)).attr("x",t=>s.nodeButtonX(t)).attr("y",t=>s.nodeButtonY(t)),g.patternify({tag:"foreignObject",selector:"node-button-foreign-object",data:t=>[t]}).attr("width",t=>s.nodeButtonWidth(t)).attr("height",t=>s.nodeButtonHeight(t)).attr("x",t=>s.nodeButtonX(t)).attr("y",t=>s.nodeButtonY(t)).style("overflow","visible").patternify({tag:"xhtml:div",selector:"node-button-div",data:t=>[t]}).style("pointer-events","none").style("display","flex").style("width","100%").style("height","100%"),e.transition().attr("opacity",0).duration(s.duration).attr("transform",({x:t,y:e,width:a,height:n})=>s.layoutBindings[s.layout].nodeUpdateTransform({x:t,y:e,width:a,height:n})).attr("opacity",1),e.select(".node-rect").attr("width",({width:t})=>t).attr("height",({height:t})=>t).attr("x",({})=>0).attr("y",({})=>0).attr("cursor","pointer").attr("rx",3).attr("fill",s.nodeDefaultBackground),e.select(".node-button-g").attr("transform",({width:t,height:e})=>{return`translate(${s.layoutBindings[s.layout].buttonX({width:t,height:e})},${s.layoutBindings[s.layout].buttonY({width:t,height:e})})`}).attr("display",({data:t})=>0!t._pagingButton&&(e||a)?1:0),e.select(".node-button-foreign-object .node-button-div").html(t=>s.buttonContent({node:t,state:s})),e.select(".node-button-text").attr("text-anchor","middle").attr("alignment-baseline","middle").attr("font-size",({children:t})=>t?40:26).text(({children:t})=>t?"-":"+").attr("y",this.isEdge()?10:0),e.each(s.nodeUpdate),l.exit());g.each(s.nodeExit);const p=g.data().reduce((t,e)=>t.depth{var{x:e,y:a,width:n,height:i}=p.parent||{};return`translate(${s.layoutBindings[s.layout].nodeJoinX({x:e,y:a,width:n,height:i})},${s.layoutBindings[s.layout].nodeJoinY({x:e,y:a,width:n,height:i})})`}).on("end",function(){u.select(this).remove()}).attr("opacity",0),t.forEach(t=>{t.x0=t.x,t.y0=t.y});e=s.allNodes.filter(t=>t.data._centered)[0];if(e){let t=[e];e.data._centeredWithDescendants&&(t=s.compact?e.descendants().filter((t,e)=>e<7):e.descendants().filter((t,e,a)=>{var n=Math.round(a.length/2);return a.length%2?n-2t).attr("height",({height:t})=>t).attr("x",({})=>0).attr("y",({})=>0),n.svg.selectAll(".node-foreign-object-div").style("width",({width:t})=>t+"px").style("height",({height:t})=>t+"px").html(function(t,e,a){return t.data._pagingButton?`
${n.pagingButton(t,e,a,n)}
`:n.nodeContent.bind(this)(t,e,a,n)})}onButtonClick(t,e){var a=this.getChartState();e.data._pagingButton||(a.setActiveNodeCentered&&(e.data._centered=!0,e.data._centeredWithDescendants=!0),e.children?(e._children=e.children,e.children=null,this.setExpansionFlagToChildren(e,!1)):(e.children=e._children,e._children=null,e.children&&e.children.forEach(({data:t})=>t._expanded=!0)),this.update(e),t.stopPropagation(),a.onExpandOrCollapse(e))}setExpansionFlagToChildren({data:t,children:e,_children:a},n){t._expanded=n,e&&e.forEach(t=>{this.setExpansionFlagToChildren(t,n)}),a&&a.forEach(t=>{this.setExpansionFlagToChildren(t,n)})}expandSomeNodes(e){if(e.data._expanded){let t=e.parent;for(;t&&t._children;)t.children=t._children,t._children=null,t=t.parent}e._children&&e._children.forEach(t=>this.expandSomeNodes(t)),e.children&&e.children.forEach(t=>this.expandSomeNodes(t))}updateNodesState(){var t=this.getChartState();this.setLayouts({expandNodesFirst:!0}),this.update(t.root)}setLayouts({expandNodesFirst:t=!0}){const r=this.getChartState();r.generateRoot=u.stratify().id(t=>r.nodeId(t)).parentId(t=>r.parentNodeId(t)),r.root=r.generateRoot(r.data);var e=r.root.descendants();1{t.depth<=r.initialExpandLevel&&(t.data._expanded=!0)}),r.initialExpandLevel=1);const n={};r.root.descendants().filter(t=>t.children).filter(t=>!t.data._pagingStep).forEach(t=>{t.data._pagingStep=r.minPagingVisibleNodes(t)}),r.root.eachBefore((a,t)=>{a.data._directSubordinatesPaging=a.children?a.children.length:0,a.children&&a.children.forEach((e,t)=>{if(e.data._pagingButton=!1,t>a.data._pagingStep&&(n[e.id]=!0),t===a.data._pagingStep&&a.children.length-1>a.data._pagingStep&&(e.data._pagingButton=!0),n[e.parent.id]&&(n[e.id]=!0),e.data._expanded||e.data._centered||e.data._highlighted||e.data._upToTheRootHighlighted){let t=e;for(;t&&(n[t.id]||t.data._pagingButton);)n[t.id]=!1,t.data._pagingButton&&(t.data._pagingButton=!1,t.parent.children.forEach(t=>{t.data._expanded=!0,n[t.id]=!1})),t=t.parent}})}),r.root=u.stratify().id(t=>r.nodeId(t)).parentId(t=>r.parentNodeId(t))(r.data.filter(t=>!0!==n[t.id])),r.root.each((t,e,a)=>{var n=t._hierarchyHeight||t.height,i=r.nodeWidth(t),o=r.nodeHeight(t);Object.assign(t,{width:i,height:o,_hierarchyHeight:n})}),r.root.x0=0,r.root.y0=0,r.allNodes=r.root.descendants(),r.allNodes.forEach(t=>{Object.assign(t.data,{_directSubordinates:t.children?t.children.length:0,_totalSubordinates:t.descendants().length-1})}),r.root.children&&(t&&r.root.children.forEach(this.expand),r.root.children.forEach(t=>this.collapse(t)),0==r.initialExpandLevel&&(r.root._children=r.root.children,r.root.children=null),[r.root].forEach(t=>this.expandSomeNodes(t)))}collapse(t){t.children&&(t._children=t.children,t._children.forEach(t=>this.collapse(t)),t.children=null)}expand(t){t._children&&(t.children=t._children,t.children.forEach(t=>this.expand(t)),t._children=null)}zoomed(t,e){var a=this.getChartState(),n=a.chart,t=t.transform;a.lastTransform=t,n.attr("transform",t),this.isEdge()&&this.restyleForeignObjectElements()}zoomTreeBounds({x0:t,x1:e,y0:a,y1:n,params:i={animate:!0,scale:!0,onCompleted:()=>{}}}){var{centerG:o,svgWidth:r,svgHeight:d,svg:s,zoomBehavior:l,duration:h,lastTransform:c}=this.getChartState(),g=Math.min(8,.9/Math.max((e-t)/r,(n-a)/d));let p=u.zoomIdentity.translate(r/2,d/2);p=(p=p.scale(i.scale?g:c.k)).translate(-(t+e)/2,-(a+n)/2),s.transition().duration(i.animate?h:0).call(l.transform,p),o.transition().duration(i.animate?h:0).attr("transform","translate(0,0)").on("end",function(){i.onCompleted&&i.onCompleted()})}fit({animate:t=!0,nodes:e,scale:a=!0,onCompleted:n=()=>{}}={}){const i=this.getChartState();var o=i["root"],e=e||o.descendants(),o=u.min(e,t=>t.x+i.layoutBindings[i.layout].nodeLeftX(t)),r=u.max(e,t=>t.x+i.layoutBindings[i.layout].nodeRightX(t)),d=u.min(e,t=>t.y+i.layoutBindings[i.layout].nodeTopY(t)),e=u.max(e,t=>t.y+i.layoutBindings[i.layout].nodeBottomY(t));return this.zoomTreeBounds({params:{animate:t,scale:a,onCompleted:n},x0:o-50,x1:r+50,y0:d-50,y1:e+50}),this}loadPagingNodes(t){var e=this.getChartState(),a=(t.data._pagingButton=!1,t.parent.data._pagingStep),e=e.pagingStep(t.parent);t.parent.data._pagingStep=a+e,this.updateNodesState()}setExpanded(e,t=!0){const a=this.getChartState();var n=a.allNodes.filter(({data:t})=>a.nodeId(t)==e)[0];if(n){if(0==(n.data._expanded=t)){const i=n.parent||{descendants:()=>[]};i.descendants().filter(t=>t!=i).forEach(t=>t.data._expanded=!1)}}else console.log(`ORG CHART - ${t?"EXPAND":"COLLAPSE"} - Node with id (${e}) not found in the tree`);return this}setCentered(e){const a=this.getChartState();var t=a.generateRoot(a.data).descendants().filter(({data:t})=>a.nodeId(t).toString()==e.toString())[0];return t?(t.ancestors().forEach(t=>t.data._expanded=!0),t.data._centered=!0,t.data._expanded=!0):console.log(`ORG CHART - CENTER - Node with id (${e}) not found in the tree`),this}setHighlighted(e){const a=this.getChartState();var t=a.generateRoot(a.data).descendants().filter(t=>a.nodeId(t.data).toString()===e.toString())[0];return t?(t.ancestors().forEach(t=>t.data._expanded=!0),t.data._highlighted=!0,t.data._expanded=!0,t.data._centered=!0):console.log(`ORG CHART - HIGHLIGHT - Node with id (${e}) not found in the tree`),this}setUpToTheRootHighlighted(e){const a=this.getChartState();var t=a.generateRoot(a.data).descendants().filter(t=>a.nodeId(t.data).toString()===e.toString())[0];return t?(t.ancestors().forEach(t=>t.data._expanded=!0),t.data._upToTheRootHighlighted=!0,t.data._expanded=!0,t.ancestors().forEach(t=>t.data._upToTheRootHighlighted=!0)):console.log(`ORG CHART - HIGHLIGHTROOT - Node with id (${e}) not found in the tree`),this}clearHighlighting(){var t=this.getChartState();return t.allNodes.forEach(t=>{t.data._highlighted=!1,t.data._upToTheRootHighlighted=!1}),this.update(t.root),this}fullscreen(t){const e=this.getChartState(),a=u.select(t||e.container).node();u.select(document).on("fullscreenchange."+e.id,function(t){(document.fullscreenElement||document.mozFullscreenElement||document.webkitFullscreenElement)==a?setTimeout(t=>{e.svg.attr("height",window.innerHeight-40)},500):e.svg.attr("height",e.svgHeight)}),a.requestFullscreen?a.requestFullscreen():a.mozRequestFullScreen?a.mozRequestFullScreen():a.webkitRequestFullscreen?a.webkitRequestFullscreen():a.msRequestFullscreen&&a.msRequestFullscreen()}zoomIn(){var{svg:t,zoomBehavior:e}=this.getChartState();t.transition().call(e.scaleBy,1.3)}zoomOut(){var{svg:t,zoomBehavior:e}=this.getChartState();t.transition().call(e.scaleBy,.78)}toDataURL(t,e){var a=new XMLHttpRequest;a.onload=function(){var t=new FileReader;t.onloadend=function(){e(t.result)},t.readAsDataURL(a.response)},a.open("GET",t),a.responseType="blob",a.send()}exportImg({full:a=!1,scale:n=3,onLoad:i=t=>t,save:o=!0,backgroundColor:r="#FAFAFA"}={}){const d=this,s=this.getChartState(),{svg:t,root:l}=s;let e=0;var h=t.selectAll("img");let c=h.size();const g=()=>{JSON.parse(JSON.stringify(d.lastTransform()));var t=d.duration();a&&d.fit();const e=d.getChartState()["svg"];setTimeout(t=>{d.downloadImage({node:e.node(),scale:n,isSvg:!1,backgroundColor:r,onAlreadySerialized:t=>{d.update(l)},imageName:s.imageName,onLoad:i,save:o})},a?t+10:0)};0{this.src=t,++e==c&&g()})}):g()}exportSvg(){var{svg:t,imageName:e}=this.getChartState();return this.downloadImage({imageName:e,node:t.node(),scale:3,isSvg:!0}),this}expandAll(){var t=this.getChartState()["data"];return t.forEach(t=>t._expanded=!0),this.render(),this}collapseAll(){var t=this.getChartState()["allNodes"];return t.forEach(t=>t.data._expanded=!1),this.initialExpandLevel(0),this.render(),this}downloadImage({node:t,scale:e=2,imageName:n="graph",isSvg:a=!1,save:i=!0,backgroundColor:o="#FAFAFA",onAlreadySerialized:r=t=>{},onLoad:d=t=>{}}){const s=t;function l(t,e){var a=document.createElement("a");"string"==typeof a.download?(document.body.appendChild(a),a.download=e,a.href=t,a.click(),document.body.removeChild(a)):location.replace(t)}function h(t){for(var e="http://www.w3.org/2000/xmlns/",a=(t=t.cloneNode(!0),window.location.href+"#"),n=document.createTreeWalker(t,NodeFilter.SHOW_ELEMENT,null,!1);n.nextNode();)for(const i of n.currentNode.attributes)i.value.includes(a)&&(i.value=i.value.replace(a,"#"));return t.setAttributeNS(e,"xmlns","http://www.w3.org/2000/svg"),t.setAttributeNS(e,"xmlns:xlink","http://www.w3.org/1999/xlink"),(new XMLSerializer).serializeToString(t)}if(a)t='\r\n'+(t=h(s)),l(c="data:image/svg+xml;charset=utf-8,"+encodeURIComponent(t),n+".svg"),r();else{const g=e,p=document.createElement("img");p.onload=function(){var t=document.createElement("canvas"),e=s.getBoundingClientRect(),a=(t.width=e.width*g,t.height=e.height*g,t.getContext("2d")),a=(a.fillStyle=o,a.fillRect(0,0,e.width*g,e.height*g),a.drawImage(p,0,0,e.width*g,e.height*g),t.toDataURL("image/png"));d&&d(a),i&&l(a,n+".png")};var c="data:image/svg+xml; charset=utf8, "+encodeURIComponent(h(s));r(),p.src=c}}getTextWidth(t,{fontSize:e=14,fontWeight:a=400,defaultFont:n="Helvetice",ctx:i}={}){return i.font=`${a||""} ${e}px ${n} `,i.measureText(t).width}clear(){var t=this.getChartState();u.select(window).on("resize."+t.id,null),t.svg&&t.svg.selectAll("*").remove()}},Object.defineProperty(t,"__esModule",{value:!0})}); \ No newline at end of file From e808cf827e0f4fdd32645df4f6978b45f56870a4 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 25 Dec 2025 02:03:01 +0330 Subject: [PATCH 2/2] feat: Enhance week filter functionality with disabled state and target week highlight --- .../wwwroot/css/admin-org-chart.css | 52 +++++++++++++++++++ src/BackOffice/wwwroot/js/admin-org-chart.js | 23 ++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/BackOffice/wwwroot/css/admin-org-chart.css b/src/BackOffice/wwwroot/css/admin-org-chart.css index 64a37bb..c1c55fa 100644 --- a/src/BackOffice/wwwroot/css/admin-org-chart.css +++ b/src/BackOffice/wwwroot/css/admin-org-chart.css @@ -168,6 +168,58 @@ color: white; } +/* Week filter disabled state */ +.admin-node-card.week-disabled { + opacity: 0.35; + filter: grayscale(80%); + transform: scale(0.95); + box-shadow: none; + border-color: #e0e0e0 !important; +} + +.admin-node-card.week-disabled:hover { + opacity: 0.5; + transform: scale(0.97); + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.admin-node-card.week-disabled .node-avatar { + background: #bdbdbd !important; +} + +.admin-node-card.week-disabled .node-name { + color: #9e9e9e; +} + +.admin-node-card.week-disabled .club-status { + color: #bdbdbd !important; +} + +/* Target week highlight (sparkle badge) */ +.admin-node-card .target-week-highlight { + position: absolute; + top: -12px; + left: -12px; + font-size: 18px; + animation: pulse-highlight 1.5s ease-in-out infinite; +} + +@keyframes pulse-highlight { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.2); opacity: 0.8; } +} + +/* Enhanced styling for target week nodes */ +.admin-node-card:not(.week-disabled) { + /* Normal nodes when filter is active get subtle enhancement */ +} + +/* When week filter is active, make matching nodes stand out more */ +.admin-node-card.leg-left:not(.week-disabled), +.admin-node-card.leg-right:not(.week-disabled) { + /* These will naturally stand out against disabled ones */ +} + /* Expand button */ .admin-expand-btn { width: 20px; diff --git a/src/BackOffice/wwwroot/js/admin-org-chart.js b/src/BackOffice/wwwroot/js/admin-org-chart.js index 9c7b408..61c82e7 100644 --- a/src/BackOffice/wwwroot/js/admin-org-chart.js +++ b/src/BackOffice/wwwroot/js/admin-org-chart.js @@ -69,14 +69,25 @@ window.AdminOrgChart = { // Club status const clubClass = data.isClubActive ? 'club-active' : 'club-inactive'; - // Week activation status (if filtering by week) + // Week filter: check if this node is activated in target week + const isTargetWeek = data.isActivatedInTargetWeek; + const weekFilterActive = filterWeek && filterWeek > 0; + const isDisabledByWeekFilter = weekFilterActive && !isTargetWeek; + const disabledClass = isDisabledByWeekFilter ? 'week-disabled' : ''; + + // Week activation status badge let weekIndicator = ''; - if (filterWeek && data.activationWeekDefinitionId) { - const isTargetWeek = data.isActivatedInTargetWeek; + if (weekFilterActive && data.activationWeekDefinitionId) { weekIndicator = `
W${data.activationWeekDefinitionId}
`; } + + // Highlight badge for target week matches + let highlightBadge = ''; + if (weekFilterActive && isTargetWeek) { + highlightBadge = '
'; + } // Avatar - first letter of name const firstChar = data.userName ? data.userName.charAt(0).toUpperCase() : '?'; @@ -103,14 +114,18 @@ window.AdminOrgChart = { if (data.activationWeekDefinitionId) { tooltipLines.push(`📆 هفته: ${data.activationWeekDefinitionId}`); } + if (weekFilterActive) { + tooltipLines.push(isTargetWeek ? '🎯 فعال در هفته انتخابی' : '⚪ خارج از هفته انتخابی'); + } const tooltipText = tooltipLines.join(' '); return ` -
${weekIndicator} + ${highlightBadge}
${firstChar}