Files
sam-react-prod/src/components/production/ProductionDashboard/actions.ts
유병철 2a2a356f58 feat: [생산] 작업지시/작업자화면/대시보드 개선
- 검사문서 모달 및 템플릿 기능 확장
- WorkOrders actions 추가
- 작업자화면 WorkOrderListPanel 개선
- 생산대시보드 actions/타입 보강

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:20:41 +09:00

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,
};
}