Files
sam-manage/resources/views/sales/development/approvals.blade.php
김보곤 f83d2a1333 feat:개발 승인 메뉴 구현
- 영업관리 하위에 "개발 승인" 메뉴 추가
- 영업/매니저 100% 완료 고객의 개발 진행 상태 관리
- 3분할 레이아웃: 승인대기 / 개발진행중 / 완료
- 7단계 진행 상태: 대기→검토→기획안작성→개발코드작성→개발테스트→개발완료→통합테스트→인계
- 승인/반려/상태변경 기능 구현
- 통계 카드 및 상세 모달 지원

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 20:15:03 +09:00

248 lines
9.4 KiB
PHP

@extends('layouts.app')
@section('title', '개발 승인')
@section('content')
<div class="flex flex-col h-full">
<!-- 페이지 헤더 -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4 flex-shrink-0">
<div>
<h1 class="text-2xl font-bold text-gray-800">개발 승인</h1>
<p class="text-sm text-gray-500 mt-1">영업 완료된 고객의 개발 진행 상태를 관리합니다</p>
</div>
</div>
<!-- 통계 카드 -->
<div class="grid grid-cols-4 gap-4 mb-4 flex-shrink-0">
<div class="bg-yellow-50 rounded-lg shadow-sm p-3">
<div class="text-xs text-yellow-600">승인 대기</div>
<div class="text-xl font-bold text-yellow-800">{{ number_format($stats['pending']) }}</div>
</div>
<div class="bg-purple-50 rounded-lg shadow-sm p-3">
<div class="text-xs text-purple-600">개발 진행중</div>
<div class="text-xl font-bold text-purple-800">{{ number_format($stats['in_progress']) }}</div>
</div>
<div class="bg-blue-50 rounded-lg shadow-sm p-3">
<div class="text-xs text-blue-600">오늘 완료</div>
<div class="text-xl font-bold text-blue-800">{{ number_format($stats['today_completed']) }}</div>
</div>
<div class="bg-emerald-50 rounded-lg shadow-sm p-3">
<div class="text-xs text-emerald-600"> 완료</div>
<div class="text-xl font-bold text-emerald-800">{{ number_format($stats['total_completed']) }}</div>
</div>
</div>
<!-- 검색 영역 -->
<div class="flex-shrink-0 mb-4">
<form method="GET" class="flex flex-wrap gap-2 sm:gap-4 items-center bg-white p-3 rounded-lg shadow-sm">
<div class="flex-1 min-w-0 w-full sm:w-auto">
<input type="text"
name="search"
value="{{ request('search') }}"
placeholder="업체명, 사업자번호로 검색..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
</div>
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition text-sm">
검색
</button>
</form>
</div>
<!-- 3분할 레이아웃 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 flex-1 min-h-0">
<!-- 좌측: 승인 대기 -->
@include('sales.development.partials.pending-list')
<!-- 중앙: 개발 진행 -->
@include('sales.development.partials.progress-list')
<!-- 우측: 완료 -->
@include('sales.development.partials.completed-list')
</div>
</div>
<!-- 반려 사유 모달 -->
@include('sales.development.partials.reject-modal')
<!-- 상세 모달 -->
<div id="detailModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeDetailModal()"></div>
<div class="flex min-h-full items-center justify-center p-4">
<div id="detailModalContent" class="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl">
<div class="p-6 text-center">
<svg class="w-8 h-8 animate-spin text-blue-600 mx-auto" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<p class="mt-2 text-gray-500">로딩 ...</p>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// 승인 처리
function approveItem(id, name) {
showConfirm(
`<strong>${name}</strong>의 개발을 승인하시겠습니까?<br><small class="text-gray-500">승인 후 '검토' 상태로 변경됩니다.</small>`,
() => {
fetch(`/sales/development/approvals/${id}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'HX-Request': 'true'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(data.message, 'success');
window.location.reload();
} else {
showToast(data.message || '승인 처리에 실패했습니다.', 'error');
}
})
.catch(error => {
showToast('서버 오류가 발생했습니다.', 'error');
console.error(error);
});
},
{ title: '개발 승인 확인', icon: 'question', confirmText: '승인' }
);
}
// 반려 모달 열기
function openRejectModal(id, name) {
document.getElementById('rejectItemId').value = id;
document.getElementById('rejectItemName').textContent = name;
document.getElementById('rejectionReason').value = '';
document.getElementById('rejectModal').classList.remove('hidden');
}
// 반려 모달 닫기
function closeRejectModal() {
document.getElementById('rejectModal').classList.add('hidden');
}
// 반려 제출
function submitReject(event) {
event.preventDefault();
const id = document.getElementById('rejectItemId').value;
const reason = document.getElementById('rejectionReason').value;
fetch(`/sales/development/approvals/${id}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'HX-Request': 'true'
},
body: JSON.stringify({ rejection_reason: reason })
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeRejectModal();
showToast(data.message, 'success');
window.location.reload();
} else {
showToast(data.message || '반려 처리에 실패했습니다.', 'error');
}
})
.catch(error => {
showToast('서버 오류가 발생했습니다.', 'error');
console.error(error);
});
}
// 상태 변경
function updateStatus(id, name) {
const select = document.getElementById(`status-select-${id}`);
const status = select.value;
const statusText = select.options[select.selectedIndex].text;
showConfirm(
`<strong>${name}</strong>의 상태를 '${statusText}'(으)로 변경하시겠습니까?`,
() => {
fetch(`/sales/development/approvals/${id}/status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'HX-Request': 'true'
},
body: JSON.stringify({ status: status })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(data.message, 'success');
window.location.reload();
} else {
showToast(data.message || '상태 변경에 실패했습니다.', 'error');
}
})
.catch(error => {
showToast('서버 오류가 발생했습니다.', 'error');
console.error(error);
});
},
{ title: '상태 변경 확인', icon: 'question', confirmText: '변경' }
);
}
// 상세 모달 열기
function openDetailModal(id) {
document.getElementById('detailModal').classList.remove('hidden');
document.getElementById('detailModalContent').innerHTML = `
<div class="p-6 text-center">
<svg class="w-8 h-8 animate-spin text-blue-600 mx-auto" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<p class="mt-2 text-gray-500">로딩 중...</p>
</div>
`;
fetch(`/sales/development/approvals/${id}/detail`, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'text/html'
}
})
.then(response => response.text())
.then(html => {
document.getElementById('detailModalContent').innerHTML = html;
})
.catch(error => {
document.getElementById('detailModalContent').innerHTML = `
<div class="p-6 text-center">
<p class="text-red-500">오류가 발생했습니다.</p>
<button onclick="closeDetailModal()" class="mt-4 px-4 py-2 bg-gray-600 text-white rounded-lg">닫기</button>
</div>
`;
});
}
// 상세 모달 닫기
function closeDetailModal() {
document.getElementById('detailModal').classList.add('hidden');
}
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeRejectModal();
closeDetailModal();
}
});
</script>
@endpush