- partner_schedules: work_date → start_date/end_date 기간 기반으로 변경 - project_id 컬럼 추가 (projects 테이블 연결, 선택사항) - 프로젝트 조회 API 추가 (GET /projects/active) - 일정 조회 시 기간 겹침 조건으로 필터링 - 체크인 시 기간 내 검증 추가 - 프론트엔드: 시작일/종료일 입력 + 프로젝트 선택 드롭다운 - 마이그레이션 SQL 포함 (scripts/migration-schedule-daterange.sql) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
12 KiB
JavaScript
242 lines
12 KiB
JavaScript
/* tkpurchase-partner-portal.js - Partner portal logic */
|
|
|
|
let portalSchedules = [];
|
|
let portalCheckins = {};
|
|
let partnerCompanyId = null;
|
|
|
|
async function loadMySchedules() {
|
|
try {
|
|
const r = await api('/schedules/my');
|
|
portalSchedules = r.data || [];
|
|
} catch(e) {
|
|
console.warn('Load schedules error:', e);
|
|
portalSchedules = [];
|
|
}
|
|
}
|
|
|
|
async function loadMyCheckins() {
|
|
try {
|
|
const r = await api('/checkins/my');
|
|
const list = r.data || [];
|
|
portalCheckins = {};
|
|
list.forEach(c => {
|
|
if (c.schedule_id) portalCheckins[c.schedule_id] = c;
|
|
});
|
|
} catch(e) {
|
|
console.warn('Load checkins error:', e);
|
|
portalCheckins = {};
|
|
}
|
|
}
|
|
|
|
async function renderScheduleCards() {
|
|
await Promise.all([loadMySchedules(), loadMyCheckins()]);
|
|
|
|
const container = document.getElementById('scheduleCards');
|
|
const noMsg = document.getElementById('noScheduleMessage');
|
|
|
|
if (!portalSchedules.length) {
|
|
container.innerHTML = '';
|
|
noMsg.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
noMsg.classList.add('hidden');
|
|
|
|
container.innerHTML = portalSchedules.map(s => {
|
|
const checkin = portalCheckins[s.id];
|
|
const isCheckedIn = checkin && !checkin.check_out_time;
|
|
const isCheckedOut = checkin && checkin.check_out_time;
|
|
const hasReport = checkin && checkin.has_work_report;
|
|
|
|
// Step indicators
|
|
const step1Class = checkin ? 'text-emerald-600' : 'text-gray-400';
|
|
const step2Class = isCheckedIn || isCheckedOut ? 'text-emerald-600' : 'text-gray-400';
|
|
const step3Class = isCheckedOut ? 'text-emerald-600' : 'text-gray-400';
|
|
|
|
return `<div class="bg-white rounded-xl shadow-sm overflow-hidden">
|
|
<!-- 일정 정보 -->
|
|
<div class="p-5 border-b">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h3 class="text-base font-semibold text-gray-800">${escapeHtml(s.workplace_name || '작업장 미지정')}</h3>
|
|
<span class="text-xs text-gray-500">${formatDate(s.start_date) === formatDate(s.end_date) ? formatDate(s.start_date) : formatDate(s.start_date) + ' ~ ' + formatDate(s.end_date)}</span>
|
|
</div>
|
|
${s.work_description ? `<p class="text-sm text-gray-600 mb-2">${escapeHtml(s.work_description)}</p>` : ''}
|
|
<div class="flex gap-4 text-xs text-gray-500">
|
|
<span><i class="fas fa-users mr-1"></i>예상 ${s.expected_workers || 0}명</span>
|
|
${s.notes ? `<span><i class="fas fa-sticky-note mr-1"></i>${escapeHtml(s.notes)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3-step 진행 표시 -->
|
|
<div class="px-5 py-3 bg-gray-50 border-b">
|
|
<div class="flex items-center justify-between text-xs">
|
|
<div class="flex items-center gap-1 ${step1Class}">
|
|
<i class="fas ${checkin ? 'fa-check-circle' : 'fa-circle'}"></i>
|
|
<span>1. 작업 시작</span>
|
|
</div>
|
|
<div class="flex-1 border-t border-gray-300 mx-2"></div>
|
|
<div class="flex items-center gap-1 ${step2Class}">
|
|
<i class="fas ${(isCheckedIn || isCheckedOut) ? 'fa-check-circle' : 'fa-circle'}"></i>
|
|
<span>2. 업무현황</span>
|
|
</div>
|
|
<div class="flex-1 border-t border-gray-300 mx-2"></div>
|
|
<div class="flex items-center gap-1 ${step3Class}">
|
|
<i class="fas ${isCheckedOut ? 'fa-check-circle' : 'fa-circle'}"></i>
|
|
<span>3. 작업 종료</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 1: 작업 시작 (체크인) -->
|
|
<div class="p-5 ${checkin ? 'bg-gray-50' : ''}">
|
|
${!checkin ? `
|
|
<div id="checkinForm_${s.id}">
|
|
<h4 class="text-sm font-semibold text-gray-700 mb-3"><i class="fas fa-play-circle text-emerald-500 mr-1"></i>작업 시작</h4>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">실투입 인원 <span class="text-red-400">*</span></label>
|
|
<input type="number" id="checkinWorkers_${s.id}" min="1" value="${s.expected_workers || 1}" class="input-field w-full px-3 py-2 rounded-lg text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">작업자 명단</label>
|
|
<input type="text" id="checkinNames_${s.id}" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="홍길동, 김철수">
|
|
</div>
|
|
</div>
|
|
<button onclick="doCheckIn(${s.id})" class="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm hover:bg-emerald-700">
|
|
<i class="fas fa-play mr-1"></i>작업 시작
|
|
</button>
|
|
</div>
|
|
` : `
|
|
<div class="text-sm text-emerald-600 mb-1">
|
|
<i class="fas fa-check-circle mr-1"></i>체크인 완료 (${formatTime(checkin.check_in_time)})
|
|
· ${checkin.actual_worker_count || 0}명
|
|
</div>
|
|
`}
|
|
</div>
|
|
|
|
<!-- Step 2: 업무현황 입력 (체크인 후 표시) -->
|
|
${isCheckedIn ? `
|
|
<div class="p-5 border-t">
|
|
<h4 class="text-sm font-semibold text-gray-700 mb-3"><i class="fas fa-clipboard-list text-blue-500 mr-1"></i>업무현황 입력</h4>
|
|
<div class="space-y-3">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">실투입 인원</label>
|
|
<input type="number" id="reportWorkers_${checkin.id}" min="0" value="${checkin.actual_worker_count || 0}" class="input-field w-full px-3 py-2 rounded-lg text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">진행률 (%)</label>
|
|
<input type="number" id="reportProgress_${checkin.id}" min="0" max="100" value="0" class="input-field w-full px-3 py-2 rounded-lg text-sm">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">작업내용 <span class="text-red-400">*</span></label>
|
|
<textarea id="reportContent_${checkin.id}" rows="3" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="오늘 수행한 작업 내용"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">이슈사항</label>
|
|
<textarea id="reportIssues_${checkin.id}" rows="2" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="문제점이나 특이사항"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">향후 계획</label>
|
|
<textarea id="reportNextPlan_${checkin.id}" rows="2" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="다음 작업 계획"></textarea>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="submitWorkReport(${checkin.id}, ${s.id})" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
|
|
<i class="fas fa-save mr-1"></i>업무현황 저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: 작업 종료 -->
|
|
<div class="p-5 border-t">
|
|
<button onclick="doCheckOut(${checkin.id})" class="w-full px-4 py-3 bg-gray-800 text-white rounded-lg text-sm hover:bg-gray-900 font-medium">
|
|
<i class="fas fa-stop-circle mr-1"></i>작업 종료 (체크아웃)
|
|
</button>
|
|
<p class="text-xs text-gray-400 text-center mt-2">업무현황을 먼저 저장한 후 작업을 종료하세요.</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
${isCheckedOut ? `
|
|
<div class="p-5 border-t bg-gray-50">
|
|
<div class="text-sm text-blue-600">
|
|
<i class="fas fa-check-double mr-1"></i>작업 종료 완료 (${formatTime(checkin.check_out_time)})
|
|
</div>
|
|
${hasReport ? '<div class="text-xs text-emerald-600 mt-1"><i class="fas fa-clipboard-check mr-1"></i>업무현황 제출 완료</div>' : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function doCheckIn(scheduleId) {
|
|
const workerCount = parseInt(document.getElementById('checkinWorkers_' + scheduleId).value) || 1;
|
|
const workerNames = document.getElementById('checkinNames_' + scheduleId).value.trim();
|
|
|
|
const body = {
|
|
schedule_id: scheduleId,
|
|
actual_worker_count: workerCount,
|
|
worker_names: workerNames || null
|
|
};
|
|
|
|
try {
|
|
await api('/checkins', { method: 'POST', body: JSON.stringify(body) });
|
|
showToast('체크인 완료');
|
|
renderScheduleCards();
|
|
} catch(e) {
|
|
showToast(e.message || '체크인 실패', 'error');
|
|
}
|
|
}
|
|
|
|
async function submitWorkReport(checkinId, scheduleId) {
|
|
const workContent = document.getElementById('reportContent_' + checkinId).value.trim();
|
|
if (!workContent) { showToast('작업내용을 입력하세요', 'error'); return; }
|
|
|
|
const body = {
|
|
checkin_id: checkinId,
|
|
schedule_id: scheduleId,
|
|
actual_workers: parseInt(document.getElementById('reportWorkers_' + checkinId).value) || 0,
|
|
work_content: workContent,
|
|
progress_rate: parseInt(document.getElementById('reportProgress_' + checkinId).value) || 0,
|
|
issues: document.getElementById('reportIssues_' + checkinId).value.trim() || null,
|
|
next_plan: document.getElementById('reportNextPlan_' + checkinId).value.trim() || null
|
|
};
|
|
|
|
try {
|
|
await api('/work-reports', { method: 'POST', body: JSON.stringify(body) });
|
|
showToast('업무현황이 저장되었습니다');
|
|
renderScheduleCards();
|
|
} catch(e) {
|
|
showToast(e.message || '저장 실패', 'error');
|
|
}
|
|
}
|
|
|
|
async function doCheckOut(checkinId) {
|
|
if (!confirm('작업을 종료하시겠습니까? 업무현황을 먼저 저장했는지 확인하세요.')) return;
|
|
try {
|
|
await api('/checkins/' + checkinId + '/checkout', { method: 'PUT' });
|
|
showToast('작업 종료 (체크아웃) 완료');
|
|
renderScheduleCards();
|
|
} catch(e) {
|
|
showToast(e.message || '체크아웃 실패', 'error');
|
|
}
|
|
}
|
|
|
|
function initPartnerPortal() {
|
|
if (!initAuth()) return;
|
|
|
|
// Check if partner account
|
|
const token = getToken();
|
|
const decoded = decodeToken(token);
|
|
if (!decoded || !decoded.partner_company_id) {
|
|
location.href = '/';
|
|
return;
|
|
}
|
|
|
|
partnerCompanyId = decoded.partner_company_id;
|
|
document.getElementById('welcomeCompanyName').textContent = decoded.partner_company_name || decoded.name || '협력업체';
|
|
|
|
renderScheduleCards();
|
|
}
|