34c5c508e0
- Updated the version of the Foursat.CMSMicroservice.Protobuf package from 0.0.197 to 0.0.199. - Added a new property `PackageName` to `NetworkNodeDto` and `FlatNetworkNodeDto` for better representation of membership packages. - Updated mapping in `NetworkMembershipService` to include the new `PackageName` property. - Enhanced the org chart display by adding a badge for the package name in the UI. These changes improve the clarity and functionality of the network membership features.
319 lines
10 KiB
JavaScript
319 lines
10 KiB
JavaScript
/**
|
||
* d3-org-chart integration for Blazor
|
||
* Network Binary Tree visualization for FourSat
|
||
*/
|
||
|
||
window.OrgChart = {
|
||
chart: null,
|
||
dotNetHelper: 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
|
||
*/
|
||
init: function (containerId, data, dotNetHelper) {
|
||
this.dotNetHelper = dotNetHelper;
|
||
|
||
const container = document.getElementById(containerId);
|
||
if (!container) {
|
||
console.error('OrgChart: Container not found:', containerId);
|
||
return;
|
||
}
|
||
|
||
// Clear previous chart
|
||
container.innerHTML = '';
|
||
|
||
if (!data || data.length === 0) {
|
||
container.innerHTML = '<div class="no-data-message">دادهای برای نمایش وجود ندارد</div>';
|
||
return;
|
||
}
|
||
|
||
// Debug: log first node to see data structure
|
||
console.log('OrgChart Data Sample:', data[0]);
|
||
|
||
try {
|
||
this.chart = new d3.OrgChart()
|
||
.container('#' + containerId)
|
||
.data(data)
|
||
.nodeWidth((d) => 140)
|
||
.nodeHeight((d) => 85)
|
||
.childrenMargin((d) => 50)
|
||
.compactMarginBetween((d) => 15)
|
||
.compactMarginPair((d) => 15)
|
||
.neighbourMargin((a, b) => 15)
|
||
.siblingsMargin((d) => 15)
|
||
.buttonContent(({ node, state }) => {
|
||
const hasChildren = node.data._directSubordinates > 0;
|
||
const isExpanded = node.children;
|
||
return hasChildren ? `<div class="org-expand-btn">
|
||
<span>${isExpanded ? '−' : '+'}</span>
|
||
</div>` : '';
|
||
})
|
||
.linkUpdate(function (d, i, arr) {
|
||
d3.select(this)
|
||
.attr('stroke', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? '#0380C0' : '#ccc')
|
||
.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 === '';
|
||
const positionClass = data.position === 'Left' ? 'position-left' :
|
||
data.position === 'Right' ? 'position-right' : 'position-root';
|
||
const activeClass = data.isActive ? 'active' : 'inactive';
|
||
const clubActiveClass = data.isClubActive ? 'club-active' : '';
|
||
|
||
// Avatar - first letter of name
|
||
const firstChar = data.fullName ? data.fullName.charAt(0) : '?';
|
||
|
||
const packageName = data.packageName || data.PackageName || '';
|
||
const packageBadge = packageName
|
||
? `<div class="package-name-badge">${packageName}</div>`
|
||
: '';
|
||
|
||
// نمایش کد معرف فقط برای کاربران فعال در باشگاه
|
||
const referralCodeHtml = data.isClubActive && data.referralCode
|
||
? `<div class="node-referral" onclick="event.stopPropagation(); OrgChart.copyReferralCode('${data.referralCode}');" title="کپی کد معرف">
|
||
<span class="referral-icon">📋</span>
|
||
<span class="referral-code">${data.referralCode}</span>
|
||
</div>`
|
||
: '';
|
||
|
||
return `
|
||
<div class="org-node-card ${positionClass} ${activeClass} ${clubActiveClass}" data-user-id="${data.id}">
|
||
<div class="node-header-compact ${isRoot ? 'root' : ''}">
|
||
<div class="node-avatar-sm">${firstChar}</div>
|
||
<div class="node-info">
|
||
<div class="node-name-sm">${data.fullName || 'بدون نام'}</div>
|
||
<div class="node-level-sm">L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}</div>
|
||
${packageBadge}
|
||
${referralCodeHtml}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
})
|
||
.onNodeClick((d) => {
|
||
if (this.dotNetHelper) {
|
||
// Convert string id to number for C# long
|
||
const userId = parseInt(d.data.id, 10);
|
||
this.dotNetHelper.invokeMethodAsync('OnNodeClicked', userId);
|
||
}
|
||
})
|
||
.render();
|
||
|
||
// Initial centering and zoom
|
||
this.chart.fit();
|
||
|
||
} catch (error) {
|
||
console.error('OrgChart: Error initializing chart:', error);
|
||
container.innerHTML = '<div class="error-message">خطا در بارگذاری نمودار</div>';
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Update the chart with new data
|
||
* @param {object} data - The new tree data
|
||
*/
|
||
update: function (data) {
|
||
if (this.chart) {
|
||
this.chart.data(data).render();
|
||
this.chart.fit();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 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 the chart
|
||
*/
|
||
center: function () {
|
||
if (this.chart) {
|
||
this.chart.fit();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Fit entire tree to screen (zoom out completely)
|
||
*/
|
||
fitToScreen: function () {
|
||
if (this.chart) {
|
||
this.chart.fit();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Zoom to specific node
|
||
* @param {string} nodeId - The ID of the node to zoom to
|
||
*/
|
||
zoomToNode: function (nodeId) {
|
||
if (this.chart) {
|
||
this.chart.setCentered(nodeId).render();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Highlight path to specific node
|
||
* @param {string} nodeId - The ID of the node
|
||
*/
|
||
highlightNode: function (nodeId) {
|
||
if (this.chart) {
|
||
this.chart.setHighlighted(nodeId).render();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Clear highlighting
|
||
*/
|
||
clearHighlight: function () {
|
||
if (this.chart) {
|
||
this.chart.clearHighlighting().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();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Copy referral code to clipboard
|
||
* @param {string} code - The referral code to copy
|
||
*/
|
||
copyReferralCode: function(code) {
|
||
if (navigator.clipboard) {
|
||
navigator.clipboard.writeText(code).then(() => {
|
||
// Show toast notification
|
||
this.showToast('کد معرف کپی شد: ' + code);
|
||
}).catch(err => {
|
||
console.error('Failed to copy:', err);
|
||
this.fallbackCopy(code);
|
||
});
|
||
} else {
|
||
this.fallbackCopy(code);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Fallback copy method for older browsers
|
||
*/
|
||
fallbackCopy: function(text) {
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
textArea.style.position = 'fixed';
|
||
textArea.style.left = '-999999px';
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
this.showToast('کد معرف کپی شد: ' + text);
|
||
} catch (err) {
|
||
console.error('Fallback copy failed:', err);
|
||
}
|
||
document.body.removeChild(textArea);
|
||
},
|
||
|
||
/**
|
||
* Show toast notification
|
||
*/
|
||
showToast: function(message) {
|
||
// Remove existing toast
|
||
const existingToast = document.querySelector('.org-chart-toast');
|
||
if (existingToast) existingToast.remove();
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = 'org-chart-toast';
|
||
toast.textContent = message;
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => toast.classList.add('show'), 10);
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, 2000);
|
||
},
|
||
|
||
/**
|
||
* Dispose the chart
|
||
*/
|
||
dispose: function () {
|
||
if (this.chart) {
|
||
// d3-org-chart doesn't have built-in dispose, so we just clear the reference
|
||
this.chart = null;
|
||
this.dotNetHelper = null;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Convert hierarchical tree data to flat array format for d3-org-chart
|
||
* @param {object} rootNode - The root node with nested children
|
||
* @returns {array} - Flat array of nodes
|
||
*/
|
||
convertToFlatArray: function (rootNode) {
|
||
if (!rootNode) return [];
|
||
|
||
const result = [];
|
||
|
||
function traverse(node, parentId) {
|
||
const flatNode = {
|
||
id: node.userId?.toString() || node.id?.toString(),
|
||
parentId: parentId,
|
||
fullName: node.fullName || '',
|
||
mobile: node.mobile || '',
|
||
avatar: node.avatar,
|
||
position: node.position || 'Root',
|
||
level: node.level || 0,
|
||
isActive: node.isActive !== false,
|
||
isClubActive: node.isClubActive || false,
|
||
activationWeekNumber: node.activationWeekNumber,
|
||
joinedAt: node.joinedAt
|
||
};
|
||
|
||
result.push(flatNode);
|
||
|
||
// Process left child
|
||
if (node.leftChild) {
|
||
traverse(node.leftChild, flatNode.id);
|
||
}
|
||
|
||
// Process right child
|
||
if (node.rightChild) {
|
||
traverse(node.rightChild, flatNode.id);
|
||
}
|
||
}
|
||
|
||
traverse(rootNode, '');
|
||
return result;
|
||
}
|
||
};
|