This commit is contained in:
MeysamMoghaddam
2025-10-13 18:56:46 +03:30
parent 15d9f7f96a
commit 18d2ac62b7
3 changed files with 475 additions and 0 deletions

View File

@@ -183,6 +183,13 @@
</div>
</MudTabPanel>
<!-- Organization Chart Tab -->
<MudTabPanel Text="شجره‌نامه" Icon="@Icons.Material.Filled.AccountTree">
<div class="pa-4">
<OrganizationChart />
</div>
</MudTabPanel>
<!-- Account Settings Tab -->
<MudTabPanel Text="تنظیمات حساب" Icon="@Icons.Material.Filled.Settings">
<div class="pa-4">

View File

@@ -0,0 +1,239 @@
@inject IJSRuntime JSRuntime
<div class="org-chart-container">
<div class="org-tree">
<div class="org-node root-node" id="root-node">
<div class="node-card">
<div class="node-avatar">
<MudAvatar Size="Size.Large">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar1.jpg" />
</MudAvatar>
@if (_currentUser?.Children?.Any() == true)
{
<button class="expand-btn" @onclick="ToggleExpand" data-user-id="@_currentUser.Id">
<MudIcon Icon="@(_isExpanded ? Icons.Material.Filled.Remove : Icons.Material.Filled.Add)" Size="Size.Small" />
</button>
}
</div>
<div class="node-info">
<div class="node-name">@(_currentUser?.FirstName) @(_currentUser?.LastName)</div>
<div class="node-amounts">
<div class="personal-amount">
<span class="label">خرید شخصی:</span>
<span class="amount">@(_currentUser?.PersonalPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
<div class="team-amount">
<span class="label">خرید تیمی:</span>
<span class="amount">@(_currentUser?.TeamPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
</div>
</div>
</div>
</div>
@if (_isExpanded && _currentUser?.Children?.Any() == true)
{
<div class="org-level" id="level-1">
@foreach (var child in _currentUser.Children)
{
<div class="org-node child-node" data-parent-id="@_currentUser.Id">
<div class="node-connector"></div>
<div class="node-card">
<div class="node-avatar">
<MudAvatar Size="Size.Medium">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="@(string.IsNullOrEmpty(child.Avatar) ? "images/avatar1.jpg" : child.Avatar)" />
</MudAvatar>
@if (child.Children?.Any() == true)
{
<button class="expand-btn" @onclick="() => ToggleChildExpand(child.Id)" data-user-id="@child.Id">
<MudIcon Icon="@(child.IsExpanded ? Icons.Material.Filled.Remove : Icons.Material.Filled.Add)" Size="Size.Small" />
</button>
}
</div>
<div class="node-info">
<div class="node-name">@child.FirstName @child.LastName</div>
<div class="node-amounts">
<div class="personal-amount">
<span class="label">خرید شخصی:</span>
<span class="amount">@(child.PersonalPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
<div class="team-amount">
<span class="label">خرید تیمی:</span>
<span class="amount">@(child.TeamPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
</div>
</div>
</div>
@if (child.IsExpanded && child.Children?.Any() == true)
{
<div class="org-sublevel">
@foreach (var grandchild in child.Children)
{
<div class="org-node grandchild-node" data-parent-id="@child.Id">
<div class="node-connector"></div>
<div class="node-card">
<div class="node-avatar">
<MudAvatar Size="Size.Small">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="@(string.IsNullOrEmpty(grandchild.Avatar) ? "images/avatar1.jpg" : grandchild.Avatar)" />
</MudAvatar>
</div>
<div class="node-info">
<div class="node-name">@grandchild.FirstName @grandchild.LastName</div>
<div class="node-amounts">
<div class="personal-amount">
<span class="label">خرید شخصی:</span>
<span class="amount">@(grandchild.PersonalPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
<div class="team-amount">
<span class="label">خرید تیمی:</span>
<span class="amount">@(grandchild.TeamPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
</div>
</div>
</div>
</div>
}
</div>
}
</div>
}
</div>
}
</div>
</div>
@code {
private UserNode? _currentUser;
private bool _isExpanded;
protected override async Task OnInitializedAsync()
{
await LoadCurrentUser();
}
private async Task LoadCurrentUser()
{
// Mock data - replace with actual API call
_currentUser = new UserNode
{
Id = 1,
FirstName = "علی",
LastName = "رضایی",
Avatar = "images/avatar1.jpg",
PersonalPurchase = 2500000,
TeamPurchase = 15000000,
Children = new List<UserNode>
{
new UserNode
{
Id = 2,
FirstName = "مریم",
LastName = "احمدی",
Avatar = "images/avatar2.jpg",
PersonalPurchase = 1800000,
TeamPurchase = 8500000,
Children = new List<UserNode>
{
new UserNode
{
Id = 5,
FirstName = "سارا",
LastName = "کریمی",
PersonalPurchase = 1200000,
TeamPurchase = 3200000
},
new UserNode
{
Id = 6,
FirstName = "امیر",
LastName = "حسینی",
PersonalPurchase = 950000,
TeamPurchase = 1800000
}
}
},
new UserNode
{
Id = 3,
FirstName = "حسن",
LastName = "کریمی",
Avatar = "images/avatar3.jpg",
PersonalPurchase = 2200000,
TeamPurchase = 9200000,
Children = new List<UserNode>
{
new UserNode
{
Id = 7,
FirstName = "فاطمه",
LastName = "رضایی",
PersonalPurchase = 1350000,
TeamPurchase = 4100000
}
}
},
new UserNode
{
Id = 4,
FirstName = "زهرا",
LastName = "محمدی",
Avatar = "images/avatar4.jpg",
PersonalPurchase = 1950000,
TeamPurchase = 7800000
}
}
};
}
private void ToggleExpand()
{
_isExpanded = !_isExpanded;
StateHasChanged();
}
private void ToggleChildExpand(long userId)
{
var child = FindChild(_currentUser, userId);
if (child != null)
{
child.IsExpanded = !child.IsExpanded;
StateHasChanged();
}
}
private UserNode? FindChild(UserNode? node, long userId)
{
if (node == null) return null;
if (node.Id == userId) return node;
if (node.Children != null)
{
foreach (var child in node.Children)
{
var found = FindChild(child, userId);
if (found != null) return found;
}
}
return null;
}
public class UserNode
{
public long Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Avatar { get; set; }
public long? PersonalPurchase { get; set; }
public long? TeamPurchase { get; set; }
public List<UserNode>? Children { get; set; }
public bool IsExpanded { get; set; }
}
}

View File

@@ -0,0 +1,229 @@
.org-chart-container {
width: 100%;
overflow-x: auto;
padding: 20px 0;
}
.org-tree {
display: flex;
flex-direction: column;
align-items: center;
min-width: 800px;
}
.org-node {
position: relative;
margin: 20px 0;
}
.root-node {
margin-bottom: 40px;
}
.child-node {
display: inline-block;
margin: 0 15px;
vertical-align: top;
}
.grandchild-node {
display: inline-block;
margin: 20px 10px 0;
vertical-align: top;
}
.node-card {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 2px solid #f0f0f0;
min-width: 200px;
max-width: 280px;
transition: all 0.3s ease;
}
.node-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
border-color: #e0e0e0;
}
.node-avatar {
position: relative;
text-align: center;
margin-bottom: 12px;
}
.expand-btn {
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
background: #0380C0;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.expand-btn:hover {
background: #026a9e;
transform: translateX(-50%) scale(1.1);
}
.node-info {
text-align: center;
}
.node-name {
font-weight: 600;
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.node-amounts {
font-size: 12px;
}
.node-amounts > div {
margin-bottom: 4px;
}
.personal-amount {
color: #0380C0;
}
.team-amount {
color: #05AF82;
}
.label {
font-weight: 500;
margin-left: 4px;
}
.amount {
font-weight: 600;
}
.node-connector {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 20px;
background: #ddd;
}
.org-level {
display: flex;
justify-content: center;
flex-wrap: wrap;
position: relative;
}
.org-level::before {
content: '';
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 20px;
background: #ddd;
}
.org-sublevel {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 20px;
position: relative;
}
.org-sublevel::before {
content: '';
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 20px;
background: #ddd;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.org-chart-container {
padding: 10px 0;
}
.org-tree {
min-width: 100%;
}
.child-node {
margin: 0 8px;
}
.grandchild-node {
margin: 15px 5px 0;
}
.node-card {
min-width: 160px;
max-width: 220px;
padding: 12px;
}
.node-name {
font-size: 13px;
}
.node-amounts {
font-size: 11px;
}
}
/* Animation for expand/collapse */
.org-level,
.org-sublevel {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Connector lines styling */
.node-connector,
.org-level::before,
.org-sublevel::before {
background: linear-gradient(to bottom, #ddd, #bbb);
}
/* Hover effects */
.node-card:hover .node-avatar .mud-avatar {
transform: scale(1.05);
transition: transform 0.2s ease;
}
.expand-btn:active {
transform: translateX(-50%) scale(0.95);
}