- 검사문서 모달 및 템플릿 기능 확장 - WorkOrders actions 추가 - 작업자화면 WorkOrderListPanel 개선 - 생산대시보드 actions/타입 보강 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
5.8 KiB
TypeScript
178 lines
5.8 KiB
TypeScript
/**
|
|
* 생산 현황판 서버 액션
|
|
* 공정 기반 동적 탭 전환
|
|
*/
|
|
|
|
'use server';
|
|
|
|
|
|
import { executeServerAction } from '@/lib/api/execute-server-action';
|
|
import { buildApiUrl } from '@/lib/api/query-params';
|
|
import type { WorkOrder, WorkerStatus, DashboardStats, ProcessOption } from './types';
|
|
|
|
// ===== API 타입 =====
|
|
interface WorkOrderApiItem {
|
|
id: number;
|
|
work_order_no: string;
|
|
project_name: string | null;
|
|
process_id: number | null;
|
|
process?: { id: number; process_code: string; process_name: string };
|
|
status: 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed' | 'shipped';
|
|
scheduled_date: string | null;
|
|
memo: string | null;
|
|
created_at: string;
|
|
sales_order?: {
|
|
id: number; order_no: string; client_id?: number; client_name?: string;
|
|
item?: { id: number; code: string; name: string } | null;
|
|
client?: { id: number; name: string }; root_nodes_count?: number;
|
|
};
|
|
assignee?: { id: number; name: string };
|
|
items?: { id: number; item_name: string; item_id?: number | null; item?: { id: number; code: string; name: string } | null; quantity: number; options?: Record<string, unknown> | null }[];
|
|
}
|
|
|
|
// ===== 상태 변환 =====
|
|
function mapApiStatus(status: WorkOrderApiItem['status']): 'waiting' | 'inProgress' | 'completed' {
|
|
switch (status) {
|
|
case 'in_progress': return 'inProgress';
|
|
case 'completed': case 'shipped': return 'completed';
|
|
default: return 'waiting';
|
|
}
|
|
}
|
|
|
|
// ===== API → WorkOrder 변환 =====
|
|
function transformToProductionFormat(api: WorkOrderApiItem): WorkOrder {
|
|
const totalQuantity = (api.items || []).reduce((sum, item) => sum + Number(item.quantity), 0);
|
|
const productCode = (api.items?.[0]?.options?.product_code as string) || api.sales_order?.item?.code || '-';
|
|
const productName = (api.items?.[0]?.options?.product_name as string) || api.items?.[0]?.item_name || '-';
|
|
const dueDate = api.scheduled_date || '';
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const due = dueDate ? new Date(dueDate) : null;
|
|
const isDelayed = due ? due < today && api.status !== 'completed' && api.status !== 'shipped' : false;
|
|
const delayDays = due && isDelayed
|
|
? Math.ceil((today.getTime() - due.getTime()) / (1000 * 60 * 60 * 24))
|
|
: undefined;
|
|
const isUrgent = due
|
|
? due.getTime() - today.getTime() <= 3 * 24 * 60 * 60 * 1000 && due.getTime() >= today.getTime()
|
|
: false;
|
|
|
|
return {
|
|
id: String(api.id),
|
|
orderNo: api.work_order_no,
|
|
productCode,
|
|
productName,
|
|
processCode: api.process?.process_code || '-',
|
|
processName: api.process?.process_name || '-',
|
|
client: api.sales_order?.client_name || api.sales_order?.client?.name || '-',
|
|
projectName: api.project_name || '-',
|
|
assignees: api.assignee ? [api.assignee.name] : [],
|
|
quantity: totalQuantity,
|
|
shutterCount: api.sales_order?.root_nodes_count || 0,
|
|
dueDate,
|
|
priority: isUrgent ? 1 : 5,
|
|
status: mapApiStatus(api.status),
|
|
isUrgent,
|
|
isDelayed,
|
|
delayDays,
|
|
instruction: api.memo || undefined,
|
|
createdAt: api.created_at,
|
|
};
|
|
}
|
|
|
|
// ===== 공정 옵션 목록 조회 =====
|
|
export async function getProcessOptions(): Promise<{
|
|
success: boolean;
|
|
data: ProcessOption[];
|
|
error?: string;
|
|
}> {
|
|
interface ProcessApiData {
|
|
id: number;
|
|
process_code: string;
|
|
process_name: string;
|
|
process_type?: string;
|
|
department?: string;
|
|
}
|
|
|
|
const result = await executeServerAction({
|
|
url: buildApiUrl('/api/v1/processes/options'),
|
|
transform: (data: ProcessApiData[]) =>
|
|
(data || []).map(p => ({
|
|
id: p.id,
|
|
code: p.process_code,
|
|
name: p.process_name,
|
|
type: p.process_type,
|
|
department: p.department,
|
|
})),
|
|
errorMessage: '공정 목록 조회에 실패했습니다.',
|
|
});
|
|
|
|
return {
|
|
success: result.success,
|
|
data: result.data || [],
|
|
error: result.error,
|
|
};
|
|
}
|
|
|
|
// ===== 대시보드 데이터 조회 =====
|
|
export async function getDashboardData(processCode?: string): Promise<{
|
|
success: boolean;
|
|
workOrders: WorkOrder[];
|
|
workerStatus: WorkerStatus[];
|
|
stats: DashboardStats;
|
|
error?: string;
|
|
}> {
|
|
const emptyResult = {
|
|
success: false,
|
|
workOrders: [] as WorkOrder[],
|
|
workerStatus: [] as WorkerStatus[],
|
|
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
|
|
};
|
|
|
|
const result = await executeServerAction<{ data: WorkOrderApiItem[] }>({
|
|
url: buildApiUrl('/api/v1/work-orders', {
|
|
per_page: 100,
|
|
process_code: processCode && processCode !== 'all' ? processCode : undefined,
|
|
}),
|
|
errorMessage: '데이터 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success || !result.data) {
|
|
return { ...emptyResult, error: result.error };
|
|
}
|
|
|
|
const apiData = result.data.data || [];
|
|
const workOrders = apiData.map(transformToProductionFormat);
|
|
|
|
// 통계 계산
|
|
const stats: DashboardStats = {
|
|
total: workOrders.length,
|
|
waiting: workOrders.filter(o => o.status === 'waiting').length,
|
|
inProgress: workOrders.filter(o => o.status === 'inProgress').length,
|
|
completed: workOrders.filter(o => o.status === 'completed').length,
|
|
urgent: workOrders.filter(o => o.isUrgent).length,
|
|
delayed: workOrders.filter(o => o.isDelayed).length,
|
|
};
|
|
|
|
// 작업자별 현황 집계
|
|
const workerMap = new Map<string, WorkerStatus>();
|
|
workOrders.forEach(order => {
|
|
order.assignees.forEach(name => {
|
|
if (!name || name === '-') return;
|
|
if (!workerMap.has(name)) {
|
|
workerMap.set(name, { id: name, name, inProgress: 0, completed: 0, assigned: 0 });
|
|
}
|
|
const worker = workerMap.get(name)!;
|
|
worker.assigned++;
|
|
if (order.status === 'inProgress') worker.inProgress++;
|
|
else if (order.status === 'completed') worker.completed++;
|
|
});
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
workOrders,
|
|
workerStatus: Array.from(workerMap.values()).slice(0, 10),
|
|
stats,
|
|
};
|
|
}
|