feat(tkpurchase): 협력업체 작업 신청 기능 추가

협력업체 포탈에서 오늘 일정이 없을 때 직접 작업을 신청할 수 있는 기능.
구매팀이 승인하면 일정이 생성되고, 반려 시 재신청 가능.

- DB: status ENUM에 requested/rejected 추가, requested_by 컬럼 추가
- API: POST /schedules/request, PUT /:id/approve, PUT /:id/reject
- 포탈: 신청 폼 + 승인 대기/반려 상태 카드
- 관리자: 신청 배지 + 승인 모달 (프로젝트 배정, 작업장 보정)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 14:40:20 +09:00
parent 03119a0849
commit 0211889636
9 changed files with 352 additions and 17 deletions

View File

@@ -67,17 +67,20 @@ function renderScheduleTable(list, total) {
}
const statusMap = {
requested: ['badge-orange', '신청'],
scheduled: ['badge-amber', '예정'],
in_progress: ['badge-green', '진행중'],
completed: ['badge-blue', '완료'],
cancelled: ['badge-gray', '취소']
cancelled: ['badge-gray', '취소'],
rejected: ['badge-red', '반려']
};
tbody.innerHTML = list.map(s => {
const [cls, label] = statusMap[s.status] || ['badge-gray', s.status];
const canEdit = s.status === 'scheduled';
const isRequest = s.status === 'requested';
const projectLabel = s.project_name ? (s.job_no ? `[${s.job_no}] ${s.project_name}` : s.project_name) : '';
return `<tr>
return `<tr${isRequest ? ' class="bg-amber-50"' : ''}>
<td class="font-medium">${escapeHtml(s.company_name || '')}</td>
<td class="whitespace-nowrap">${formatDateRange(s.start_date, s.end_date)}</td>
<td class="text-xs text-gray-500">${escapeHtml(projectLabel)}</td>
@@ -86,6 +89,7 @@ function renderScheduleTable(list, total) {
<td class="text-center">${s.expected_workers || 0}명</td>
<td><span class="badge ${cls}">${label}</span></td>
<td class="text-right">
${isRequest ? `<button onclick="openApproveModal(${s.id})" class="text-amber-600 hover:text-amber-800 text-xs mr-1 font-medium" title="승인/반려"><i class="fas fa-check-circle mr-1"></i>처리</button>` : ''}
${(s.status === 'in_progress' || s.status === 'completed') ? `<button onclick="viewCheckins(${s.id})" class="text-emerald-600 hover:text-emerald-800 text-xs mr-1" title="체크인 현황"><i class="fas fa-clipboard-check"></i></button>` : ''}
${canEdit ? `<button onclick="openEditSchedule(${s.id})" class="text-blue-600 hover:text-blue-800 text-xs mr-1" title="수정"><i class="fas fa-edit"></i></button>
<button onclick="deleteSchedule(${s.id})" class="text-red-500 hover:text-red-700 text-xs" title="삭제"><i class="fas fa-trash"></i></button>` : ''}
@@ -368,6 +372,72 @@ async function editCheckinWorkers(checkinId) {
}
}
/* ===== Approve/Reject Request ===== */
async function openApproveModal(id) {
try {
const r = await api('/schedules/' + id);
const s = r.data || r;
scheduleCache[id] = s;
document.getElementById('approveScheduleId').value = id;
document.getElementById('approveInfo').innerHTML = `
<div><strong>업체:</strong> ${escapeHtml(s.company_name || '')}</div>
<div><strong>작업일:</strong> ${formatDate(s.start_date)}</div>
<div><strong>예상 인원:</strong> ${s.expected_workers || 0}명</div>
<div><strong>작업 내용:</strong> ${escapeHtml(s.work_description || '-')}</div>
<div><strong>작업장:</strong> ${escapeHtml(s.workplace_name || '-')}</div>
`;
document.getElementById('approveWorkplace').value = s.workplace_name || '';
document.getElementById('approveStartDate').value = formatDate(s.start_date);
document.getElementById('approveEndDate').value = formatDate(s.end_date);
await loadProjects();
populateProjectDropdown('approveProject', '');
document.getElementById('approveModal').classList.remove('hidden');
} catch(e) {
showToast('신청 정보를 불러올 수 없습니다', 'error');
}
}
function closeApproveModal() {
document.getElementById('approveModal').classList.add('hidden');
}
async function doApprove() {
const id = document.getElementById('approveScheduleId').value;
if (!confirm('이 작업 신청을 승인하시겠습니까?')) return;
const projectId = document.getElementById('approveProject').value;
const body = {
project_id: projectId ? parseInt(projectId) : null,
workplace_name: document.getElementById('approveWorkplace').value.trim(),
start_date: document.getElementById('approveStartDate').value,
end_date: document.getElementById('approveEndDate').value
};
try {
await api('/schedules/' + id + '/approve', { method: 'PUT', body: JSON.stringify(body) });
showToast('승인 완료');
closeApproveModal();
loadSchedules();
} catch(e) {
showToast(e.message || '승인 실패', 'error');
}
}
async function doReject() {
const id = document.getElementById('approveScheduleId').value;
if (!confirm('이 작업 신청을 반려하시겠습니까?')) return;
try {
await api('/schedules/' + id + '/reject', { method: 'PUT' });
showToast('반려 완료');
closeApproveModal();
loadSchedules();
} catch(e) {
showToast(e.message || '반려 실패', 'error');
}
}
/* ===== Init ===== */
function initSchedulePage() {
if (!initAuth()) return;