feat: [approvals] 결재선/참조선 2영역 분리 UI
- 결재선 에디터를 결재선(결재/합의)과 참조선으로 분리 - 좌측 인원 목록에 '결재' / '참조' 두 버튼 제공 - 결재선: 드래그 정렬, 결재/합의 유형 선택 - 참조선: 칩(태그) 형태로 표시, 상신 즉시 열람 가능 - show 페이지에 참조자 목록 표시 추가 - getStepsData()에서 결재선+참조선 합산하여 기존 API 호환 유지
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
{{-- 결재선 편집 컴포넌트 (2패널 구조 — Alpine.js + SortableJS) --}}
|
||||
{{-- 결재선 + 참조선 편집 컴포넌트 (좌측 인원 / 우측 결재선·참조선 — Alpine.js + SortableJS) --}}
|
||||
<div id="approval-line-editor"
|
||||
x-data="approvalLineEditor()"
|
||||
x-init="init()"
|
||||
class="bg-white rounded-lg shadow-sm">
|
||||
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800">결재선</h3>
|
||||
</div>
|
||||
|
||||
{{-- 2패널 컨테이너 --}}
|
||||
<div class="flex" style="min-height: 400px;">
|
||||
<div class="flex" style="min-height: 480px;">
|
||||
|
||||
{{-- 좌측: 인원 목록 --}}
|
||||
<div class="border-r border-gray-200" style="flex: 0 0 280px; max-width: 280px;">
|
||||
{{-- 헤더 --}}
|
||||
<div class="p-3 border-b border-gray-200">
|
||||
<h3 class="text-sm font-semibold text-gray-800">인원 목록</h3>
|
||||
</div>
|
||||
|
||||
{{-- 검색 --}}
|
||||
<div class="p-3 border-b border-gray-100">
|
||||
<div class="relative">
|
||||
@@ -27,7 +28,7 @@ class="w-full pl-8 pr-3 py-2 border border-gray-300 rounded-lg text-xs focus:out
|
||||
</div>
|
||||
|
||||
{{-- 부서별 인원 목록 (스크롤) --}}
|
||||
<div class="overflow-y-auto" style="max-height: 420px;">
|
||||
<div class="overflow-y-auto" style="max-height: 400px;">
|
||||
<template x-if="loading">
|
||||
<div class="flex items-center justify-center py-8 text-gray-400 text-sm">
|
||||
<svg class="animate-spin w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24">
|
||||
@@ -69,21 +70,33 @@ class="w-3 h-3 transition-transform" fill="none" stroke="currentColor" viewBox="
|
||||
<div x-show="isDeptExpanded(dept.department_id)"
|
||||
x-collapse>
|
||||
<template x-for="user in dept.users" :key="user.id">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 hover:bg-blue-50 transition"
|
||||
:class="isAdded(user.id) ? 'opacity-50' : ''">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 hover:bg-blue-50 transition">
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="text-xs font-medium text-gray-800" x-text="user.name"></span>
|
||||
<span class="text-xs text-gray-400 ml-1" x-text="user.position || user.job_title || ''"></span>
|
||||
</div>
|
||||
<button type="button"
|
||||
@click="addStep(user, dept.department_name)"
|
||||
:disabled="isAdded(user.id)"
|
||||
class="shrink-0 ml-2 text-xs px-2 py-0.5 rounded transition"
|
||||
:class="isAdded(user.id)
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-50 text-blue-600 hover:bg-blue-100'">
|
||||
<span x-text="isAdded(user.id) ? '추가됨' : '+ 추가'"></span>
|
||||
</button>
|
||||
<div class="shrink-0 ml-2 flex gap-1">
|
||||
<button type="button"
|
||||
@click="addStep(user, dept.department_name)"
|
||||
:disabled="isInApprovalLine(user.id)"
|
||||
class="text-xs px-1.5 py-0.5 rounded transition"
|
||||
:class="isInApprovalLine(user.id)
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-50 text-blue-600 hover:bg-blue-100'"
|
||||
title="결재선 추가">
|
||||
<span x-text="isInApprovalLine(user.id) ? '결재' : '+ 결재'"></span>
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="addReference(user, dept.department_name)"
|
||||
:disabled="isInReferenceLine(user.id)"
|
||||
class="text-xs px-1.5 py-0.5 rounded transition"
|
||||
:class="isInReferenceLine(user.id)
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-50 text-green-600 hover:bg-green-100'"
|
||||
title="참조선 추가">
|
||||
<span x-text="isInReferenceLine(user.id) ? '참조' : '+ 참조'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -92,78 +105,125 @@ class="shrink-0 ml-2 text-xs px-2 py-0.5 rounded transition"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 우측: 결재선 --}}
|
||||
{{-- 우측: 결재선 + 참조선 --}}
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
{{-- 결재선 템플릿 --}}
|
||||
<div class="p-3 border-b border-gray-100">
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">결재선 템플릿</label>
|
||||
<select x-model="selectedLineId"
|
||||
@change="loadLine()"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">직접 설정</option>
|
||||
<template x-for="line in lines" :key="line.id">
|
||||
<option :value="line.id" x-text="line.name + ' (' + (line.steps?.length || 0) + '단계)'"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 결재자 목록 (드래그 가능) --}}
|
||||
<div class="flex-1 overflow-y-auto p-3" style="max-height: 380px;">
|
||||
<div x-ref="sortableList" class="space-y-2">
|
||||
<template x-for="(step, index) in steps" :key="step._key">
|
||||
<div class="flex items-center gap-2 p-2 bg-gray-50 rounded-lg border border-gray-200 group"
|
||||
:data-index="index">
|
||||
{{-- 드래그 핸들 --}}
|
||||
<span class="drag-handle shrink-0 cursor-grab active:cursor-grabbing text-gray-300 hover:text-gray-500 transition">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/>
|
||||
<circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/>
|
||||
<circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="19" r="1.5"/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
{{-- 순번 --}}
|
||||
<span class="shrink-0 flex items-center justify-center bg-blue-100 text-blue-700 rounded-full font-medium text-xs"
|
||||
style="width: 22px; height: 22px;"
|
||||
x-text="index + 1"></span>
|
||||
|
||||
{{-- 이름/부서/직위 --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-medium text-xs text-gray-800" x-text="step.user_name"></span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 truncate"
|
||||
x-text="[step.department, step.position].filter(Boolean).join(' / ')"></div>
|
||||
</div>
|
||||
|
||||
{{-- 유형 선택 --}}
|
||||
<select x-model="step.step_type"
|
||||
class="shrink-0 px-1.5 py-1 border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
style="width: 60px;">
|
||||
<option value="approval">결재</option>
|
||||
<option value="agreement">합의</option>
|
||||
<option value="reference">참조</option>
|
||||
</select>
|
||||
|
||||
{{-- 삭제 --}}
|
||||
<button type="button"
|
||||
@click="removeStep(index)"
|
||||
class="shrink-0 p-1 text-gray-300 hover:text-red-500 transition opacity-0 group-hover:opacity-100">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
{{-- ===== 결재선 영역 ===== --}}
|
||||
<div class="flex-1 flex flex-col border-b border-gray-200" style="min-height: 240px;">
|
||||
<div class="p-3 border-b border-gray-100 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-800 flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
결재선
|
||||
</h3>
|
||||
<div style="min-width: 180px;">
|
||||
<select x-model="selectedLineId"
|
||||
@change="loadLine()"
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
<option value="">직접 설정</option>
|
||||
<template x-for="line in lines" :key="line.id">
|
||||
<option :value="line.id" x-text="line.name + ' (' + (line.steps?.length || 0) + '단계)'"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 빈 상태 --}}
|
||||
<div x-show="steps.length === 0" class="flex flex-col items-center justify-center py-12 text-gray-400">
|
||||
<svg class="w-10 h-10 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<span class="text-xs">좌측에서 결재자를 추가하세요</span>
|
||||
{{-- 결재자 목록 (드래그 가능) --}}
|
||||
<div class="flex-1 overflow-y-auto p-3">
|
||||
<div x-ref="sortableList" class="space-y-2">
|
||||
<template x-for="(step, index) in steps" :key="step._key">
|
||||
<div class="flex items-center gap-2 p-2 bg-blue-50/50 rounded-lg border border-blue-200 group"
|
||||
:data-index="index">
|
||||
{{-- 드래그 핸들 --}}
|
||||
<span class="drag-handle shrink-0 cursor-grab active:cursor-grabbing text-gray-300 hover:text-gray-500 transition">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/>
|
||||
<circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/>
|
||||
<circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="19" r="1.5"/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
{{-- 순번 --}}
|
||||
<span class="shrink-0 flex items-center justify-center bg-blue-100 text-blue-700 rounded-full font-medium text-xs"
|
||||
style="width: 22px; height: 22px;"
|
||||
x-text="index + 1"></span>
|
||||
|
||||
{{-- 이름/부서/직위 --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-medium text-xs text-gray-800" x-text="step.user_name"></span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 truncate"
|
||||
x-text="[step.department, step.position].filter(Boolean).join(' / ')"></div>
|
||||
</div>
|
||||
|
||||
{{-- 유형 선택 (결재/합의) --}}
|
||||
<select x-model="step.step_type"
|
||||
class="shrink-0 px-1.5 py-1 border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
style="width: 60px;">
|
||||
<option value="approval">결재</option>
|
||||
<option value="agreement">합의</option>
|
||||
</select>
|
||||
|
||||
{{-- 삭제 --}}
|
||||
<button type="button"
|
||||
@click="removeStep(index)"
|
||||
class="shrink-0 p-1 text-gray-300 hover:text-red-500 transition opacity-0 group-hover:opacity-100">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- 빈 상태 --}}
|
||||
<div x-show="steps.length === 0" class="flex flex-col items-center justify-center py-8 text-gray-400">
|
||||
<svg class="w-8 h-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<span class="text-xs">좌측에서 결재자를 추가하세요</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== 참조선 영역 ===== --}}
|
||||
<div class="flex flex-col" style="min-height: 160px;">
|
||||
<div class="p-3 border-b border-gray-100">
|
||||
<h3 class="text-sm font-semibold text-gray-800 flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
참조
|
||||
<span class="text-xs font-normal text-gray-400 ml-1">상신 시 즉시 열람 가능</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{{-- 참조자 목록 --}}
|
||||
<div class="flex-1 overflow-y-auto p-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="(ref, index) in references" :key="ref._key">
|
||||
<div class="inline-flex items-center gap-1.5 px-2.5 py-1.5 bg-green-50 border border-green-200 rounded-full group">
|
||||
<span class="text-xs font-medium text-gray-700" x-text="ref.user_name"></span>
|
||||
<span class="text-xs text-gray-400" x-text="ref.department ? '(' + ref.department + ')' : ''"></span>
|
||||
<button type="button"
|
||||
@click="removeReference(index)"
|
||||
class="shrink-0 text-gray-300 hover:text-red-500 transition opacity-0 group-hover:opacity-100">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- 빈 상태 --}}
|
||||
<div x-show="references.length === 0" class="flex items-center justify-center py-6 text-gray-400">
|
||||
<span class="text-xs">좌측에서 참조자를 추가하세요</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,33 +232,46 @@ class="shrink-0 p-1 text-gray-300 hover:text-red-500 transition opacity-0 group-
|
||||
{{-- 하단: 요약 바 --}}
|
||||
<div class="px-4 py-2 border-t border-gray-200 bg-gray-50 rounded-b-lg flex items-center justify-between text-xs text-gray-500">
|
||||
<div>
|
||||
<span>결재: <strong class="text-gray-700" x-text="countByType('approval')"></strong>명</span>
|
||||
<span>결재: <strong class="text-blue-600" x-text="countByType('approval')"></strong>명</span>
|
||||
<span class="mx-1">|</span>
|
||||
<span>합의: <strong class="text-gray-700" x-text="countByType('agreement')"></strong>명</span>
|
||||
<span class="mx-1">|</span>
|
||||
<span>참조: <strong class="text-gray-700" x-text="countByType('reference')"></strong>명</span>
|
||||
<span>합의: <strong class="text-blue-600" x-text="countByType('agreement')"></strong>명</span>
|
||||
<span class="mx-2 text-gray-300">|</span>
|
||||
<span>참조: <strong class="text-green-600" x-text="references.length"></strong>명</span>
|
||||
</div>
|
||||
<div>
|
||||
합계 <strong class="text-gray-700" x-text="steps.length"></strong>명
|
||||
합계 <strong class="text-gray-700" x-text="steps.length + references.length"></strong>명
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- hidden inputs --}}
|
||||
{{-- hidden inputs: 결재선 --}}
|
||||
<template x-for="(step, index) in steps" :key="'hidden-' + step._key">
|
||||
<div>
|
||||
<input type="hidden" :name="'steps[' + index + '][user_id]'" :value="step.user_id">
|
||||
<input type="hidden" :name="'steps[' + index + '][step_type]'" :value="step.step_type">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- hidden inputs: 참조선 --}}
|
||||
<template x-for="(ref, index) in references" :key="'hidden-ref-' + ref._key">
|
||||
<div>
|
||||
<input type="hidden" :name="'references[' + index + '][user_id]'" :value="ref.user_id">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function approvalLineEditor() {
|
||||
let keyCounter = 0;
|
||||
|
||||
// 기존 steps에서 reference 유형을 분리
|
||||
const allSteps = (@json($initialSteps ?? []));
|
||||
const initSteps = allSteps.filter(s => s.step_type !== 'reference').map(s => ({ ...s, _key: ++keyCounter }));
|
||||
const initRefs = allSteps.filter(s => s.step_type === 'reference').map(s => ({ ...s, _key: ++keyCounter }));
|
||||
|
||||
return {
|
||||
departments: [],
|
||||
steps: (@json($initialSteps ?? [])).map(s => ({ ...s, _key: ++keyCounter })),
|
||||
steps: initSteps,
|
||||
references: initRefs,
|
||||
lines: @json($lines ?? []),
|
||||
selectedLineId: {!! $selectedLineId ? $selectedLineId : "''" !!},
|
||||
searchQuery: '',
|
||||
@@ -212,7 +285,6 @@ function approvalLineEditor() {
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.departments = data.data;
|
||||
// 기본적으로 모든 부서 펼침
|
||||
this.departments.forEach(d => {
|
||||
this.expandedDepts[d.department_id ?? 'none'] = true;
|
||||
});
|
||||
@@ -221,7 +293,6 @@ function approvalLineEditor() {
|
||||
console.error('인원 목록 로딩 실패:', e);
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
this.$nextTick(() => this.initSortable());
|
||||
},
|
||||
|
||||
@@ -253,15 +324,23 @@ function approvalLineEditor() {
|
||||
return this.expandedDepts[key] ?? false;
|
||||
},
|
||||
|
||||
isAdded(userId) {
|
||||
isInApprovalLine(userId) {
|
||||
return this.steps.some(s => s.user_id === userId);
|
||||
},
|
||||
|
||||
isInReferenceLine(userId) {
|
||||
return this.references.some(r => r.user_id === userId);
|
||||
},
|
||||
|
||||
addStep(user, deptName) {
|
||||
if (this.isAdded(user.id)) {
|
||||
if (typeof showToast === 'function') showToast('이미 추가된 결재자입니다.', 'warning');
|
||||
if (this.isInApprovalLine(user.id)) {
|
||||
if (typeof showToast === 'function') showToast('이미 결재선에 추가된 사용자입니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
// 참조선에 있으면 제거 후 결재선으로 이동
|
||||
const refIdx = this.references.findIndex(r => r.user_id === user.id);
|
||||
if (refIdx !== -1) this.references.splice(refIdx, 1);
|
||||
|
||||
this.steps.push({
|
||||
_key: ++keyCounter,
|
||||
user_id: user.id,
|
||||
@@ -273,23 +352,60 @@ function approvalLineEditor() {
|
||||
this.$nextTick(() => this.initSortable());
|
||||
},
|
||||
|
||||
addReference(user, deptName) {
|
||||
if (this.isInReferenceLine(user.id)) {
|
||||
if (typeof showToast === 'function') showToast('이미 참조선에 추가된 사용자입니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
// 결재선에 있으면 제거 후 참조선으로 이동
|
||||
const stepIdx = this.steps.findIndex(s => s.user_id === user.id);
|
||||
if (stepIdx !== -1) {
|
||||
this.steps.splice(stepIdx, 1);
|
||||
this.$nextTick(() => this.initSortable());
|
||||
}
|
||||
|
||||
this.references.push({
|
||||
_key: ++keyCounter,
|
||||
user_id: user.id,
|
||||
user_name: user.name,
|
||||
department: deptName || '',
|
||||
position: user.position || user.job_title || '',
|
||||
step_type: 'reference',
|
||||
});
|
||||
},
|
||||
|
||||
removeStep(index) {
|
||||
this.steps.splice(index, 1);
|
||||
this.$nextTick(() => this.initSortable());
|
||||
},
|
||||
|
||||
removeReference(index) {
|
||||
this.references.splice(index, 1);
|
||||
},
|
||||
|
||||
loadLine() {
|
||||
if (!this.selectedLineId) return;
|
||||
const line = this.lines.find(l => l.id == this.selectedLineId);
|
||||
if (line && line.steps) {
|
||||
this.steps = line.steps.map(s => ({
|
||||
_key: ++keyCounter,
|
||||
user_id: s.user_id,
|
||||
user_name: s.user_name || '사용자 ' + s.user_id,
|
||||
department: s.department || '',
|
||||
position: s.position || '',
|
||||
step_type: s.step_type || 'approval',
|
||||
}));
|
||||
const lineSteps = [];
|
||||
const lineRefs = [];
|
||||
line.steps.forEach(s => {
|
||||
const item = {
|
||||
_key: ++keyCounter,
|
||||
user_id: s.user_id,
|
||||
user_name: s.user_name || '사용자 ' + s.user_id,
|
||||
department: s.department || '',
|
||||
position: s.position || '',
|
||||
step_type: s.step_type || 'approval',
|
||||
};
|
||||
if (s.step_type === 'reference') {
|
||||
lineRefs.push(item);
|
||||
} else {
|
||||
lineSteps.push(item);
|
||||
}
|
||||
});
|
||||
this.steps = lineSteps;
|
||||
this.references = lineRefs;
|
||||
this.$nextTick(() => this.initSortable());
|
||||
}
|
||||
},
|
||||
@@ -319,10 +435,15 @@ function approvalLineEditor() {
|
||||
},
|
||||
|
||||
getStepsData() {
|
||||
return this.steps.map(s => ({
|
||||
const approvalSteps = this.steps.map(s => ({
|
||||
user_id: s.user_id,
|
||||
step_type: s.step_type,
|
||||
}));
|
||||
const refSteps = this.references.map(r => ({
|
||||
user_id: r.user_id,
|
||||
step_type: 'reference',
|
||||
}));
|
||||
return [...approvalSteps, ...refSteps];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,6 +70,27 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 참조자 --}}
|
||||
@php
|
||||
$refSteps = $approval->steps->where('step_type', 'reference')->values();
|
||||
@endphp
|
||||
@if($refSteps->count() > 0)
|
||||
<div class="mb-4 flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs font-semibold text-gray-500 flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
참조
|
||||
</span>
|
||||
@foreach($refSteps as $ref)
|
||||
<span class="inline-flex items-center px-2 py-0.5 bg-green-50 border border-green-200 rounded-full text-xs text-gray-700">
|
||||
{{ $ref->approver_name ?? ($ref->approver?->name ?? '-') }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 회수 사유 표시 --}}
|
||||
@if($approval->status === 'cancelled' && $approval->recall_reason)
|
||||
<div class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
|
||||
Reference in New Issue
Block a user