Files
sam-manage/resources/views/equipment/partials/tabs/inspection-items.blade.php
김보곤 8a6ee9f2fe feat: [equipment] 점검항목 다른 주기로 복사 기능 추가
- 서비스: copyTemplatesToCycles 메서드 추가 (중복 항목 스킵)
- 컨트롤러: copyTemplates API 엔드포인트 추가
- UI: 다른 주기에 복사 버튼 + 체크박스 모달
2026-02-28 14:17:18 +09:00

280 lines
15 KiB
PHP

<!-- 점검항목 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<div class="flex justify-between items-center mb-4 pb-2 border-b">
<h2 class="text-lg font-semibold text-gray-800">점검항목 템플릿</h2>
<div class="flex gap-2">
<button onclick="openCopyModal()"
class="bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-lg text-sm transition"
id="copyTemplateBtn" style="display: none;">
다른 주기에 복사
</button>
<button onclick="document.getElementById('templateModal').classList.remove('hidden')"
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm transition">
+ 항목 추가
</button>
</div>
</div>
@php
$allCycles = \App\Enums\InspectionCycle::all();
$templatesByCycle = $equipment->inspectionTemplates->groupBy('inspection_cycle');
@endphp
@if($equipment->inspectionTemplates->isNotEmpty())
<!-- 주기별 -->
<div class="flex gap-1 mb-4 bg-gray-50 rounded-lg p-1" style="width: fit-content;">
@foreach($allCycles as $code => $label)
@php $count = isset($templatesByCycle[$code]) ? $templatesByCycle[$code]->count() : 0; @endphp
<button type="button"
class="tmpl-cycle-tab px-3 py-1.5 rounded-md text-xs font-medium transition {{ $loop->first ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700' }}"
data-cycle="{{ $code }}"
onclick="switchTemplateTab('{{ $code }}')">
{{ $label }}
@if($count > 0)
<span class="ml-1 inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold bg-blue-100 text-blue-700 rounded-full">{{ $count }}</span>
@endif
</button>
@endforeach
</div>
<!-- 주기별 테이블 -->
@foreach($allCycles as $code => $label)
<div class="tmpl-cycle-content {{ $loop->first ? '' : 'hidden' }}" data-cycle="{{ $code }}">
@if(isset($templatesByCycle[$code]) && $templatesByCycle[$code]->isNotEmpty())
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">번호</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">점검개소</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">점검항목</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">시기</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">주기</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">점검방법</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">액션</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($templatesByCycle[$code] as $tmpl)
<tr>
<td class="px-3 py-2 text-sm text-center">{{ $tmpl->item_no }}</td>
<td class="px-3 py-2 text-sm">{{ $tmpl->check_point }}</td>
<td class="px-3 py-2 text-sm">{{ $tmpl->check_item }}</td>
<td class="px-3 py-2 text-sm text-center">{{ $tmpl->timing_label }}</td>
<td class="px-3 py-2 text-sm text-center">{{ $tmpl->check_frequency ?? '-' }}</td>
<td class="px-3 py-2 text-sm text-gray-600">{{ $tmpl->check_method ?? '-' }}</td>
<td class="px-3 py-2 text-sm text-center">
<button onclick="deleteTemplate({{ $tmpl->id }})" class="text-red-600 hover:text-red-900">삭제</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-gray-400 text-center py-4 text-sm">{{ $label }} 점검항목이 없습니다.</p>
@endif
</div>
@endforeach
@else
<p class="text-gray-500 text-center py-8">등록된 점검항목이 없습니다.</p>
@endif
</div>
<!-- 점검항목 추가 모달 -->
<div id="templateModal" class="fixed inset-0 z-50 hidden">
<div class="fixed inset-0 bg-black/50" onclick="document.getElementById('templateModal').classList.add('hidden')"></div>
<div class="fixed inset-0 flex items-center justify-center p-4">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg">
<div class="flex justify-between items-center p-6 border-b">
<h3 class="text-lg font-semibold">점검항목 추가</h3>
<button onclick="document.getElementById('templateModal').classList.add('hidden')" class="text-gray-400 hover:text-gray-600">&times;</button>
</div>
<form id="templateForm" class="p-6 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">점검주기 <span class="text-red-500">*</span></label>
<select name="inspection_cycle" id="tmplInspectionCycle"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
@foreach($allCycles as $code => $label)
<option value="{{ $code }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">항목번호 <span class="text-red-500">*</span></label>
<input type="number" name="item_no" required min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">점검개소 <span class="text-red-500">*</span></label>
<input type="text" name="check_point" required placeholder="겉모양"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">점검항목 <span class="text-red-500">*</span></label>
<input type="text" name="check_item" required placeholder="청결상태"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">시기</label>
<select name="check_timing"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">선택</option>
<option value="operating">가동 </option>
<option value="stopped">정지 </option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">주기</label>
<input type="text" name="check_frequency" placeholder="1회/일"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">점검방법</label>
<textarea name="check_method" rows="2" placeholder="육안 확인"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="document.getElementById('templateModal').classList.add('hidden')"
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg">취소</button>
<button type="button" onclick="addTemplate()"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">추가</button>
</div>
</form>
</div>
</div>
</div>
<!-- 다른 주기에 복사 모달 -->
<div id="copyTemplateModal" class="fixed inset-0 z-50 hidden">
<div class="fixed inset-0 bg-black/50" onclick="document.getElementById('copyTemplateModal').classList.add('hidden')"></div>
<div class="fixed inset-0 flex items-center justify-center p-4">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md">
<div class="flex justify-between items-center p-6 border-b">
<h3 class="text-lg font-semibold">점검항목 복사</h3>
<button onclick="document.getElementById('copyTemplateModal').classList.add('hidden')" class="text-gray-400 hover:text-gray-600">&times;</button>
</div>
<div class="p-6">
<p class="text-sm text-gray-600 mb-4">
<span id="copySourceLabel" class="font-semibold text-blue-700"></span> 점검항목을 선택한 주기에 복사합니다.
</p>
<div class="space-y-2 mb-6" id="copyTargetCycles">
@foreach($allCycles as $code => $label)
<label class="copy-cycle-option flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer" data-cycle="{{ $code }}">
<input type="checkbox" name="target_cycles[]" value="{{ $code }}"
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $label }}</span>
<span class="copy-cycle-count text-xs text-gray-400 ml-auto"></span>
</label>
@endforeach
</div>
<div class="flex justify-end gap-3">
<button type="button" onclick="document.getElementById('copyTemplateModal').classList.add('hidden')"
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg text-sm">취소</button>
<button type="button" onclick="executeCopyTemplates()"
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg text-sm" id="copyExecuteBtn">복사</button>
</div>
</div>
</div>
</div>
</div>
<script>
let currentTemplateTab = 'daily';
function switchTemplateTab(cycle) {
currentTemplateTab = cycle;
document.querySelectorAll('.tmpl-cycle-tab').forEach(btn => {
if (btn.dataset.cycle === cycle) {
btn.className = 'tmpl-cycle-tab px-3 py-1.5 rounded-md text-xs font-medium transition bg-white text-blue-700 shadow-sm';
} else {
btn.className = 'tmpl-cycle-tab px-3 py-1.5 rounded-md text-xs font-medium transition text-gray-500 hover:text-gray-700';
}
});
document.querySelectorAll('.tmpl-cycle-content').forEach(el => {
el.classList.toggle('hidden', el.dataset.cycle !== cycle);
});
const cycleSelect = document.getElementById('tmplInspectionCycle');
if (cycleSelect) cycleSelect.value = cycle;
updateCopyButton();
}
function updateCopyButton() {
const btn = document.getElementById('copyTemplateBtn');
const content = document.querySelector(`.tmpl-cycle-content[data-cycle="${currentTemplateTab}"]`);
const hasItems = content && content.querySelector('table');
btn.style.display = hasItems ? '' : 'none';
}
function openCopyModal() {
const cycleLabels = @json($allCycles);
document.getElementById('copySourceLabel').textContent = cycleLabels[currentTemplateTab];
document.querySelectorAll('.copy-cycle-option').forEach(el => {
const code = el.dataset.cycle;
const checkbox = el.querySelector('input[type="checkbox"]');
if (code === currentTemplateTab) {
el.style.display = 'none';
checkbox.checked = false;
} else {
el.style.display = '';
checkbox.checked = false;
const countEl = el.querySelector('.copy-cycle-count');
const tab = document.querySelector(`.tmpl-cycle-tab[data-cycle="${code}"]`);
const badge = tab ? tab.querySelector('span') : null;
countEl.textContent = badge ? `${badge.textContent.trim()}개 항목` : '';
}
});
document.getElementById('copyTemplateModal').classList.remove('hidden');
}
function executeCopyTemplates() {
const checked = document.querySelectorAll('#copyTargetCycles input[type="checkbox"]:checked');
if (checked.length === 0) {
alert('복사할 주기를 선택해주세요.');
return;
}
const targetCycles = Array.from(checked).map(cb => cb.value);
const equipmentId = @json($equipment->id);
const btn = document.getElementById('copyExecuteBtn');
btn.disabled = true;
btn.textContent = '복사 중...';
fetch(`/api/admin/equipment/${equipmentId}/templates/copy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({
source_cycle: currentTemplateTab,
target_cycles: targetCycles,
}),
})
.then(res => res.json())
.then(data => {
if (data.success) {
alert(data.message);
location.reload();
} else {
alert(data.message || '복사 실패');
}
})
.catch(() => alert('오류가 발생했습니다.'))
.finally(() => {
btn.disabled = false;
btn.textContent = '복사';
});
}
document.addEventListener('DOMContentLoaded', () => updateCopyButton());
</script>