redmine-workflow-engine/app/views/workflows/show.html.erb
ioresponse e67fb92189 Initial commit: Redmine Workflow Engine Plugin
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
2025-12-23 00:16:43 +09:00

229 lines
8.6 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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...' %>
&nbsp;
<label>이슈 상태:</label>
<%= select_tag 'workflow_step[issue_status_id]',
options_from_collection_for_select(IssueStatus.sorted, :id, :name),
include_blank: '-- 선택 --', style: 'width: 150px;' %>
&nbsp;
<%= 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 %>