Features:
- Custom workflow creation per project/tracker
- Step-by-step workflow definition
- Assignees per step (user, role group, department)
- Next/Previous step navigation
- Reject to first step
- Skip step (admin only)
- Step deadline settings
- Workflow dashboard
- Group member selection when proceeding
🤖 Generated with Claude Code
229 lines
8.6 KiB
Plaintext
229 lines
8.6 KiB
Plaintext
<h2><%= @workflow.name %></h2>
|
||
|
||
<% if @workflow.description.present? %>
|
||
<p><%= @workflow.description %></p>
|
||
<% end %>
|
||
|
||
<p>
|
||
<strong>프로젝트:</strong> <%= @workflow.project&.name || '모든 프로젝트' %> |
|
||
<strong>트래커:</strong> <%= @workflow.tracker&.name || '모든 트래커' %> |
|
||
<strong>상태:</strong> <%= @workflow.active ? '활성' : '비활성' %>
|
||
<% if @workflow.is_default %>
|
||
| <strong>기본 워크플로우</strong>
|
||
<% end %>
|
||
</p>
|
||
|
||
<hr />
|
||
|
||
<h3>워크플로우 단계</h3>
|
||
|
||
<div class="box" style="margin-bottom: 20px;">
|
||
<%= form_tag custom_workflow_steps_path(@workflow), method: :post, class: 'tabular' do %>
|
||
<p>
|
||
<label>단계명:</label>
|
||
<%= text_field_tag 'workflow_step[name]', '', size: 20, required: true, placeholder: '예: 개발, QA...' %>
|
||
|
||
<label>이슈 상태:</label>
|
||
<%= select_tag 'workflow_step[issue_status_id]',
|
||
options_from_collection_for_select(IssueStatus.sorted, :id, :name),
|
||
include_blank: '-- 선택 --', style: 'width: 150px;' %>
|
||
|
||
<%= submit_tag '단계 추가', class: 'button-positive' %>
|
||
</p>
|
||
<% end %>
|
||
</div>
|
||
|
||
<% if @steps.any? %>
|
||
<table class="list">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">순서</th>
|
||
<th>단계명</th>
|
||
<th>이슈 상태</th>
|
||
<th style="width: 80px;">기한(일)</th>
|
||
<th>담당자</th>
|
||
<th style="width: 200px;">작업</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% @steps.each_with_index do |step, idx| %>
|
||
<tr>
|
||
<td style="text-align: center;">
|
||
<%= idx + 1 %>
|
||
<% if step.is_start %>
|
||
<br/><span style="background: #007bff; color: white; padding: 1px 4px; border-radius: 2px; font-size: 9px;">시작</span>
|
||
<% end %>
|
||
<% if step.is_end %>
|
||
<br/><span style="background: #dc3545; color: white; padding: 1px 4px; border-radius: 2px; font-size: 9px;">종료</span>
|
||
<% end %>
|
||
</td>
|
||
<td>
|
||
<strong><%= step.name %></strong>
|
||
<% if step.description.present? %>
|
||
<br/><small style="color: #666;"><%= step.description %></small>
|
||
<% end %>
|
||
</td>
|
||
<td>
|
||
<%= form_tag custom_workflow_step_path(@workflow, step), method: :patch, style: 'display: inline;' do %>
|
||
<%= select_tag 'workflow_step[issue_status_id]',
|
||
options_from_collection_for_select(IssueStatus.sorted, :id, :name, step.issue_status_id),
|
||
include_blank: '-- 선택 --',
|
||
style: 'font-size: 11px; padding: 2px;',
|
||
onchange: 'this.form.submit();' %>
|
||
<% end %>
|
||
</td>
|
||
<td>
|
||
<%= form_tag custom_workflow_step_path(@workflow, step), method: :patch, style: 'display: inline;' do %>
|
||
<%= number_field_tag 'workflow_step[due_days]', step.due_days,
|
||
min: 1, max: 365,
|
||
style: 'width: 50px; font-size: 11px; padding: 2px;',
|
||
placeholder: '-',
|
||
onchange: 'this.form.submit();' %>
|
||
<% end %>
|
||
</td>
|
||
<td>
|
||
<% if step.workflow_step_assignees.any? %>
|
||
<% step.workflow_step_assignees.each do |assignee| %>
|
||
<span style="display: inline-block; background: #e9ecef; padding: 2px 8px; border-radius: 3px; margin: 2px; font-size: 12px;">
|
||
<%= assignee.assignee_name %>
|
||
<%= link_to '×', step_assignee_path(step_id: step.id, id: assignee.id),
|
||
method: :delete, style: 'color: #dc3545; margin-left: 5px;', title: '제거' %>
|
||
</span>
|
||
<% end %>
|
||
<% else %>
|
||
<em style="color: #999;">담당자 없음</em>
|
||
<% end %>
|
||
<br/>
|
||
<a href="#" onclick="showAssigneeModal(<%= step.id %>); return false;" style="font-size: 11px;">+ 담당자 추가</a>
|
||
</td>
|
||
<td>
|
||
<%= link_to '▲', move_custom_workflow_step_path(@workflow, step, direction: 'up'), method: :patch, class: 'icon', title: '위로' unless idx == 0 %>
|
||
<%= link_to '▼', move_custom_workflow_step_path(@workflow, step, direction: 'down'), method: :patch, class: 'icon', title: '아래로' unless idx == @steps.length - 1 %>
|
||
|
|
||
<%= link_to '삭제', custom_workflow_step_path(@workflow, step), method: :delete,
|
||
data: { confirm: '이 단계를 삭제하시겠습니까?' }, class: 'icon icon-del' %>
|
||
</td>
|
||
</tr>
|
||
<% end %>
|
||
</tbody>
|
||
</table>
|
||
<% else %>
|
||
<p class="nodata">단계가 없습니다. 위에서 단계를 추가하세요.</p>
|
||
<% end %>
|
||
|
||
<hr />
|
||
|
||
<p>
|
||
<%= link_to '목록으로', '/custom_workflows', class: 'icon icon-back' %>
|
||
<%= link_to '수정', edit_custom_workflow_path(@workflow), class: 'icon icon-edit' %>
|
||
</p>
|
||
|
||
<!-- 담당자 추가 모달 -->
|
||
<div id="assignee-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;">
|
||
<div style="background: white; width: 500px; margin: 100px auto; padding: 20px; border-radius: 8px; max-height: 80vh; overflow-y: auto;">
|
||
<h3>담당자 추가</h3>
|
||
<input type="hidden" id="modal-step-id" />
|
||
|
||
<div style="margin-bottom: 15px;">
|
||
<label><strong>담당자 유형:</strong></label><br/>
|
||
<select id="assignee-type" onchange="loadAssigneeOptions()" style="width: 100%; padding: 5px;">
|
||
<option value="user">사용자</option>
|
||
<option value="role_group">역할 그룹</option>
|
||
<option value="department">부서</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 15px;">
|
||
<label><strong>검색:</strong></label><br/>
|
||
<input type="text" id="assignee-search" placeholder="검색어 입력..." style="width: 100%; padding: 5px;" />
|
||
</div>
|
||
|
||
<div id="assignee-list" style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
|
||
<p style="color: #999;">유형을 선택하고 검색하세요.</p>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; text-align: right;">
|
||
<button onclick="closeAssigneeModal()" class="button">닫기</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<%= javascript_tag do %>
|
||
var currentStepId = null;
|
||
var searchTimeout = null;
|
||
|
||
function showAssigneeModal(stepId) {
|
||
currentStepId = stepId;
|
||
document.getElementById('modal-step-id').value = stepId;
|
||
document.getElementById('assignee-modal').style.display = 'block';
|
||
document.getElementById('assignee-search').value = '';
|
||
loadAssigneeOptions();
|
||
}
|
||
|
||
function closeAssigneeModal() {
|
||
document.getElementById('assignee-modal').style.display = 'none';
|
||
currentStepId = null;
|
||
}
|
||
|
||
function loadAssigneeOptions() {
|
||
var type = document.getElementById('assignee-type').value;
|
||
var query = document.getElementById('assignee-search').value;
|
||
var listDiv = document.getElementById('assignee-list');
|
||
|
||
var url = '';
|
||
if (type === 'user') {
|
||
url = '<%= search_users_workflows_path %>';
|
||
} else if (type === 'role_group') {
|
||
url = '<%= search_role_groups_workflows_path %>';
|
||
} else if (type === 'department') {
|
||
url = '<%= search_departments_workflows_path %>';
|
||
}
|
||
|
||
$.ajax({
|
||
url: url,
|
||
data: { q: query },
|
||
dataType: 'json',
|
||
success: function(data) {
|
||
var html = '';
|
||
if (data.length === 0) {
|
||
html = '<p style="color: #999;">결과 없음</p>';
|
||
} else {
|
||
html = '<table style="width: 100%;"><tbody>';
|
||
data.forEach(function(item) {
|
||
var label = item.name;
|
||
if (item.count !== undefined) {
|
||
label += ' (' + item.count + '명)';
|
||
}
|
||
if (item.login) {
|
||
label += ' - ' + item.login;
|
||
}
|
||
html += '<tr>';
|
||
html += '<td>' + label + '</td>';
|
||
html += '<td style="text-align: right;">';
|
||
html += '<form action="/workflow_steps/' + currentStepId + '/assignees" method="post" style="display:inline;">';
|
||
html += '<input type="hidden" name="authenticity_token" value="' + $('meta[name="csrf-token"]').attr('content') + '">';
|
||
html += '<input type="hidden" name="assignee_type" value="' + type + '">';
|
||
html += '<input type="hidden" name="assignee_id" value="' + item.id + '">';
|
||
html += '<input type="submit" value="추가" class="button-positive" style="padding: 2px 8px; font-size: 11px;">';
|
||
html += '</form>';
|
||
html += '</td>';
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
}
|
||
listDiv.innerHTML = html;
|
||
}
|
||
});
|
||
}
|
||
|
||
$('#assignee-search').on('keyup', function() {
|
||
if (searchTimeout) clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(loadAssigneeOptions, 300);
|
||
});
|
||
|
||
// 모달 외부 클릭시 닫기
|
||
document.getElementById('assignee-modal').addEventListener('click', function(e) {
|
||
if (e.target === this) closeAssigneeModal();
|
||
});
|
||
<% end %>
|