feat: [생산지시] 전용 API 엔드포인트 신규 생성
- ProductionOrderService: 목록(index), 통계(stats), 상세(show) 구현
- Order 기반 생산지시 대상 필터링 (IN_PROGRESS~SHIPPED)
- workOrderProgress 가공 필드 (total/completed/inProgress)
- production_ordered_at (첫 WorkOrder created_at 기반)
- BOM 공정 분류 추출 (order_nodes.options.bom_result)
- ProductionOrderController: FormRequest + ApiResponse 패턴
- ProductionOrderIndexRequest: search, production_status, sort, pagination 검증
- ProductionOrderApi.php: Swagger 문서 (목록/통계/상세)
- production.php: GET /production-orders, /stats, /{orderId} 라우트 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
231
app/Services/ProductionOrderService.php
Normal file
231
app/Services/ProductionOrderService.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class ProductionOrderService extends Service
|
||||
{
|
||||
/**
|
||||
* 생산지시 대상 상태 코드
|
||||
*/
|
||||
private const PRODUCTION_STATUSES = [
|
||||
Order::STATUS_IN_PROGRESS,
|
||||
Order::STATUS_IN_PRODUCTION,
|
||||
Order::STATUS_PRODUCED,
|
||||
Order::STATUS_SHIPPING,
|
||||
Order::STATUS_SHIPPED,
|
||||
];
|
||||
|
||||
/**
|
||||
* 생산지시 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Order::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('status_code', self::PRODUCTION_STATUSES)
|
||||
->with(['client', 'workOrders.process', 'workOrders.assignees.user'])
|
||||
->withCount('workOrders');
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('order_no', 'like', "%{$search}%")
|
||||
->orWhere('client_name', 'like', "%{$search}%")
|
||||
->orWhere('site_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 생산 상태 필터
|
||||
if (! empty($params['production_status'])) {
|
||||
switch ($params['production_status']) {
|
||||
case 'waiting':
|
||||
$query->where('status_code', Order::STATUS_IN_PROGRESS);
|
||||
break;
|
||||
case 'in_production':
|
||||
$query->where('status_code', Order::STATUS_IN_PRODUCTION);
|
||||
break;
|
||||
case 'completed':
|
||||
$query->whereIn('status_code', [
|
||||
Order::STATUS_PRODUCED,
|
||||
Order::STATUS_SHIPPING,
|
||||
Order::STATUS_SHIPPED,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
$result = $query->paginate($perPage);
|
||||
|
||||
// 가공 필드 추가
|
||||
$result->getCollection()->transform(function (Order $order) {
|
||||
$order->production_ordered_at = $order->workOrders->min('created_at');
|
||||
|
||||
$workOrders = $order->workOrders;
|
||||
$order->work_order_progress = [
|
||||
'total' => $workOrders->count(),
|
||||
'completed' => $workOrders->where('status', 'completed')->count()
|
||||
+ $workOrders->where('status', 'shipped')->count(),
|
||||
'in_progress' => $workOrders->where('status', 'in_progress')->count(),
|
||||
];
|
||||
|
||||
// 프론트 탭용 production_status 매핑
|
||||
$order->production_status = $this->mapProductionStatus($order->status_code);
|
||||
|
||||
return $order;
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 통계
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$waiting = Order::where('tenant_id', $tenantId)
|
||||
->where('status_code', Order::STATUS_IN_PROGRESS)
|
||||
->count();
|
||||
|
||||
$inProduction = Order::where('tenant_id', $tenantId)
|
||||
->where('status_code', Order::STATUS_IN_PRODUCTION)
|
||||
->count();
|
||||
|
||||
$completed = Order::where('tenant_id', $tenantId)
|
||||
->whereIn('status_code', [
|
||||
Order::STATUS_PRODUCED,
|
||||
Order::STATUS_SHIPPING,
|
||||
Order::STATUS_SHIPPED,
|
||||
])
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total' => $waiting + $inProduction + $completed,
|
||||
'waiting' => $waiting,
|
||||
'in_production' => $inProduction,
|
||||
'completed' => $completed,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산지시 상세 조회
|
||||
*/
|
||||
public function show(int $orderId): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$order = Order::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('status_code', self::PRODUCTION_STATUSES)
|
||||
->with([
|
||||
'client',
|
||||
'workOrders.process',
|
||||
'workOrders.items',
|
||||
'workOrders.assignees.user',
|
||||
'nodes',
|
||||
])
|
||||
->findOrFail($orderId);
|
||||
|
||||
// 생산지시일
|
||||
$order->production_ordered_at = $order->workOrders->min('created_at');
|
||||
$order->production_status = $this->mapProductionStatus($order->status_code);
|
||||
|
||||
// WorkOrder 진행 현황
|
||||
$workOrderProgress = [
|
||||
'total' => $order->workOrders->count(),
|
||||
'completed' => $order->workOrders->where('status', 'completed')->count()
|
||||
+ $order->workOrders->where('status', 'shipped')->count(),
|
||||
'in_progress' => $order->workOrders->where('status', 'in_progress')->count(),
|
||||
];
|
||||
|
||||
// WorkOrder 목록 가공
|
||||
$workOrders = $order->workOrders->map(function ($wo) {
|
||||
return [
|
||||
'id' => $wo->id,
|
||||
'work_order_no' => $wo->work_order_no,
|
||||
'process_name' => $wo->process?->process_name ?? '',
|
||||
'quantity' => $wo->items->count(),
|
||||
'status' => $wo->status,
|
||||
'assignees' => $wo->assignees->map(fn ($a) => $a->user?->name ?? '')->filter()->values()->toArray(),
|
||||
];
|
||||
});
|
||||
|
||||
// BOM 데이터 (order_nodes에서 추출)
|
||||
$bomProcessGroups = $this->extractBomProcessGroups($order->nodes);
|
||||
|
||||
return [
|
||||
'order' => $order->makeHidden(['workOrders', 'nodes']),
|
||||
'production_ordered_at' => $order->production_ordered_at,
|
||||
'production_status' => $order->production_status,
|
||||
'work_order_progress' => $workOrderProgress,
|
||||
'work_orders' => $workOrders,
|
||||
'bom_process_groups' => $bomProcessGroups,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Order status_code → 프론트 production_status 매핑
|
||||
*/
|
||||
private function mapProductionStatus(string $statusCode): string
|
||||
{
|
||||
return match ($statusCode) {
|
||||
Order::STATUS_IN_PROGRESS => 'waiting',
|
||||
Order::STATUS_IN_PRODUCTION => 'in_production',
|
||||
default => 'completed',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* order_nodes에서 BOM 공정 분류 추출
|
||||
*/
|
||||
private function extractBomProcessGroups($nodes): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$bomResult = $node->options['bom_result'] ?? null;
|
||||
if (! $bomResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// bom_result 구조에 따라 공정별 그룹화
|
||||
foreach ($bomResult as $item) {
|
||||
$processName = $item['process_name'] ?? '기타';
|
||||
|
||||
if (! isset($groups[$processName])) {
|
||||
$groups[$processName] = [
|
||||
'process_name' => $processName,
|
||||
'size_spec' => $item['size_spec'] ?? null,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$groups[$processName]['items'][] = [
|
||||
'id' => $item['id'] ?? null,
|
||||
'item_code' => $item['item_code'] ?? '',
|
||||
'item_name' => $item['item_name'] ?? '',
|
||||
'spec' => $item['spec'] ?? '',
|
||||
'lot_no' => $item['lot_no'] ?? '',
|
||||
'required_qty' => $item['required_qty'] ?? 0,
|
||||
'qty' => $item['qty'] ?? 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($groups);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user