/** * 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 = '
داده‌ای برای نمایش وجود ندارد
'; 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 ? `
${isExpanded ? '−' : '+'}
` : ''; }) .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 ? `
${packageName}
` : ''; // نمایش کد معرف فقط برای کاربران فعال در باشگاه const referralCodeHtml = data.isClubActive && data.referralCode ? `
📋 ${data.referralCode}
` : ''; return `
${firstChar}
${data.fullName || 'بدون نام'}
L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}
${packageBadge} ${referralCodeHtml}
`; }) .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 = '
خطا در بارگذاری نمودار
'; } }, /** * 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; } };