Files
sam-api/app/Services/WorkResultService.php
권혁성 7246ac003f fix(WEB): 수주 페이지 필드 매핑 및 제품-부품 트리 구조 개선
- ApiClient 인터페이스: representative → manager_name, contact_person 변경
- transformApiToFrontend: client.representative → client.manager_name 수정
- ApiOrderItem에 floor_code, symbol_code 필드 추가 (제품-부품 매핑)
- ApiOrder에 options 타입 정의 추가
- ApiQuote에 calculation_inputs 타입 정의 추가
- 수주 상세 페이지 제품-부품 트리 구조 UI 개선
2026-01-20 16:14:46 +09:00

293 lines
9.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\Members\User;
use App\Models\Production\WorkOrderItem;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* 작업실적 서비스
*
* 완료된 WorkOrderItem의 options->result 데이터를 기반으로 작업실적 조회/수정
*/
class WorkResultService extends Service
{
/**
* 목록 조회 (검색/필터링/페이징)
*
* 완료된 WorkOrderItem에서 result 데이터가 있는 항목만 조회
*/
public function index(array $params)
{
$tenantId = $this->tenantId();
$page = (int) ($params['page'] ?? 1);
$size = (int) ($params['size'] ?? 20);
$q = trim((string) ($params['q'] ?? ''));
$workOrderId = $params['work_order_id'] ?? null;
$workerId = $params['worker_id'] ?? null;
$workDateFrom = $params['work_date_from'] ?? null;
$workDateTo = $params['work_date_to'] ?? null;
$isInspected = isset($params['is_inspected']) ? filter_var($params['is_inspected'], FILTER_VALIDATE_BOOLEAN) : null;
$isPackaged = isset($params['is_packaged']) ? filter_var($params['is_packaged'], FILTER_VALIDATE_BOOLEAN) : null;
$query = WorkOrderItem::query()
->where('tenant_id', $tenantId)
->where('status', WorkOrderItem::STATUS_COMPLETED)
->whereNotNull('options->result')
->with([
'workOrder:id,work_order_no,project_name,process_id,completed_at',
'workOrder.process:id,process_name,process_code',
]);
// 검색어 (로트번호, 품목명, 작업지시번호)
if ($q !== '') {
$query->where(function ($qq) use ($q) {
$qq->where('options->result->lot_no', 'like', "%{$q}%")
->orWhere('item_name', 'like', "%{$q}%")
->orWhereHas('workOrder', fn ($wo) => $wo->where('work_order_no', 'like', "%{$q}%"));
});
}
// 작업지시 필터
if ($workOrderId !== null) {
$query->where('work_order_id', $workOrderId);
}
// 작업자 필터
if ($workerId !== null) {
$query->where('options->result->worker_id', $workerId);
}
// 작업일 범위 (completed_at 기준)
if ($workDateFrom !== null) {
$query->where('options->result->completed_at', '>=', $workDateFrom);
}
if ($workDateTo !== null) {
$query->where('options->result->completed_at', '<=', $workDateTo.' 23:59:59');
}
// 검사 완료 필터
if ($isInspected !== null) {
$query->where('options->result->is_inspected', $isInspected);
}
// 포장 완료 필터
if ($isPackaged !== null) {
$query->where('options->result->is_packaged', $isPackaged);
}
// 최신 완료순 정렬
$query->orderByDesc('options->result->completed_at')->orderByDesc('id');
$paginated = $query->paginate($size, ['*'], 'page', $page);
// worker_id로 작업자 이름 조회하여 추가
$workerIds = $paginated->getCollection()
->map(fn ($item) => $item->options['result']['worker_id'] ?? null)
->filter()
->unique()
->values()
->toArray();
$workers = [];
if (! empty($workerIds)) {
$workers = User::whereIn('id', $workerIds)
->pluck('name', 'id')
->toArray();
}
// 각 아이템에 worker_name 추가
$paginated->getCollection()->transform(function ($item) use ($workers) {
$workerId = $item->options['result']['worker_id'] ?? null;
$item->worker_name = $workerId ? ($workers[$workerId] ?? null) : null;
return $item;
});
return $paginated;
}
/**
* 통계 조회
*/
public function stats(array $params = []): array
{
$tenantId = $this->tenantId();
$workDateFrom = $params['work_date_from'] ?? null;
$workDateTo = $params['work_date_to'] ?? null;
$query = WorkOrderItem::where('tenant_id', $tenantId)
->where('status', WorkOrderItem::STATUS_COMPLETED)
->whereNotNull('options->result');
// 작업일 범위
if ($workDateFrom !== null) {
$query->where('options->result->completed_at', '>=', $workDateFrom);
}
if ($workDateTo !== null) {
$query->where('options->result->completed_at', '<=', $workDateTo.' 23:59:59');
}
// JSON에서 집계 (MySQL/MariaDB 기준)
$items = $query->get();
$totalProduction = 0;
$totalGood = 0;
$totalDefect = 0;
foreach ($items as $item) {
$result = $item->options['result'] ?? [];
$goodQty = (float) ($result['good_qty'] ?? 0);
$defectQty = (float) ($result['defect_qty'] ?? 0);
$totalGood += $goodQty;
$totalDefect += $defectQty;
$totalProduction += ($goodQty + $defectQty);
}
$defectRate = $totalProduction > 0
? round(($totalDefect / $totalProduction) * 100, 1)
: 0;
return [
'total_production' => (int) $totalProduction,
'total_good' => (int) $totalGood,
'total_defect' => (int) $totalDefect,
'defect_rate' => $defectRate,
];
}
/**
* 단건 조회
*/
public function show(int $id)
{
$tenantId = $this->tenantId();
$item = WorkOrderItem::where('tenant_id', $tenantId)
->where('status', WorkOrderItem::STATUS_COMPLETED)
->whereNotNull('options->result')
->with([
'workOrder:id,work_order_no,project_name,process_id,completed_at',
'workOrder.process:id,process_name,process_code',
])
->find($id);
if (! $item) {
throw new NotFoundHttpException(__('error.not_found'));
}
// worker_name 추가
$workerId = $item->options['result']['worker_id'] ?? null;
if ($workerId) {
$item->worker_name = User::where('id', $workerId)->value('name');
} else {
$item->worker_name = null;
}
return $item;
}
/**
* 작업실적 수정 (양품/불량 수량 등)
*/
public function update(int $id, array $data)
{
$tenantId = $this->tenantId();
$item = WorkOrderItem::where('tenant_id', $tenantId)
->where('status', WorkOrderItem::STATUS_COMPLETED)
->whereNotNull('options->result')
->find($id);
if (! $item) {
throw new NotFoundHttpException(__('error.not_found'));
}
return DB::transaction(function () use ($item, $data) {
$options = $item->options ?? [];
$result = $options['result'] ?? [];
// 수정 가능한 필드만 업데이트
$allowedFields = ['good_qty', 'defect_qty', 'lot_no', 'is_inspected', 'is_packaged', 'memo'];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $data)) {
$result[$field] = $data[$field];
}
}
// 불량률 재계산
$totalQty = ($result['good_qty'] ?? 0) + ($result['defect_qty'] ?? 0);
$result['defect_rate'] = $totalQty > 0
? round(($result['defect_qty'] / $totalQty) * 100, 2)
: 0;
$options['result'] = $result;
$item->options = $options;
$item->save();
return $item->fresh([
'workOrder:id,work_order_no,project_name,process_id',
'workOrder.process:id,process_name,process_code',
]);
});
}
/**
* 검사 상태 토글
*/
public function toggleInspection(int $id)
{
$tenantId = $this->tenantId();
$item = WorkOrderItem::where('tenant_id', $tenantId)
->where('status', WorkOrderItem::STATUS_COMPLETED)
->whereNotNull('options->result')
->find($id);
if (! $item) {
throw new NotFoundHttpException(__('error.not_found'));
}
$options = $item->options ?? [];
$result = $options['result'] ?? [];
$result['is_inspected'] = ! ($result['is_inspected'] ?? false);
$options['result'] = $result;
$item->options = $options;
$item->save();
return $item->fresh();
}
/**
* 포장 상태 토글
*/
public function togglePackaging(int $id)
{
$tenantId = $this->tenantId();
$item = WorkOrderItem::where('tenant_id', $tenantId)
->where('status', WorkOrderItem::STATUS_COMPLETED)
->whereNotNull('options->result')
->find($id);
if (! $item) {
throw new NotFoundHttpException(__('error.not_found'));
}
$options = $item->options ?? [];
$result = $options['result'] ?? [];
$result['is_packaged'] = ! ($result['is_packaged'] ?? false);
$options['result'] = $result;
$item->options = $options;
$item->save();
return $item->fresh();
}
}