Files
sam-manage/resources/views/documents/show.blade.php

1008 lines
66 KiB
PHP
Raw Normal View History

@extends('layouts.app')
@section('title', '문서 상세')
@section('content')
@php $docData = $document->getRelation('data') ?? $document->data()->get(); @endphp
<!-- 헤더 -->
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">문서 상세</h1>
<p class="text-sm text-gray-500 mt-1 hidden sm:block">{{ $document->document_no }} - {{ $document->title }}</p>
</div>
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
@if($document->canEdit())
<a href="{{ route('documents.edit', $document->id) }}"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
수정
</a>
@endif
@if($document->isPending())
<button onclick="approveDocument()"
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
승인
</button>
<button onclick="showRejectModal()"
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
반려
</button>
@endif
<a href="{{ route('documents.print', $document->id) }}"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
성적서
</a>
<a href="{{ route('documents.index') }}"
class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transition">
목록
</a>
</div>
</div>
{{-- 문서 정보 --}}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- 메인 컨텐츠 --}}
<div class="lg:col-span-2 space-y-6">
{{-- 기본 정보 --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">기본 정보</h2>
<dl class="grid grid-cols-2 gap-4">
<div>
<dt class="text-sm font-medium text-gray-500">문서번호</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->document_no }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">템플릿</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->template->name ?? '-' }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">제목</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->title }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">상태</dt>
<dd class="mt-1">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
@switch($document->status)
@case('DRAFT') bg-gray-100 text-gray-800 @break
@case('PENDING') bg-yellow-100 text-yellow-800 @break
@case('APPROVED') bg-green-100 text-green-800 @break
@case('REJECTED') bg-red-100 text-red-800 @break
@default bg-gray-100 text-gray-800
@endswitch
">
{{ $document->status_label }}
</span>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">작성자</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->creator->name ?? '-' }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">작성일</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->created_at?->format('Y-m-d H:i') ?? '-' }}</dd>
</div>
@if($document->updated_at && $document->updated_at->ne($document->created_at))
<div>
<dt class="text-sm font-medium text-gray-500">수정자</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->updater->name ?? '-' }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">수정일</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->updated_at?->format('Y-m-d H:i') ?? '-' }}</dd>
</div>
@endif
</dl>
</div>
{{-- HTML 스냅샷 우선 출력 (React에서 저장한 rendered_html) --}}
@if($document->rendered_html)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="document-snapshot-container">
{!! $document->rendered_html !!}
</div>
</div>
{{-- 블록 빌더 서식: 블록 렌더러로 조회 --}}
@elseif($document->template?->isBlockBuilder() && !empty($document->template->schema))
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ $document->template->title ?? $document->template->name }}</h2>
<div>{!! $blockHtml !!}</div>
</div>
@else
{{-- 기본 필드 데이터 (절곡 작업일지는 전용 partial에서 렌더링하므로 스킵) --}}
@if($document->template?->basicFields && $document->template->basicFields->count() > 0 && !(str_contains($document->template->name ?? '', '절곡') && str_contains($document->template->name ?? '', '작업일지')))
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ $document->template->title ?? '문서 정보' }}</h2>
<dl class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($document->template->basicFields as $field)
@php
$fieldKey = 'bf_' . $field->id;
$fieldData = $docData->where('field_key', $fieldKey)->first();
// 레거시 호환: bf_{label} 형식으로 저장된 데이터도 조회
if (!$fieldData) {
$fieldData = $docData->where('field_key', 'bf_' . $field->label)->first();
}
$value = $fieldData?->field_value ?? '-';
@endphp
<div class="{{ $field->field_type === 'textarea' ? 'col-span-2' : '' }}">
<dt class="text-sm font-medium text-gray-500">{{ $field->label }}</dt>
<dd class="mt-1 text-sm text-gray-900 {{ $field->field_type === 'textarea' ? 'whitespace-pre-wrap' : '' }}">{{ $value }}</dd>
</div>
@endforeach
</dl>
</div>
@endif
{{-- 절곡 작업일지 전용 렌더링 (React BendingWorkLogContent와 동일 구조) --}}
@if($document->linkable_type === 'work_order' && $document->template && str_contains($document->template->name ?? '', '절곡') && str_contains($document->template->name ?? '', '작업일지'))
@include('documents.partials.bending-worklog')
@elseif($document->linkable_type === 'work_order' && $workOrderItems->isNotEmpty() && (!$document->template?->sections || $document->template->sections->count() === 0))
{{-- 일반 작업일지: 작업내역 + 자재LOT + 통계 (섹션 없는 work_order 연결 문서) --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">작업 내역</h2>
{{-- 작업지시 기본정보 --}}
@if($workOrder)
<dl class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 text-sm">
<div>
<dt class="font-medium text-gray-500">작업지시번호</dt>
<dd class="mt-0.5 text-gray-900">{{ $workOrder->work_order_no ?? '-' }}</dd>
</div>
<div>
<dt class="font-medium text-gray-500">상태</dt>
<dd class="mt-0.5">
@php
$statusLabels = ['pending' => '대기', 'in_progress' => '진행중', 'completed' => '완료', 'shipped' => '출고'];
$statusColors = ['pending' => 'bg-gray-100 text-gray-700', 'in_progress' => 'bg-blue-100 text-blue-700', 'completed' => 'bg-green-100 text-green-700', 'shipped' => 'bg-purple-100 text-purple-700'];
@endphp
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$workOrder->status] ?? 'bg-gray-100 text-gray-700' }}">
{{ $statusLabels[$workOrder->status] ?? $workOrder->status }}
</span>
</dd>
</div>
@if($salesOrder)
<div>
<dt class="font-medium text-gray-500">발주처</dt>
<dd class="mt-0.5 text-gray-900">{{ $salesOrder->client_name ?? '-' }}</dd>
</div>
<div>
<dt class="font-medium text-gray-500">현장명</dt>
<dd class="mt-0.5 text-gray-900">{{ $salesOrder->site_name ?? $workOrder->project_name ?? '-' }}</dd>
</div>
@endif
</dl>
@endif
{{-- 품목 테이블: 템플릿 컬럼 기반 렌더링 --}}
@php
$templateColumns = $document->template->columns ?? collect();
$hasComplexCol = $templateColumns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels);
// 개소별 투입자재 LOT 매핑 (work_order_item_id → lot_no)
$itemLotMapData = $itemLotMap ?? collect();
// 재단 알고리즘 (React ScreenWorkLogContent calculateCutSize 동일)
$fabricConfigs = [
'실리카' => ['width' => 1220, 'threshold' => 940, 'ranges' => ['900' => [841,940], '800' => [641,840], '600' => [441,640], '400' => [341,440], '300' => [1,340]]],
'와이어' => ['width' => 1100, 'threshold' => 860, 'ranges' => ['900' => [761,860], '800' => [561,760], '600' => [361,560], '400' => [261,360], '300' => [1,260]]],
'화이바' => ['width' => 1100, 'threshold' => 860, 'ranges' => ['900' => [761,860], '800' => [561,760], '600' => [361,560], '400' => [261,360], '300' => [1,260]]],
];
$detectFabricType = function($name) {
if (str_contains($name, '와이어')) return '와이어';
if (str_contains($name, '화이바')) return '화이바';
return '실리카';
};
$calculateCutSize = function($fabricType, $height) use ($fabricConfigs) {
if (!$height || $height <= 0) return null;
$cfg = $fabricConfigs[$fabricType] ?? $fabricConfigs['실리카'];
$w = $cfg['width'];
$makeVertical = $height + 140 + floor($height / $w) * 40;
$firstCut = floor($makeVertical / $w);
$remaining = $makeVertical - ($firstCut * $w);
if ($remaining > $cfg['threshold']) {
$firstCut++;
$remaining = $makeVertical - ($firstCut * $w);
}
$sizes = [];
foreach ($cfg['ranges'] as $key => [$min, $max]) {
$sizes[$key] = ($remaining >= $min && $remaining <= $max) ? 1 : 0;
}
return ['firstCut' => $firstCut, 'remaining' => $remaining, 'sizes' => $sizes, 'baseWidth' => $w];
};
// 각 품목별 재단 계산
$itemCuts = $workOrderItems->map(function($item) use ($detectFabricType, $calculateCutSize) {
$opts = $item->options ?? [];
$ft = $detectFabricType($item->item_name ?? '');
return $calculateCutSize($ft, $opts['height'] ?? 0);
});
// 컬럼 label → 셀 값 매핑 함수
$getCellValue = function($col, $item, $index) use ($itemLotMapData, $itemCuts) {
$label = trim($col->label);
$opts = $item->options ?? [];
$cut = $itemCuts[$index] ?? null;
$lowerLabel = mb_strtolower($label);
if (str_contains($lowerLabel, 'no') && !str_contains($label, 'LOT')) return $index + 1;
if (str_contains($label, 'LOT')) return $itemLotMapData[$item->id] ?? '-';
if (str_contains($label, '제품명')) return $item->item_name ?? '-';
if (str_contains($label, '부호')) return $opts['code'] ?? '-';
if (str_contains($label, '나머지높이')) return $cut && $cut['remaining'] > 0 ? $cut['remaining'] : '';
return '-';
};
// complex 컬럼 sub_label별 값 매핑
$getSubCellValue = function($col, $subLabel, $subIndex, $item, $index) use ($itemCuts) {
$label = trim($col->label);
$opts = $item->options ?? [];
$cut = $itemCuts[$index] ?? null;
// 제작사이즈(mm): 가로, 세로
if (str_contains($label, '사이즈') || str_contains($label, '제작')) {
if (str_contains($subLabel, '가로')) return isset($opts['width']) ? number_format($opts['width']) : '-';
if (str_contains($subLabel, '세로')) return isset($opts['height']) ? number_format($opts['height']) : '-';
}
// 규격(매수): 기준폭, 900, 800, 600, 400, 300
if (str_contains($label, '규격') || str_contains($label, '매수')) {
if (str_contains($subLabel, '기준') || str_contains($subLabel, '폭')) {
return $cut && $cut['firstCut'] > 0 ? $cut['firstCut'] : '';
}
// 나머지 사이즈 키 (900, 800, 600, 400, 300)
$sizeKey = trim($subLabel);
if ($cut && isset($cut['sizes'][$sizeKey])) {
return $cut['sizes'][$sizeKey] > 0 ? $cut['sizes'][$sizeKey] : '';
}
}
return '';
};
@endphp
@if($templateColumns->isNotEmpty())
<div class="overflow-x-auto">
<table class="min-w-full border border-gray-300 text-sm">
<thead class="bg-gray-50">
{{-- 헤더 1 --}}
<tr>
@foreach($templateColumns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
<th colspan="{{ count($col->sub_labels) }}"
class="px-2 py-2 text-center text-xs font-medium text-gray-600 border border-gray-300"
style="min-width: {{ $col->width }}">
{{ $col->label }}
</th>
@else
<th rowspan="{{ $hasComplexCol ? 2 : 1 }}"
class="px-2 py-2 text-center text-xs font-medium text-gray-600 border border-gray-300"
style="min-width: {{ $col->width }}">
{{ $col->label }}
</th>
@endif
@endforeach
</tr>
{{-- 헤더 2 (sub_labels) --}}
@if($hasComplexCol)
<tr>
@foreach($templateColumns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
@foreach($col->sub_labels as $subLabel)
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 border border-gray-300 bg-gray-50">
{{ $subLabel }}
</th>
@endforeach
@endif
@endforeach
</tr>
@endif
</thead>
<tbody>
@foreach($workOrderItems as $index => $item)
<tr class="hover:bg-gray-50">
@foreach($templateColumns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
@foreach($col->sub_labels as $subIndex => $subLabel)
@php $subVal = $getSubCellValue($col, $subLabel, $subIndex, $item, $index); @endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm {{ (str_contains($col->label, '사이즈')) ? 'font-bold text-red-600' : '' }}">
{{ $subVal !== '' ? $subVal : '' }}
</td>
@endforeach
@else
@php $cellVal = $getCellValue($col, $item, $index); @endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm">
{{ $cellVal }}
</td>
@endif
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
{{-- 작업 통계 (document_data에서 stats_ 조회) --}}
@php
$statsData = $docData->filter(fn($d) => str_starts_with($d->field_key, 'stats_'));
$statsMap = $statsData->pluck('field_value', 'field_key')->toArray();
@endphp
@if(!empty($statsMap))
<div class="mt-4 grid grid-cols-2 md:grid-cols-5 gap-3">
@php
$statItems = [
'stats_order_qty' => ['label' => '총 수량', 'color' => 'bg-blue-50 text-blue-700'],
'stats_completed_qty' => ['label' => '완료', 'color' => 'bg-green-50 text-green-700'],
'stats_in_progress_qty' => ['label' => '진행중', 'color' => 'bg-yellow-50 text-yellow-700'],
'stats_waiting_qty' => ['label' => '대기', 'color' => 'bg-gray-50 text-gray-700'],
'stats_progress' => ['label' => '진행률', 'color' => 'bg-indigo-50 text-indigo-700'],
];
@endphp
@foreach($statItems as $key => $stat)
@if(isset($statsMap[$key]))
<div class="px-3 py-2 rounded-lg {{ $stat['color'] }}">
<div class="text-xs font-medium opacity-75">{{ $stat['label'] }}</div>
<div class="text-lg font-bold">{{ $statsMap[$key] }}{{ $key === 'stats_progress' ? '%' : '' }}</div>
</div>
@endif
@endforeach
</div>
@endif
</div>
{{-- 투입 자재 LOT --}}
@if($materialInputLots->isNotEmpty())
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">투입 자재 LOT</h2>
<div class="overflow-x-auto">
<table class="min-w-full border border-gray-300 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-center border border-gray-300 text-xs font-medium text-gray-600 w-10">No.</th>
<th class="px-3 py-2 text-center border border-gray-300 text-xs font-medium text-gray-600">LOT 번호</th>
<th class="px-3 py-2 text-center border border-gray-300 text-xs font-medium text-gray-600">자재코드</th>
<th class="px-3 py-2 text-center border border-gray-300 text-xs font-medium text-gray-600">자재명</th>
<th class="px-3 py-2 text-center border border-gray-300 text-xs font-medium text-gray-600 w-24">투입 수량</th>
<th class="px-3 py-2 text-center border border-gray-300 text-xs font-medium text-gray-600 w-20">투입 횟수</th>
<th class="px-3 py-2 text-center border border-gray-300 text-xs font-medium text-gray-600 w-36">최초 투입일</th>
</tr>
</thead>
<tbody>
@foreach($materialInputLots as $index => $lot)
<tr class="hover:bg-gray-50">
<td class="px-3 py-2 text-center border border-gray-300">{{ $index + 1 }}</td>
<td class="px-3 py-2 border border-gray-300 font-mono text-blue-600">{{ $lot->lot_no }}</td>
<td class="px-3 py-2 border border-gray-300">{{ $lot->item_code }}</td>
<td class="px-3 py-2 border border-gray-300">{{ $lot->item_name }}</td>
<td class="px-3 py-2 text-center border border-gray-300">{{ number_format($lot->total_qty, 1) }}</td>
<td class="px-3 py-2 text-center border border-gray-300">{{ $lot->input_count }}</td>
<td class="px-3 py-2 text-center border border-gray-300 text-gray-500">{{ substr($lot->first_input_at, 0, 16) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
{{-- 비고 (document_data에서 remarks 조회) --}}
@php
$remarksData = $docData->where('field_key', 'remarks')->first();
@endphp
@if($remarksData && $remarksData->field_value)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">비고</h2>
<p class="text-sm text-gray-900 whitespace-pre-wrap">{{ $remarksData->field_value }}</p>
</div>
@endif
@endif
{{-- 절곡 중간검사 DATA 전용 렌더링 (React BendingInspectionContent와 동일 구조) --}}
@if($document->template && str_contains($document->template->name ?? '', '절곡') && str_contains($document->template->name ?? '', '검사'))
@php
$docData = $document->getRelation('data') ?? $document->data()->get();
@endphp
{{-- 검사기준서 섹션 (이미지만 렌더링) --}}
@foreach(($document->template->sections ?? collect()) as $section)
@if($section->title !== '중간검사 DATA' && $section->image_path)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ $section->title }}</h2>
@php
$sectionImgUrl = preg_match('/^\d+\//', $section->image_path)
? rtrim(config('app.api_url', 'http://api.sam.kr'), '/') . '/storage/tenants/' . $section->image_path
: asset('storage/' . $section->image_path);
@endphp
<img src="{{ $sectionImgUrl }}" alt="{{ $section->title }}" class="max-w-full h-auto mb-4 rounded border">
</div>
@endif
@endforeach
{{-- 중간검사 DATA 테이블 (rowSpan + 간격 포인트) --}}
@include('documents.partials.bending-inspection-data', ['inspectionData' => $inspectionData ?? null])
@else
{{-- 섹션 데이터 (테이블) --}}
@if($document->template?->sections && $document->template->sections->count() > 0)
@foreach($document->template->sections as $section)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ $section->title }}</h2>
@if($section->image_path)
@php
$sectionImgUrl = preg_match('/^\d+\//', $section->image_path)
? rtrim(config('app.api_url', 'http://api.sam.kr'), '/') . '/storage/tenants/' . $section->image_path
: asset('storage/' . $section->image_path);
@endphp
<img src="{{ $sectionImgUrl }}" alt="{{ $section->title }}" class="max-w-full h-auto mb-4 rounded border">
@endif
{{-- 검사 데이터 테이블 (읽기 전용) --}}
{{-- 정규화 형식: section_id + column_id + row_index + field_key 기반 조회 --}}
@if($section->items->count() > 0 && $document->template->columns->count() > 0)
@php
// 데이터 조회 헬퍼: 정규화 형식 우선, 레거시 fallback
$getData = function($sectionId, $colId, $rowIdx, $fieldKey) use ($docData) {
// 1차: 정규화 형식 (section_id + column_id + row_index + field_key)
$record = $docData
->where('section_id', $sectionId)
->where('column_id', $colId)
->where('row_index', $rowIdx)
->where('field_key', $fieldKey)
->first();
return $record?->field_value ?? '';
};
// 레거시 데이터 조회 헬퍼 (수입검사 호환: {itemId}_n{n}, {itemId}_result 등)
$getLegacyData = function($itemId, $fieldKey) use ($docData) {
return $docData->where('field_key', "{$itemId}_{$fieldKey}")->first()?->field_value
?? $docData->where('field_key', $fieldKey)->first()?->field_value
?? '';
};
// 컬럼 → 섹션 아이템 매핑 (React normalizeLabel 로직 동기화)
$normalizeLabel = fn($label) => preg_replace('/[①②③④⑤⑥⑦⑧⑨⑩\s]/u', '', trim($label));
$allItems = $section->items;
$columnItemMap = [];
foreach ($document->template->columns as $col) {
$colKey = $normalizeLabel($col->label);
foreach ($allItems as $sItem) {
$itemKey = $normalizeLabel($sItem->item ?? $sItem->category ?? '');
if ($itemKey === $colKey) {
$columnItemMap[$col->id] = $sItem;
break;
}
}
}
// 기준치 해석: reference_attribute → work_order_item 치수
$resolveStandard = function($colId, $rowIndex) use ($columnItemMap, &$workOrderItems) {
$sItem = $columnItemMap[$colId] ?? null;
if (!$sItem) return '';
$woItem = $workOrderItems[$rowIndex] ?? null;
// 1. reference_attribute → work_order_item 치수
if ($woItem) {
$fv = $sItem->field_values;
$refAttr = is_array($fv) ? ($fv['reference_attribute'] ?? null) : null;
if ($refAttr) {
$dimKey = $refAttr === 'length' ? 'width' : $refAttr;
$dimVal = $woItem->options[$dimKey] ?? null;
if ($dimVal) return (string) $dimVal;
}
}
// 2. standard_criteria
$sc = $sItem->standard_criteria;
if ($sc) {
if (is_array($sc)) {
if (isset($sc['nominal'])) return (string) $sc['nominal'];
if (isset($sc['min'], $sc['max'])) return $sc['min'] . ' ~ ' . $sc['max'];
if (isset($sc['max'])) return '≤ ' . $sc['max'];
if (isset($sc['min'])) return '≥ ' . $sc['min'];
}
return (string) $sc;
}
// 3. standard 텍스트
return $sItem->standard ?? '';
};
@endphp
<div class="overflow-x-auto mt-4">
<table class="min-w-full border border-gray-300 text-sm">
{{-- 테이블 헤더 --}}
<thead class="bg-gray-50">
<tr>
@foreach($document->template->columns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
<th colspan="{{ count($col->sub_labels) }}"
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300"
style="min-width: {{ $col->width }}">
{{ $col->label }}
</th>
@else
<th rowspan="{{ $document->template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels) ? 2 : 1 }}"
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300"
style="min-width: {{ $col->width }}">
{{ $col->label }}
</th>
@endif
@endforeach
</tr>
{{-- 서브 라벨 --}}
@if($document->template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels))
<tr>
@foreach($document->template->columns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
@foreach($col->sub_labels as $subLabel)
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 border border-gray-300 bg-gray-50">
{{ $subLabel }}
</th>
@endforeach
@endif
@endforeach
</tr>
@endif
</thead>
<tbody>
@php
// 행 수 결정: workOrderItems 기반 (React effectiveWorkItems와 동일)
// fallback: document_data의 max row_index + 1
$rowCount = $workOrderItems->isNotEmpty()
? $workOrderItems->count()
: max(1, ($docData->where('section_id', $section->id)->max('row_index') ?? 0) + 1);
@endphp
@for($rowIndex = 0; $rowIndex < $rowCount; $rowIndex++)
<tr class="hover:bg-gray-50">
@foreach($document->template->columns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
{{-- complex: sub_label 유형별 분리 (React inputIdx 로직 동기화) --}}
@php
$inputIdx = 0;
$mappedItem = $columnItemMap[$col->id] ?? null;
$isOkng = $mappedItem?->measurement_type === 'checkbox';
@endphp
@foreach($col->sub_labels as $subIndex => $subLabel)
@php
$sl = strtolower($subLabel);
$isStandardSub = str_contains($sl, '도면') || str_contains($sl, '기준');
$isOkNgSub = str_contains($sl, 'ok') || str_contains($sl, 'ng');
@endphp
@if($isStandardSub)
{{-- 기준치: document_data work_order_item 치수 item standard --}}
@php
$standardVal = $getData($section->id, $col->id, $rowIndex, 'standard');
if (!$standardVal) $standardVal = $resolveStandard($col->id, $rowIndex);
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm text-gray-500">
{{ $standardVal ?: '-' }}
</td>
@elseif($isOkNgSub)
{{-- OK·NG sub_label --}}
@php
$n = $inputIdx + 1;
$okVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ok");
$ngVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ng");
$savedVal = $okVal === 'OK' ? 'OK' : ($ngVal === 'NG' ? 'NG' : '');
if (!$savedVal) {
$valFallback = $getData($section->id, $col->id, $rowIndex, 'value');
if (in_array(strtolower($valFallback), ['ok', 'pass', '적합', '합격'])) $savedVal = 'OK';
elseif (in_array(strtolower($valFallback), ['ng', 'fail', '부적합'])) $savedVal = 'NG';
}
$inputIdx++;
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm">
@if($savedVal)
@if(strtolower($savedVal) === 'ok')
<span class="inline-flex items-center justify-center gap-1 px-2 py-1 rounded-md text-sm font-bold bg-green-100 text-green-700 border border-green-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
OK
</span>
@else
<span class="inline-flex items-center justify-center gap-1 px-2 py-1 rounded-md text-sm font-bold bg-red-100 text-red-700 border border-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
NG
</span>
@endif
@else
-
@endif
</td>
@else
{{-- 측정값 sub_label --}}
@php
$n = $inputIdx + 1;
if ($isOkng) {
$okVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ok");
$ngVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ng");
$savedVal = $okVal === 'OK' ? 'OK' : ($ngVal === 'NG' ? 'NG' : '');
if (!$savedVal) {
$valFallback = $getData($section->id, $col->id, $rowIndex, 'value');
if (in_array(strtolower($valFallback), ['ok', 'pass', '적합', '합격'])) $savedVal = 'OK';
elseif (in_array(strtolower($valFallback), ['ng', 'fail', '부적합'])) $savedVal = 'NG';
}
} else {
$savedVal = $getData($section->id, $col->id, $rowIndex, "n{$n}");
if (!$savedVal) $savedVal = $getData($section->id, $col->id, $rowIndex, 'value');
}
$inputIdx++;
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm {{ !$isOkng ? 'font-mono' : '' }}">
@if($isOkng && $savedVal)
@if(strtolower($savedVal) === 'ok')
<span class="inline-flex items-center justify-center gap-1 px-2 py-1 rounded-md text-sm font-bold bg-green-100 text-green-700 border border-green-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
OK
</span>
@else
<span class="inline-flex items-center justify-center gap-1 px-2 py-1 rounded-md text-sm font-bold bg-red-100 text-red-700 border border-red-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
NG
</span>
@endif
@else
{{ $savedVal ?: '-' }}
@endif
</td>
@endif
@endforeach
@elseif($col->column_type === 'select')
{{-- select: 판정 --}}
@php
$savedVal = $getData($section->id, $col->id, $rowIndex, 'value');
if (!$savedVal) {
$rowJudgment = $docData->where('field_key', 'row_judgment')->where('row_index', $rowIndex)->first()?->field_value ?? '';
if (in_array(strtolower($rowJudgment), ['pass', 'ok', '적합'])) $savedVal = 'OK';
elseif (in_array(strtolower($rowJudgment), ['fail', 'ng', '부적합'])) $savedVal = 'NG';
}
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm">
@if($savedVal)
@php $isPass = in_array(strtolower($savedVal), ['ok', '적합', '합격', 'pass', '적']); @endphp
<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-sm font-bold border
{{ $isPass ? 'bg-green-100 text-green-700 border-green-300' : 'bg-red-100 text-red-700 border-red-300' }}">
@if($isPass)
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
@else
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
@endif
{{ $isPass ? '적합' : '부적합' }}
</span>
@else
-
@endif
</td>
@elseif($col->column_type === 'check')
{{-- check: 체크 결과 --}}
@php
$savedVal = $getData($section->id, $col->id, $rowIndex, 'value');
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm">
@if(in_array(strtolower($savedVal), ['ok', 'pass', '적합', '합격']))
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-green-100 border border-green-300 mx-auto">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
</span>
@elseif($savedVal)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-red-100 border border-red-300 mx-auto">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</span>
@else
-
@endif
</td>
@elseif($col->column_type === 'measurement')
{{-- measurement: 수치 데이터 --}}
@php
$savedVal = $getData($section->id, $col->id, $rowIndex, 'n1');
if (!$savedVal) $savedVal = $getData($section->id, $col->id, $rowIndex, 'value');
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm font-mono">{{ $savedVal ?: '-' }}</td>
@else
{{-- text: 일련번호 / 판정 / 기타 --}}
@php
$label = trim($col->label);
$isNoCol = (str_contains(strtolower($label), 'no') && strlen($label) <= 4)
|| in_array($label, ['일렬번호', '일련번호', '번호', '순번']);
$isJudgeCol = str_contains($label, '판정');
@endphp
@if($isNoCol)
<td class="px-2 py-2 border border-gray-300 text-sm text-center text-gray-700">{{ $rowIndex + 1 }}</td>
@elseif($isJudgeCol)
@php
$savedVal = $getData($section->id, $col->id, $rowIndex, 'value');
if (!$savedVal) {
$rowJudgment = $docData->where('field_key', 'row_judgment')->where('row_index', $rowIndex)->first()?->field_value ?? '';
if (in_array(strtolower($rowJudgment), ['pass', 'ok', '적합'])) $savedVal = 'OK';
elseif (in_array(strtolower($rowJudgment), ['fail', 'ng', '부적합'])) $savedVal = 'NG';
}
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm">
@if(in_array(strtolower($savedVal ?? ''), ['ok', 'pass', '적합', '합격']))
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-green-100 border border-green-300 mx-auto">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
</span>
@elseif($savedVal)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-red-100 border border-red-300 mx-auto">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</span>
@else
-
@endif
</td>
@else
@php
$savedVal = $getData($section->id, $col->id, $rowIndex, 'value');
if (!$savedVal) $savedVal = $getData($section->id, $col->id, $rowIndex, 'n1');
@endphp
<td class="px-2 py-2 border border-gray-300 text-center text-sm">{{ $savedVal ?: '-' }}</td>
@endif
@endif
@endforeach
</tr>
@endfor
</tbody>
</table>
</div>
@endif
{{-- 종합판정 / 비고 (마지막 섹션에만) --}}
@if($loop->last)
@php
// React 형식: overall_result, remark (+ 레거시 호환: footer_judgement, footer_remark)
$remarkVal = $docData->where('field_key', 'remark')->first()?->field_value
?? $docData->where('field_key', 'footer_remark')->first()?->field_value
?? '';
$judgementVal = $docData->where('field_key', 'overall_result')->first()?->field_value
?? $docData->where('field_key', 'footer_judgement')->first()?->field_value
?? '';
// fallback: overall_result 없으면 row_judgment에서 계산 (전체 행 수 대비 판정)
if (!$judgementVal) {
$rowJudgments = $docData->where('field_key', 'row_judgment')->pluck('field_value');
if ($rowJudgments->isNotEmpty() && $rowJudgments->count() >= $rowCount) {
$hasFail = $rowJudgments->contains(fn($v) => in_array(strtolower($v), ['fail', '부', '부적합', '불합격']));
$allPass = $rowJudgments->every(fn($v) => in_array(strtolower($v), ['pass', '적', '적합', '합격']));
$judgementVal = $hasFail ? '불합격' : ($allPass ? '합격' : '');
}
}
$isPass = in_array(strtolower($judgementVal), ['pass', 'ok', '적합', '합격']);
@endphp
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200">
<div>
<dt class="text-sm font-medium text-gray-500">{{ $document->template->footer_remark_label ?? '비고' }}</dt>
<dd class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">{{ $remarkVal ?: '-' }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">{{ $document->template->footer_judgement_label ?? '종합판정' }}</dt>
<dd class="mt-1">
@if($judgementVal)
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
{{ $isPass ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $isPass ? '적합' : '부적합' }}
</span>
@else
<span class="text-sm text-gray-500">-</span>
@endif
</dd>
</div>
</div>
@endif
</div>
@endforeach
@endif
@endif {{-- end: 스냅샷 vs 블록빌더 vs 레거시 분기 --}}
@endif {{-- end: 절곡 검사 vs 일반 섹션 분기 --}}
{{-- 첨부파일 --}}
@if($document->attachments && $document->attachments->count() > 0)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">첨부파일</h2>
<ul class="divide-y divide-gray-200">
@foreach($document->attachments as $attachment)
<li class="py-3 flex items-center justify-between">
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/>
</svg>
<div>
<p class="text-sm font-medium text-gray-900">{{ $attachment->file->original_name ?? '파일명 없음' }}</p>
<p class="text-xs text-gray-500">
{{ $attachment->type_label }} ·
{{ $attachment->file ? number_format($attachment->file->size / 1024, 1) . ' KB' : '-' }}
</p>
</div>
</div>
@if($attachment->file)
<a href="{{ route('files.download', $attachment->file->id) }}"
class="text-sm text-blue-600 hover:text-blue-800">
다운로드
</a>
@endif
</li>
@endforeach
</ul>
</div>
@endif
</div>
{{-- 사이드바 --}}
<div class="space-y-6">
{{-- 결재 현황 --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">결재 현황</h2>
@if($document->approvals && $document->approvals->count() > 0)
<ol class="relative border-l border-gray-200 ml-3">
@foreach($document->approvals as $approval)
<li class="mb-6 ml-6">
<span class="absolute flex items-center justify-center w-6 h-6 rounded-full -left-3 ring-4 ring-white
@if($approval->status === 'APPROVED') bg-green-500
@elseif($approval->status === 'REJECTED') bg-red-500
@elseif($approval->status === 'PENDING') bg-yellow-500
@else bg-gray-300
@endif">
@if($approval->status === 'APPROVED')
<svg class="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
@elseif($approval->status === 'REJECTED')
<svg class="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
@else
<span class="text-white text-xs font-bold">{{ $approval->step }}</span>
@endif
</span>
<div>
<h3 class="text-sm font-medium text-gray-900">
{{ $approval->role }}
<span class="text-xs text-gray-400 ml-1">
({{ $approval->status_label }})
</span>
</h3>
<p class="text-xs text-gray-500">{{ $approval->user->name ?? '미지정' }}</p>
@if($approval->acted_at)
<p class="text-xs text-gray-400 mt-1">{{ $approval->acted_at->format('Y-m-d H:i') }}</p>
@endif
@if($approval->comment)
<p class="text-xs text-gray-600 mt-1 bg-gray-50 p-2 rounded">{{ $approval->comment }}</p>
@endif
</div>
</li>
@endforeach
</ol>
@else
<p class="text-sm text-gray-500">결재선이 설정되지 않았습니다.</p>
@endif
</div>
{{-- 문서 이력 --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">문서 이력</h2>
<p class="text-sm text-gray-500">문서 이력 기능은 추후 구현 예정입니다.</p>
</div>
</div>
</div>
{{-- 반려 사유 모달 --}}
@if($document->isPending())
<div id="rejectModal" class="hidden fixed inset-0 bg-gray-600/50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">문서 반려</h3>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">반려 사유 <span class="text-red-500">*</span></label>
<textarea id="rejectComment" rows="4" required
class="w-full rounded-lg border-gray-300 text-sm focus:border-red-500 focus:ring-red-500"
placeholder="반려 사유를 입력하세요"></textarea>
</div>
<div class="flex justify-end gap-3">
<button onclick="closeRejectModal()"
class="px-4 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200">
취소
</button>
<button onclick="rejectDocument()"
class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700">
반려
</button>
</div>
</div>
</div>
@endif
@endsection
@push('scripts')
<script>
@if($document->isPending())
window.approveDocument = function() {
if (!confirm('이 문서를 승인하시겠습니까?')) return;
fetch('/api/admin/documents/{{ $document->id }}/approve', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ comment: '' })
})
.then(response => response.json())
.then(result => {
if (result.success) {
showToast(result.message, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(result.message || '오류가 발생했습니다.', 'error');
}
})
.catch(error => {
console.error('Approve error:', error);
showToast('승인 중 오류가 발생했습니다.', 'error');
});
};
window.showRejectModal = function() {
document.getElementById('rejectModal').classList.remove('hidden');
};
window.closeRejectModal = function() {
document.getElementById('rejectModal').classList.add('hidden');
document.getElementById('rejectComment').value = '';
};
window.rejectDocument = function() {
const comment = document.getElementById('rejectComment').value.trim();
if (!comment) {
showToast('반려 사유를 입력해주세요.', 'error');
return;
}
fetch('/api/admin/documents/{{ $document->id }}/reject', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ comment: comment })
})
.then(response => response.json())
.then(result => {
if (result.success) {
showToast(result.message, 'success');
closeRejectModal();
setTimeout(() => location.reload(), 1000);
} else {
showToast(result.message || '오류가 발생했습니다.', 'error');
}
})
.catch(error => {
console.error('Reject error:', error);
showToast('반려 중 오류가 발생했습니다.', 'error');
});
};
@endif
</script>
@endpush