- ApiClient 인터페이스: representative → manager_name, contact_person 변경 - transformApiToFrontend: client.representative → client.manager_name 수정 - ApiOrderItem에 floor_code, symbol_code 필드 추가 (제품-부품 매핑) - ApiOrder에 options 타입 정의 추가 - ApiQuote에 calculation_inputs 타입 정의 추가 - 수주 상세 페이지 제품-부품 트리 구조 UI 개선
293 lines
9.2 KiB
PHP
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();
|
|
}
|
|
}
|