redmine-org-chart/app/views/departments/org_chart.html.erb
ioresponse b50ab25083 Initial commit: Redmine Organization Chart Plugin
🤖 Generated with Claude Code
2025-12-23 00:21:49 +09:00

361 lines
9.4 KiB
Plaintext

<%= javascript_tag do %>
function toggleDept(id) {
var children = document.querySelectorAll('.dept-children-' + id);
var toggle = document.getElementById('toggle-' + id);
children.forEach(function(el) {
if (el.style.display === 'none') {
el.style.display = 'flex';
toggle.textContent = '[-]';
} else {
el.style.display = 'none';
toggle.textContent = '[+]';
}
});
}
<% end %>
<style>
.org-chart-container {
font-family: 'Malgun Gothic', Arial, sans-serif;
padding: 30px;
background: #f5f5f5;
min-height: 80vh;
overflow-x: auto;
}
.org-chart-title {
text-align: center;
margin-bottom: 30px;
color: #333;
}
.org-level {
display: flex;
justify-content: center;
flex-wrap: wrap;
position: relative;
padding: 20px 0;
}
.org-level::before {
content: '';
position: absolute;
top: 0;
left: 50%;
border-left: 2px solid #ccc;
height: 20px;
}
.org-level.first-level::before {
display: none;
}
.org-level-label {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
background: #e9ecef;
padding: 5px 10px;
border-radius: 4px;
font-size: 11px;
color: #666;
}
.org-node {
display: flex;
flex-direction: column;
align-items: center;
margin: 10px 15px;
position: relative;
}
.org-node::before {
content: '';
position: absolute;
top: -20px;
left: 50%;
border-left: 2px solid #ccc;
height: 20px;
}
.org-level.first-level .org-node::before {
display: none;
}
.org-card {
background: #fff;
border-radius: 8px;
padding: 15px 20px;
min-width: 160px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
position: relative;
border-top: 4px solid #6c757d;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.org-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.org-card.org-ceo {
border-top-color: #dc3545;
min-width: 200px;
background: linear-gradient(to bottom, #fff5f5, #fff);
}
.org-card.org-executive {
border-top-color: #6f42c1;
background: linear-gradient(to bottom, #f8f5ff, #fff);
}
.org-card.org-division {
border-top-color: #007bff;
}
.org-card.org-team {
border-top-color: #28a745;
}
.org-type {
font-size: 10px;
color: #999;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.org-card h4 {
margin: 0 0 8px 0;
color: #333;
font-size: 14px;
font-weight: bold;
}
.org-leader {
font-size: 13px;
color: #007bff;
margin-bottom: 5px;
}
.org-leader.no-leader {
color: #999;
font-style: italic;
}
.org-leader.acting {
color: #856404;
}
.org-members {
font-size: 11px;
color: #666;
}
.org-connector {
width: 100%;
display: flex;
justify-content: center;
position: relative;
height: 30px;
}
.org-connector::after {
content: '';
position: absolute;
top: 0;
left: 50%;
border-left: 2px solid #ccc;
height: 30px;
}
.org-branch {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 10px;
}
.org-branch-children {
display: flex;
justify-content: center;
flex-wrap: wrap;
position: relative;
padding-top: 20px;
}
.org-branch-children::before {
content: '';
position: absolute;
top: 0;
left: 50%;
border-left: 2px solid #ccc;
height: 20px;
}
.org-branch-children-vertical {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding-top: 15px;
margin-top: 5px;
}
.org-branch-children-vertical::before {
content: '';
position: absolute;
top: 0;
left: 50%;
border-left: 2px solid #ccc;
height: 15px;
}
.org-node-vertical {
position: relative;
margin: 5px 0;
}
.org-node-vertical::before {
content: '';
position: absolute;
top: 50%;
left: -20px;
border-top: 2px solid #ccc;
width: 20px;
}
.org-node-vertical:first-child::after {
content: '';
position: absolute;
top: 50%;
left: -20px;
border-left: 2px solid #ccc;
height: 50%;
transform: translateY(-100%);
}
.org-node-vertical:last-child::after {
content: '';
position: absolute;
top: 0;
left: -20px;
border-left: 2px solid #ccc;
height: 50%;
}
.org-node-vertical:not(:first-child):not(:last-child)::after {
content: '';
position: absolute;
top: 0;
left: -20px;
border-left: 2px solid #ccc;
height: 100%;
}
.org-branch-children-vertical .org-card {
min-width: 140px;
padding: 10px 15px;
}
.org-branch-children-vertical .org-card h4 {
font-size: 13px;
}
.no-data {
text-align: center;
padding: 50px;
color: #999;
}
.horizontal-line {
height: 2px;
background: #ccc;
margin: 0 20px;
position: relative;
}
</style>
<div class="org-chart-container">
<h2 class="org-chart-title">조직도</h2>
<p style="text-align: center; margin-bottom: 20px;">
<%= link_to '조직도 관리', '/org_chart', class: 'button' %>
</p>
<% ceo = Department.ceo.first %>
<% if ceo %>
<%# CEO 레벨 %>
<div class="org-level first-level">
<div class="org-node" style="margin: 0;">
<div class="org-card org-ceo" onclick="location.href='<%= department_path(ceo) %>'">
<div class="org-type">CEO</div>
<h4><%= ceo.name %></h4>
<% if ceo.effective_leader %>
<div class="org-leader <%= 'acting' if ceo.has_acting_leader? %>">
<%= ceo.has_acting_leader? ? '대결: ' : '' %><%= ceo.effective_leader.name %>
</div>
<% end %>
</div>
</div>
</div>
<% executives = Department.executives.sorted %>
<% if executives.any? %>
<div class="org-connector"></div>
<%# C-Level 레벨 %>
<div class="org-level" style="background: rgba(111, 66, 193, 0.05); border-radius: 8px; margin: 0 50px;">
<span class="org-level-label">C-Level</span>
<% executives.each do |exec| %>
<div class="org-node">
<div class="org-card org-executive" onclick="location.href='<%= department_path(exec) %>'">
<div class="org-type"><%= exec.type_name %></div>
<h4><%= exec.name %></h4>
<% if exec.effective_leader %>
<div class="org-leader <%= 'acting' if exec.has_acting_leader? %>">
<%= exec.has_acting_leader? ? '대결: ' : '' %><%= exec.effective_leader.name %>
</div>
<% else %>
<div class="org-leader no-leader">(담당자)</div>
<% end %>
<div class="org-members"><%= exec.all_members.count %>명</div>
</div>
<%# C-Level 산하 본부/팀 %>
<% exec_divisions = Department.divisions.where(parent_id: exec.id).sorted %>
<% exec_teams = Department.teams.where(parent_id: exec.id).sorted %>
<% if exec_divisions.any? || exec_teams.any? %>
<div class="org-branch-children">
<% exec_divisions.each do |div| %>
<%= render_org_branch(div) %>
<% end %>
<% exec_teams.each do |team| %>
<div class="org-node">
<div class="org-card org-team" onclick="location.href='<%= department_path(team) %>'">
<div class="org-type">팀</div>
<h4><%= team.name %></h4>
<% if team.effective_leader %>
<div class="org-leader <%= 'acting' if team.has_acting_leader? %>">
<%= team.has_acting_leader? ? '대결: ' : '' %><%= team.effective_leader.name %>
</div>
<% else %>
<div class="org-leader no-leader">(팀장 미지정)</div>
<% end %>
<div class="org-members"><%= team.all_members.count %>명</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
<%# CEO 직속 본부/팀 (C-Level 거치지 않는) %>
<% direct_divisions = Department.divisions.where(parent_id: [nil, ceo.id]).sorted %>
<% direct_teams = Department.teams.where(parent_id: [nil, ceo.id]).sorted %>
<% if direct_divisions.any? || direct_teams.any? %>
<div class="org-connector"></div>
<div class="org-level" style="background: rgba(0, 123, 255, 0.05); border-radius: 8px; margin: 0 50px;">
<span class="org-level-label">본부/팀</span>
<% direct_divisions.each do |div| %>
<%= render_org_branch(div) %>
<% end %>
<% direct_teams.each do |team| %>
<div class="org-node">
<div class="org-card org-team" onclick="location.href='<%= department_path(team) %>'">
<div class="org-type">팀</div>
<h4><%= team.name %></h4>
<% if team.effective_leader %>
<div class="org-leader <%= 'acting' if team.has_acting_leader? %>">
<%= team.has_acting_leader? ? '대결: ' : '' %><%= team.effective_leader.name %>
</div>
<% else %>
<div class="org-leader no-leader">(팀장 미지정)</div>
<% end %>
<div class="org-members"><%= team.all_members.count %>명</div>
</div>
</div>
<% end %>
</div>
<% end %>
<% else %>
<div class="no-data">
<p>조직도가 설정되지 않았습니다.</p>
<p><%= link_to 'CEO 등록하기', new_department_path(type: 'ceo'), class: 'button-positive' %></p>
</div>
<% end %>
</div>