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

@@ -1,6 +1,7 @@
/* tkpurchase-partner-portal.js - Partner portal logic (2-step flow) */
let portalSchedules = [];
let portalRequests = [];
let portalCheckins = {};
let partnerCompanyId = null;
let companyWorkersCache = null;
@@ -8,10 +9,13 @@ let companyWorkersCache = null;
async function loadMySchedules() {
try {
const r = await api('/schedules/my');
portalSchedules = r.data || [];
const data = r.data || {};
portalSchedules = data.schedules || [];
portalRequests = data.requests || [];
} catch(e) {
console.warn('Load schedules error:', e);
portalSchedules = [];
portalRequests = [];
}
}
@@ -46,15 +50,59 @@ async function renderScheduleCards() {
await Promise.all([loadMySchedules(), loadMyCheckins()]);
const container = document.getElementById('scheduleCards');
const noMsg = document.getElementById('noScheduleMessage');
const requestCardsEl = document.getElementById('requestCards');
const workRequestFormEl = document.getElementById('workRequestForm');
if (!portalSchedules.length) {
container.innerHTML = '';
noMsg.classList.remove('hidden');
// 신청 건 표시
if (portalRequests.length) {
requestCardsEl.classList.remove('hidden');
requestCardsEl.innerHTML = portalRequests.map(r => {
const isRejected = r.status === 'rejected';
const statusBg = isRejected ? 'bg-red-50 border-red-200' : 'bg-amber-50 border-amber-200';
const statusIcon = isRejected ? 'fa-times-circle text-red-400' : 'fa-clock text-amber-400';
const statusText = isRejected ? '반려됨' : '승인 대기 중';
const statusTextClass = isRejected ? 'text-red-600' : 'text-amber-600';
return `<div class="bg-white rounded-xl shadow-sm overflow-hidden border ${isRejected ? 'border-red-100' : 'border-amber-100'}">
<div class="p-5">
<div class="flex items-center justify-between mb-2">
<h3 class="text-base font-semibold text-gray-800">${escapeHtml(r.workplace_name || '작업장 미지정')}</h3>
<span class="text-xs ${statusTextClass} font-medium px-2 py-1 rounded-full ${statusBg}">
<i class="fas ${statusIcon} mr-1"></i>${statusText}
</span>
</div>
<div class="text-sm text-gray-600 mb-2">${escapeHtml(r.work_description || '')}</div>
<div class="flex gap-4 text-xs text-gray-500">
<span><i class="fas fa-calendar mr-1"></i>${formatDate(r.start_date)}</span>
<span><i class="fas fa-users mr-1"></i>예상 ${r.expected_workers || 0}명</span>
</div>
</div>
</div>`;
}).join('');
// 반려 건만 있으면 재신청 폼도 표시
const hasOnlyRejected = portalRequests.every(r => r.status === 'rejected');
if (hasOnlyRejected) {
workRequestFormEl.classList.remove('hidden');
workRequestFormEl.querySelector('p').textContent = '반려된 신청 건이 있습니다. 필요시 재신청해주세요.';
} else {
workRequestFormEl.classList.add('hidden');
}
} else {
requestCardsEl.classList.add('hidden');
workRequestFormEl.classList.remove('hidden');
workRequestFormEl.querySelector('p').textContent = '오늘 예정된 작업 일정이 없습니다. 작업이 필요하시면 아래에서 신청해주세요.';
}
// 기본 날짜 설정
const today = new Date().toISOString().substring(0, 10);
const reqDate = document.getElementById('reqStartDate');
if (reqDate && !reqDate.value) reqDate.value = today;
return;
}
noMsg.classList.add('hidden');
// 오늘 일정 있으면 기존 카드 렌더
requestCardsEl.classList.add('hidden');
workRequestFormEl.classList.add('hidden');
container.innerHTML = portalSchedules.map(s => {
const checkin = portalCheckins[s.id];
@@ -303,6 +351,32 @@ async function doCheckIn(scheduleId) {
}
}
async function doWorkRequest() {
const startDate = document.getElementById('reqStartDate').value;
if (!startDate) { showToast('작업일을 선택하세요', 'error'); return; }
const workDescription = document.getElementById('reqWorkDescription').value.trim();
if (!workDescription) { showToast('작업 내용을 입력하세요', 'error'); return; }
const body = {
start_date: startDate,
expected_workers: parseInt(document.getElementById('reqExpectedWorkers').value) || 1,
work_description: workDescription,
workplace_name: document.getElementById('reqWorkplaceName').value.trim() || null
};
try {
await api('/schedules/request', { method: 'POST', body: JSON.stringify(body) });
showToast('작업 신청이 완료되었습니다');
// 폼 초기화
document.getElementById('reqWorkDescription').value = '';
document.getElementById('reqWorkplaceName').value = '';
document.getElementById('reqExpectedWorkers').value = '1';
renderScheduleCards();
} catch(e) {
showToast(e.message || '작업 신청 실패', 'error');
}
}
function initPartnerPortal() {
if (!initAuth()) return;