feat:문서관리 필터 기능 개선 - 양식분류 필터 추가, onchange 즉시 검색
- 상태/템플릿 필터에 onchange 즉시 검색 적용 - 양식분류(category) 필터 추가 (API + 뷰 컨트롤러 + 프론트) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,14 @@ public function index(Request $request): JsonResponse
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// 양식분류 필터
|
||||
if ($request->filled('category')) {
|
||||
$category = $request->category;
|
||||
$query->whereHas('template', function ($q) use ($category) {
|
||||
$q->where('category', $category);
|
||||
});
|
||||
}
|
||||
|
||||
// 템플릿 필터
|
||||
if ($request->filled('template_id')) {
|
||||
$query->where('template_id', $request->template_id);
|
||||
|
||||
@@ -31,8 +31,21 @@ public function index(Request $request): View|Response
|
||||
})->where('is_active', true)->orderBy('name')->get()
|
||||
: collect();
|
||||
|
||||
// 양식분류 목록 (필터용)
|
||||
$categories = $tenantId
|
||||
? DocumentTemplate::where(function ($q) use ($tenantId) {
|
||||
$q->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
|
||||
})->where('is_active', true)
|
||||
->whereNotNull('category')
|
||||
->distinct()
|
||||
->pluck('category')
|
||||
->sort()
|
||||
->values()
|
||||
: collect();
|
||||
|
||||
return view('documents.index', [
|
||||
'templates' => $templates,
|
||||
'categories' => $categories,
|
||||
'statuses' => Document::STATUS_LABELS,
|
||||
]);
|
||||
}
|
||||
@@ -178,6 +191,9 @@ public function show(int $id): View
|
||||
|
||||
// 연결된 작업지시서의 work_order_items 로드
|
||||
$workOrderItems = collect();
|
||||
$workOrder = null;
|
||||
$salesOrder = null;
|
||||
$materialInputLots = collect();
|
||||
if ($document->linkable_type === 'work_order' && $document->linkable_id) {
|
||||
$workOrderItems = DB::table('work_order_items')
|
||||
->where('work_order_id', $document->linkable_id)
|
||||
@@ -188,6 +204,63 @@ public function show(int $id): View
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
// 작업일지용: 작업지시 + 수주 + 자재투입LOT 로드
|
||||
$workOrder = DB::table('work_orders')
|
||||
->where('id', $document->linkable_id)
|
||||
->first();
|
||||
|
||||
if ($workOrder?->sales_order_id) {
|
||||
$salesOrder = DB::table('orders')
|
||||
->where('id', $workOrder->sales_order_id)
|
||||
->first();
|
||||
}
|
||||
|
||||
// 자재 투입 LOT (stock_transactions 기반, 취소 트랜잭션 상쇄 포함)
|
||||
$transactions = DB::table('stock_transactions')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('reference_type', ['work_order_input', 'work_order_input_cancel'])
|
||||
->where('reference_id', $document->linkable_id)
|
||||
->orderBy('created_at')
|
||||
->get(['lot_no', 'item_code', 'item_name', 'qty', 'reference_type', 'created_at']);
|
||||
|
||||
$lotMap = [];
|
||||
foreach ($transactions as $tx) {
|
||||
$lotNo = $tx->lot_no;
|
||||
if (! isset($lotMap[$lotNo])) {
|
||||
$lotMap[$lotNo] = (object) [
|
||||
'lot_no' => $lotNo,
|
||||
'item_code' => $tx->item_code,
|
||||
'item_name' => $tx->item_name,
|
||||
'total_qty' => 0,
|
||||
'input_count' => 0,
|
||||
'first_input_at' => $tx->created_at,
|
||||
];
|
||||
}
|
||||
// OUT(투입)은 음수, IN(취소)은 양수 → 합산하면 순수 투입량
|
||||
$lotMap[$lotNo]->total_qty += (float) $tx->qty;
|
||||
if ($tx->reference_type === 'work_order_input') {
|
||||
$lotMap[$lotNo]->input_count++;
|
||||
}
|
||||
}
|
||||
// 순수 투입량이 0 이하인 LOT 제외, qty를 절대값으로 변환
|
||||
$materialInputLots = collect(array_values($lotMap))
|
||||
->filter(fn ($lot) => $lot->total_qty < 0)
|
||||
->map(function ($lot) {
|
||||
$lot->total_qty = abs($lot->total_qty);
|
||||
return $lot;
|
||||
})
|
||||
->values();
|
||||
|
||||
// 개소별 투입자재 LOT (work_order_material_inputs 기반, work_order_item_id → lot_no)
|
||||
$itemLotMap = DB::table('work_order_material_inputs as mi')
|
||||
->join('stock_lots as sl', 'sl.id', '=', 'mi.stock_lot_id')
|
||||
->where('mi.work_order_id', $document->linkable_id)
|
||||
->select('mi.work_order_item_id', 'sl.lot_no', 'mi.qty')
|
||||
->orderBy('mi.work_order_item_id')
|
||||
->get()
|
||||
->groupBy('work_order_item_id')
|
||||
->map(fn ($rows) => $rows->pluck('lot_no')->unique()->join(', '));
|
||||
}
|
||||
|
||||
// 기본정보 bf_ 자동 backfill
|
||||
@@ -196,12 +269,17 @@ public function show(int $id): View
|
||||
return view('documents.show', [
|
||||
'document' => $document,
|
||||
'workOrderItems' => $workOrderItems,
|
||||
'workOrder' => $workOrder,
|
||||
'salesOrder' => $salesOrder,
|
||||
'materialInputLots' => $materialInputLots,
|
||||
'itemLotMap' => $itemLotMap ?? collect(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본정보(bf_) 레코드가 없으면 원본 데이터에서 resolve → document_data에 저장
|
||||
* React resolveFieldValue와 동일 매핑 로직
|
||||
* 검사 문서: field_key 기반 매핑
|
||||
* 작업일지: label 기반 매핑 (DB에서 직접 JOIN)
|
||||
*/
|
||||
private function resolveAndBackfillBasicFields(Document $document): void
|
||||
{
|
||||
@@ -242,31 +320,25 @@ private function resolveAndBackfillBasicFields(Document $document): void
|
||||
? DB::table('orders')->find($workOrder->sales_order_id)
|
||||
: null;
|
||||
|
||||
// 검사자 정보: work_order_items[0].options.inspection_data.inspected_by
|
||||
$firstItem = $workOrderItems->first();
|
||||
$inspectionData = $firstItem?->options['inspection_data'] ?? [];
|
||||
$inspectedBy = $inspectionData['inspected_by'] ?? null;
|
||||
$inspectedAt = $inspectionData['inspected_at'] ?? null;
|
||||
$inspectorName = $inspectedBy
|
||||
? DB::table('users')->where('id', $inspectedBy)->value('name')
|
||||
: null;
|
||||
// 작업일지 vs 검사 문서 판별: 섹션이 없으면 작업일지
|
||||
$isWorkLog = ! $document->template->sections || $document->template->sections->isEmpty();
|
||||
|
||||
// field_key → 값 매핑
|
||||
$resolveMap = [
|
||||
'product_name' => $firstItem?->item_name ?? '',
|
||||
'specification' => $firstItem?->specification ?? '',
|
||||
'lot_no' => $order?->order_no ?? '',
|
||||
'lot_size' => $workOrderItems->count().' 개소',
|
||||
'client' => $order?->client_name ?? '',
|
||||
'site_name' => $workOrder->project_name ?? '',
|
||||
'inspection_date' => $inspectedAt ? substr($inspectedAt, 0, 10) : now()->format('Y-m-d'),
|
||||
'inspector' => $inspectorName ?? '',
|
||||
];
|
||||
if ($isWorkLog) {
|
||||
$resolveMap = $this->buildWorkLogResolveMap($workOrder, $order);
|
||||
} else {
|
||||
$resolveMap = $this->buildInspectionResolveMap($workOrder, $workOrderItems, $order);
|
||||
}
|
||||
|
||||
// document_data에 bf_ 레코드 저장
|
||||
$records = [];
|
||||
foreach ($basicFields as $field) {
|
||||
$value = $resolveMap[$field->field_key] ?? $field->default_value ?? '';
|
||||
if ($isWorkLog) {
|
||||
// 작업일지: label 기반 매핑
|
||||
$value = $resolveMap[$field->label] ?? $field->default_value ?? '';
|
||||
} else {
|
||||
// 검사 문서: field_key 기반 매핑
|
||||
$value = $resolveMap[$field->field_key] ?? $field->default_value ?? '';
|
||||
}
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
@@ -289,6 +361,60 @@ private function resolveAndBackfillBasicFields(Document $document): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업일지용 기본필드 resolve 맵 (label → 값)
|
||||
* API WorkOrderService::buildWorkLogAutoValues 와 동일 매핑
|
||||
*/
|
||||
private function buildWorkLogResolveMap(object $workOrder, ?object $salesOrder): array
|
||||
{
|
||||
$receivedAt = $salesOrder?->received_at ?? $salesOrder?->created_at ?? null;
|
||||
$orderDate = $receivedAt ? substr((string) $receivedAt, 0, 10) : '';
|
||||
$deliveryDate = $salesOrder?->delivery_date ?? null;
|
||||
$deliveryStr = $deliveryDate ? substr((string) $deliveryDate, 0, 10) : '';
|
||||
$orderNo = $salesOrder?->order_no ?? '';
|
||||
|
||||
return [
|
||||
'발주처' => $salesOrder?->client_name ?? '',
|
||||
'현장명' => $salesOrder?->site_name ?? $workOrder->project_name ?? '',
|
||||
'작업일자' => now()->format('Y-m-d'),
|
||||
'LOT NO' => $orderNo,
|
||||
'납기일' => $deliveryStr,
|
||||
'작업지시번호' => $workOrder->work_order_no ?? '',
|
||||
'수주일' => $orderDate,
|
||||
'수주처' => $salesOrder?->client_name ?? '',
|
||||
'담당자' => '',
|
||||
'연락처' => '',
|
||||
'제품 LOT NO' => $orderNo,
|
||||
'생산담당자' => '',
|
||||
'출고예정일' => $deliveryStr,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 검사 문서용 기본필드 resolve 맵 (field_key → 값)
|
||||
*/
|
||||
private function buildInspectionResolveMap(object $workOrder, $workOrderItems, ?object $order): array
|
||||
{
|
||||
$firstItem = $workOrderItems->first();
|
||||
$inspectionData = $firstItem?->options['inspection_data'] ?? [];
|
||||
$inspectedBy = $inspectionData['inspected_by'] ?? null;
|
||||
$inspectedAt = $inspectionData['inspected_at'] ?? null;
|
||||
$inspectorName = $inspectedBy
|
||||
? DB::table('users')->where('id', $inspectedBy)->value('name')
|
||||
: null;
|
||||
|
||||
return [
|
||||
'product_name' => $firstItem?->item_name ?? '',
|
||||
'specification' => $firstItem?->specification ?? '',
|
||||
'lot_no' => $order?->order_no ?? '',
|
||||
'lot_size' => $workOrderItems->count().' 개소',
|
||||
'client' => $order?->client_name ?? '',
|
||||
'site_name' => $workOrder->project_name ?? '',
|
||||
'inspection_date' => $inspectedAt ? substr($inspectedAt, 0, 10) : now()->format('Y-m-d'),
|
||||
'inspector' => $inspectorName ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿에 연결된 품목들의 규격 정보 (thickness, width, length) 조회
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
|
||||
<!-- 상태 필터 -->
|
||||
<div class="w-full sm:w-32">
|
||||
<select name="status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<select name="status" onchange="loadDocuments()" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체 상태</option>
|
||||
@foreach($statuses as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@@ -49,9 +49,19 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 양식분류 필터 -->
|
||||
<div class="w-full sm:w-40">
|
||||
<select name="category" onchange="loadDocuments()" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체 양식분류</option>
|
||||
@foreach($categories as $category)
|
||||
<option value="{{ $category }}">{{ $category }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 템플릿 필터 -->
|
||||
<div class="w-full sm:w-40">
|
||||
<select name="template_id" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<select name="template_id" onchange="loadDocuments()" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체 템플릿</option>
|
||||
@foreach($templates as $template)
|
||||
<option value="{{ $template->id }}">{{ $template->name }}</option>
|
||||
|
||||
Reference in New Issue
Block a user