feat: 작업보고서 시스템 완성
- 일일 공수 입력 기능 - 부적합 사항 등록 (이미지 선택사항) - 날짜별 부적합 조회 (시간순 나열) - 목록 관리 (인라인 편집, 작업시간 확인 버튼) - 보고서 생성 (총 공수/부적합 시간 분리) - JWT 인증 및 권한 관리 - Docker 기반 배포 환경 구성
This commit is contained in:
671
index.html
Normal file
671
index.html
Normal file
@@ -0,0 +1,671 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>작업보고서 시스템</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--primary-dark: #2563eb;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: 1px solid var(--gray-300);
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-progress {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-complete {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #6b7280;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: var(--gray-100);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 로그인 화면 -->
|
||||
<div id="loginScreen" class="min-h-screen flex items-center justify-center p-4 bg-gray-50">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 w-full max-w-sm">
|
||||
<div class="text-center mb-6">
|
||||
<i class="fas fa-clipboard-check text-5xl text-blue-500 mb-4"></i>
|
||||
<h1 class="text-2xl font-bold text-gray-800">작업보고서 시스템</h1>
|
||||
<p class="text-gray-600 text-sm mt-2">부적합 사항 관리 및 공수 계산</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">아이디</label>
|
||||
<input
|
||||
type="text"
|
||||
id="userId"
|
||||
class="input-field w-full px-4 py-2 rounded-lg"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
class="input-field w-full px-4 py-2 rounded-lg"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full py-2 rounded-lg font-medium">
|
||||
로그인
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메인 화면 -->
|
||||
<div id="mainScreen" class="hidden min-h-screen bg-gray-50">
|
||||
<!-- 헤더 -->
|
||||
<header class="bg-white shadow-sm sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 py-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-xl font-bold text-gray-800">
|
||||
<i class="fas fa-clipboard-check text-blue-500 mr-2"></i>작업보고서
|
||||
</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-gray-600" id="userDisplay"></span>
|
||||
<button onclick="logout()" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 네비게이션 -->
|
||||
<nav class="bg-white border-b">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex gap-2 py-2 overflow-x-auto">
|
||||
<a href="daily-work.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check mr-2"></i>일일 공수
|
||||
</a>
|
||||
<button class="nav-link active" onclick="showSection('report')">
|
||||
<i class="fas fa-camera-retro mr-2"></i>부적합 등록
|
||||
</button>
|
||||
<button class="nav-link" onclick="showSection('list')">
|
||||
<i class="fas fa-list mr-2"></i>목록 관리
|
||||
</button>
|
||||
<button class="nav-link" onclick="showSection('summary')">
|
||||
<i class="fas fa-chart-bar mr-2"></i>보고서
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 부적합 등록 섹션 (모바일 최적화) -->
|
||||
<section id="reportSection" class="container mx-auto px-4 py-6 max-w-lg">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>부적합 사항 등록
|
||||
</h2>
|
||||
|
||||
<form id="reportForm" class="space-y-4">
|
||||
<!-- 사진 업로드 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">사진</label>
|
||||
<div
|
||||
id="photoUpload"
|
||||
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
|
||||
onclick="document.getElementById('photoInput').click()"
|
||||
>
|
||||
<div id="photoPreview" class="hidden mb-4">
|
||||
<img id="previewImg" class="max-h-48 mx-auto rounded-lg">
|
||||
</div>
|
||||
<div id="photoPlaceholder">
|
||||
<i class="fas fa-camera text-4xl text-gray-400 mb-2"></i>
|
||||
<p class="text-gray-600">사진 촬영 또는 선택</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="photoInput" accept="image/*" capture="camera" class="hidden">
|
||||
</div>
|
||||
|
||||
<!-- 카테고리 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">카테고리</label>
|
||||
<select id="category" class="input-field w-full px-4 py-2 rounded-lg" required>
|
||||
<option value="">선택하세요</option>
|
||||
<option value="material_missing">자재누락</option>
|
||||
<option value="dimension_defect">치수불량</option>
|
||||
<option value="incoming_defect">입고자재 불량</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 간단 설명 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">간단 설명</label>
|
||||
<textarea
|
||||
id="description"
|
||||
rows="3"
|
||||
class="input-field w-full px-4 py-2 rounded-lg resize-none"
|
||||
placeholder="발견된 문제를 간단히 설명하세요"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-medium text-lg">
|
||||
<i class="fas fa-check mr-2"></i>등록하기
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 목록 관리 섹션 -->
|
||||
<section id="listSection" class="hidden container mx-auto px-4 py-6">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">부적합 사항 목록</h2>
|
||||
<div id="issueList" class="space-y-3">
|
||||
<!-- 목록이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세보기 모달 -->
|
||||
<div id="detailModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" onclick="if(event.target === this) closeDetailModal()">
|
||||
<div class="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()">
|
||||
<div class="sticky top-0 bg-white border-b p-4 flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold text-gray-800">부적합 사항 상세</h3>
|
||||
<button onclick="closeDetailModal()" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- 사진 -->
|
||||
<div id="modalPhoto" class="mb-6"></div>
|
||||
|
||||
<!-- 수정 가능한 정보 -->
|
||||
<div class="mb-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">카테고리</label>
|
||||
<select id="modalCategory" class="input-field w-full px-4 py-2 rounded-lg">
|
||||
<option value="material_missing">자재누락</option>
|
||||
<option value="dimension_defect">치수불량</option>
|
||||
<option value="incoming_defect">입고자재 불량</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
||||
<textarea
|
||||
id="modalDescription"
|
||||
rows="3"
|
||||
class="input-field w-full px-4 py-2 rounded-lg resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500 pt-2">
|
||||
<span><i class="fas fa-user mr-1"></i><span id="modalReporter"></span></span>
|
||||
<span><i class="fas fa-calendar mr-1"></i><span id="modalDate"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업 시간 입력 -->
|
||||
<div class="border-t pt-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">해결하는데 걸린 총 시간</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
id="workHours"
|
||||
step="0.5"
|
||||
min="0"
|
||||
class="input-field flex-1 px-4 py-2 rounded-lg"
|
||||
placeholder="예: 2.5"
|
||||
>
|
||||
<span class="text-gray-600">시간</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick="saveIssueDetails()" class="btn-primary w-full py-2 rounded-lg font-medium">
|
||||
<i class="fas fa-save mr-2"></i>저장하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 보고서 섹션 -->
|
||||
<section id="summarySection" class="hidden container mx-auto px-4 py-6">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">작업 보고서</h2>
|
||||
<button onclick="printReport()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||
<i class="fas fa-print mr-2"></i>인쇄
|
||||
</button>
|
||||
</div>
|
||||
<div id="reportContent">
|
||||
<!-- 보고서 내용이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 사용자 데이터
|
||||
const users = {
|
||||
'inspector1': { password: 'pass123', name: '검사자1' },
|
||||
'inspector2': { password: 'pass456', name: '검사자2' },
|
||||
'admin': { password: 'admin123', name: '관리자' }
|
||||
};
|
||||
|
||||
let currentUser = null;
|
||||
let currentPhoto = null;
|
||||
let issues = [];
|
||||
|
||||
// 로그인
|
||||
document.getElementById('loginForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const userId = document.getElementById('userId').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (users[userId] && users[userId].password === password) {
|
||||
currentUser = { id: userId, ...users[userId] };
|
||||
document.getElementById('userDisplay').textContent = currentUser.name;
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainScreen').classList.remove('hidden');
|
||||
loadIssues();
|
||||
} else {
|
||||
alert('아이디 또는 비밀번호가 올바르지 않습니다.');
|
||||
}
|
||||
});
|
||||
|
||||
// 로그아웃
|
||||
function logout() {
|
||||
currentUser = null;
|
||||
document.getElementById('loginScreen').classList.remove('hidden');
|
||||
document.getElementById('mainScreen').classList.add('hidden');
|
||||
document.getElementById('loginForm').reset();
|
||||
}
|
||||
|
||||
// 섹션 전환
|
||||
function showSection(section) {
|
||||
// 모든 섹션 숨기기
|
||||
document.querySelectorAll('section').forEach(s => s.classList.add('hidden'));
|
||||
// 선택된 섹션 표시
|
||||
document.getElementById(section + 'Section').classList.remove('hidden');
|
||||
|
||||
// 네비게이션 활성화 상태 변경
|
||||
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
|
||||
event.target.closest('.nav-link').classList.add('active');
|
||||
|
||||
// 섹션별 초기화
|
||||
if (section === 'list') {
|
||||
displayIssueList();
|
||||
} else if (section === 'summary') {
|
||||
generateReport();
|
||||
}
|
||||
}
|
||||
|
||||
// 사진 업로드
|
||||
document.getElementById('photoInput').addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
currentPhoto = e.target.result;
|
||||
document.getElementById('previewImg').src = currentPhoto;
|
||||
document.getElementById('photoPreview').classList.remove('hidden');
|
||||
document.getElementById('photoPlaceholder').classList.add('hidden');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// 부적합 사항 등록
|
||||
document.getElementById('reportForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const issue = {
|
||||
id: Date.now(),
|
||||
photo: currentPhoto,
|
||||
category: document.getElementById('category').value,
|
||||
description: document.getElementById('description').value,
|
||||
status: 'new',
|
||||
reporter: currentUser.id,
|
||||
reporterName: currentUser.name,
|
||||
reportDate: new Date().toISOString(),
|
||||
workHours: 0,
|
||||
detailNotes: ''
|
||||
};
|
||||
|
||||
issues.push(issue);
|
||||
saveIssues();
|
||||
|
||||
// 성공 메시지
|
||||
alert('부적합 사항이 등록되었습니다.');
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('reportForm').reset();
|
||||
currentPhoto = null;
|
||||
document.getElementById('photoPreview').classList.add('hidden');
|
||||
document.getElementById('photoPlaceholder').classList.remove('hidden');
|
||||
});
|
||||
|
||||
// 데이터 저장/로드
|
||||
function saveIssues() {
|
||||
localStorage.setItem('work-report-issues', JSON.stringify(issues));
|
||||
}
|
||||
|
||||
function loadIssues() {
|
||||
const saved = localStorage.getItem('work-report-issues');
|
||||
if (saved) {
|
||||
issues = JSON.parse(saved);
|
||||
}
|
||||
}
|
||||
|
||||
// 목록 표시
|
||||
function displayIssueList() {
|
||||
const container = document.getElementById('issueList');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (issues.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 부적합 사항이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
issues.forEach(issue => {
|
||||
const categoryNames = {
|
||||
material_missing: '자재누락',
|
||||
dimension_defect: '치수불량',
|
||||
incoming_defect: '입고자재 불량'
|
||||
};
|
||||
|
||||
const statusBadges = {
|
||||
new: 'status-new',
|
||||
progress: 'status-progress',
|
||||
complete: 'status-complete'
|
||||
};
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer';
|
||||
div.innerHTML = `
|
||||
<div class="flex items-start gap-4">
|
||||
${issue.photo ? `<img src="${issue.photo}" class="w-20 h-20 object-cover rounded-lg">` : ''}
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="font-medium text-gray-800">${categoryNames[issue.category] || issue.category}</h3>
|
||||
<span class="status-badge ${statusBadges[issue.status]}">${
|
||||
issue.status === 'new' ? '신규' :
|
||||
issue.status === 'progress' ? '진행중' : '완료'
|
||||
}</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-2">${issue.description}</p>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<span class="text-gray-500">
|
||||
<i class="fas fa-calendar mr-1"></i>
|
||||
${new Date(issue.reportDate).toLocaleDateString()}
|
||||
</span>
|
||||
<span class="text-gray-500">
|
||||
<i class="fas fa-clock mr-1"></i>
|
||||
${issue.workHours}시간
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
div.onclick = () => showIssueDetail(issue.id);
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// 현재 편집 중인 이슈 ID
|
||||
let currentEditingIssueId = null;
|
||||
|
||||
// 부적합 사항 상세보기
|
||||
function showIssueDetail(issueId) {
|
||||
const issue = issues.find(i => i.id === issueId);
|
||||
if (!issue) return;
|
||||
|
||||
currentEditingIssueId = issueId;
|
||||
|
||||
const categoryNames = {
|
||||
material_missing: '자재누락',
|
||||
dimension_defect: '치수불량',
|
||||
incoming_defect: '입고자재 불량'
|
||||
};
|
||||
|
||||
// 사진 표시
|
||||
const photoContainer = document.getElementById('modalPhoto');
|
||||
if (issue.photo) {
|
||||
photoContainer.innerHTML = `<img src="${issue.photo}" class="w-full rounded-lg shadow-md">`;
|
||||
} else {
|
||||
photoContainer.innerHTML = '<div class="bg-gray-100 rounded-lg p-8 text-center text-gray-500">사진 없음</div>';
|
||||
}
|
||||
|
||||
// 정보 표시
|
||||
document.getElementById('modalCategory').value = issue.category;
|
||||
document.getElementById('modalDescription').value = issue.description;
|
||||
document.getElementById('modalReporter').textContent = issue.reporterName;
|
||||
document.getElementById('modalDate').textContent = new Date(issue.reportDate).toLocaleDateString();
|
||||
|
||||
// 작업 시간
|
||||
document.getElementById('workHours').value = issue.workHours || '';
|
||||
|
||||
// 모달 표시
|
||||
document.getElementById('detailModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 모달 닫기
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detailModal').classList.add('hidden');
|
||||
currentEditingIssueId = null;
|
||||
}
|
||||
|
||||
// 상세 정보 저장
|
||||
function saveIssueDetails() {
|
||||
if (!currentEditingIssueId) return;
|
||||
|
||||
const issue = issues.find(i => i.id === currentEditingIssueId);
|
||||
if (!issue) return;
|
||||
|
||||
// 값 가져오기
|
||||
const category = document.getElementById('modalCategory').value;
|
||||
const description = document.getElementById('modalDescription').value.trim();
|
||||
const workHours = parseFloat(document.getElementById('workHours').value) || 0;
|
||||
|
||||
// 유효성 검사
|
||||
if (!description) {
|
||||
alert('설명을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 업데이트
|
||||
issue.category = category;
|
||||
issue.description = description;
|
||||
issue.workHours = workHours;
|
||||
|
||||
// 작업 시간이 입력되었으면 상태를 자동으로 '완료'로 변경
|
||||
if (workHours > 0 && issue.status === 'new') {
|
||||
issue.status = 'complete';
|
||||
}
|
||||
|
||||
// 저장
|
||||
saveIssues();
|
||||
|
||||
// UI 업데이트
|
||||
displayIssueList();
|
||||
closeDetailModal();
|
||||
|
||||
// 성공 메시지
|
||||
alert('저장되었습니다!');
|
||||
}
|
||||
|
||||
// 보고서 생성
|
||||
function generateReport() {
|
||||
const container = document.getElementById('reportContent');
|
||||
|
||||
// 통계 계산
|
||||
const totalHours = issues.reduce((sum, issue) => sum + issue.workHours, 0);
|
||||
const categoryCount = {};
|
||||
|
||||
issues.forEach(issue => {
|
||||
categoryCount[issue.category] = (categoryCount[issue.category] || 0) + 1;
|
||||
});
|
||||
|
||||
// 날짜 범위
|
||||
const dates = issues.map(i => new Date(i.reportDate));
|
||||
const startDate = dates.length > 0 ? new Date(Math.min(...dates)) : new Date();
|
||||
const endDate = new Date();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<!-- 요약 페이지 -->
|
||||
<div class="border-b pb-6">
|
||||
<h3 class="text-2xl font-bold text-center mb-6">작업 보고서</h3>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-700 mb-3">작업 기간</h4>
|
||||
<p class="text-lg">${startDate.toLocaleDateString()} ~ ${endDate.toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-blue-700 mb-3">총 공수</h4>
|
||||
<p class="text-3xl font-bold text-blue-600">${totalHours}시간</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h4 class="font-semibold text-gray-700 mb-3">카테고리별 분석</h4>
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
<div class="bg-blue-50 rounded-lg p-4 text-center">
|
||||
<p class="text-2xl font-bold text-blue-600">${categoryCount.material_missing || 0}</p>
|
||||
<p class="text-sm text-gray-600">자재누락</p>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded-lg p-4 text-center">
|
||||
<p class="text-2xl font-bold text-orange-600">${categoryCount.dimension_defect || 0}</p>
|
||||
<p class="text-sm text-gray-600">치수불량</p>
|
||||
</div>
|
||||
<div class="bg-purple-50 rounded-lg p-4 text-center">
|
||||
<p class="text-2xl font-bold text-purple-600">${categoryCount.incoming_defect || 0}</p>
|
||||
<p class="text-sm text-gray-600">입고자재 불량</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 내역 -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-700 mb-4">부적합 사항 상세</h4>
|
||||
${issues.map(issue => {
|
||||
const categoryNames = {
|
||||
material_missing: '자재누락',
|
||||
dimension_defect: '치수불량',
|
||||
incoming_defect: '입고자재 불량'
|
||||
};
|
||||
return `
|
||||
<div class="border rounded-lg p-4 mb-4 break-inside-avoid">
|
||||
<div class="flex gap-4">
|
||||
${issue.photo ? `<img src="${issue.photo}" class="w-32 h-32 object-cover rounded-lg">` : ''}
|
||||
<div class="flex-1">
|
||||
<h5 class="font-semibold text-gray-800 mb-2">${categoryNames[issue.category] || issue.category}</h5>
|
||||
<p class="text-gray-600 mb-2">${issue.description}</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<p><span class="font-medium">상태:</span> ${
|
||||
issue.status === 'new' ? '신규' :
|
||||
issue.status === 'progress' ? '진행중' : '완료'
|
||||
}</p>
|
||||
<p><span class="font-medium">작업시간:</span> ${issue.workHours}시간</p>
|
||||
<p><span class="font-medium">보고자:</span> ${issue.reporterName}</p>
|
||||
<p><span class="font-medium">보고일:</span> ${new Date(issue.reportDate).toLocaleDateString()}</p>
|
||||
</div>
|
||||
${issue.detailNotes ? `
|
||||
<div class="mt-2 p-2 bg-gray-50 rounded">
|
||||
<p class="text-sm text-gray-600">${issue.detailNotes}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 인쇄
|
||||
function printReport() {
|
||||
window.print();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user