/**
* 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 `
`;
})
.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;
}
};