- 영업관리 하위에 "개발 승인" 메뉴 추가 - 영업/매니저 100% 완료 고객의 개발 진행 상태 관리 - 3분할 레이아웃: 승인대기 / 개발진행중 / 완료 - 7단계 진행 상태: 대기→검토→기획안작성→개발코드작성→개발테스트→개발완료→통합테스트→인계 - 승인/반려/상태변경 기능 구현 - 통계 카드 및 상세 모달 지원 Co-Authored-By: Claude <noreply@anthropic.com>
248 lines
9.4 KiB
PHP
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
|