- WorkOrderCreate: 수동 모드 품목 검색/추가/수량 관리 UI 구현 - WorkOrders/actions: items 파라미터 추가, searchItemsForWorkOrder 함수 추가 - StockStatusList: 품목분류(BENDING/SCREEN/STEEL/ALUMINUM) 필터 추가 - StockStatus/actions: itemCategory 파라미터 지원
1137 lines
32 KiB
TypeScript
1137 lines
32 KiB
TypeScript
/**
|
|
* 작업지시 관리 서버 액션
|
|
*
|
|
* API Endpoints:
|
|
* - GET /api/v1/work-orders - 목록 조회
|
|
* - GET /api/v1/work-orders/stats - 통계 조회
|
|
* - GET /api/v1/work-orders/{id} - 상세 조회
|
|
* - POST /api/v1/work-orders - 등록
|
|
* - PUT /api/v1/work-orders/{id} - 수정
|
|
* - DELETE /api/v1/work-orders/{id} - 삭제
|
|
* - PATCH /api/v1/work-orders/{id}/status - 상태 변경
|
|
* - PATCH /api/v1/work-orders/{id}/assign - 담당자 배정
|
|
* - PATCH /api/v1/work-orders/{id}/bending/toggle - 벤딩 필드 토글
|
|
* - POST /api/v1/work-orders/{id}/issues - 이슈 등록
|
|
* - PATCH /api/v1/work-orders/{id}/issues/{issueId}/resolve - 이슈 해결
|
|
* - PATCH /api/v1/work-orders/{id}/items/{itemId}/status - 품목 상태 변경
|
|
*/
|
|
|
|
'use server';
|
|
|
|
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
|
import { buildApiUrl } from '@/lib/api/query-params';
|
|
import type {
|
|
WorkOrder,
|
|
WorkOrderStats,
|
|
WorkOrderStatus,
|
|
WorkOrderApiPaginatedResponse,
|
|
WorkOrderStatsApi,
|
|
} from './types';
|
|
import {
|
|
transformApiToFrontend,
|
|
transformFrontendToApi,
|
|
transformStatsApiToFrontend,
|
|
} from './types';
|
|
|
|
// ===== 페이지네이션 타입 =====
|
|
interface PaginationMeta {
|
|
currentPage: number;
|
|
lastPage: number;
|
|
perPage: number;
|
|
total: number;
|
|
}
|
|
|
|
// ===== 작업지시 목록 조회 =====
|
|
export async function getWorkOrders(params?: {
|
|
page?: number;
|
|
perPage?: number;
|
|
status?: WorkOrderStatus | 'all';
|
|
processId?: number | 'all' | 'none'; // 공정 ID (FK → processes.id), 'none' = 미지정
|
|
processType?: 'screen' | 'slat' | 'bending'; // 공정 타입 필터
|
|
priority?: string; // 우선순위 필터 (urgent/priority/normal)
|
|
search?: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
data: WorkOrder[];
|
|
pagination: PaginationMeta;
|
|
error?: string;
|
|
}> {
|
|
const emptyResponse = {
|
|
success: false,
|
|
data: [] as WorkOrder[],
|
|
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
|
};
|
|
|
|
try {
|
|
const url = buildApiUrl('/api/v1/work-orders', {
|
|
page: params?.page,
|
|
per_page: params?.perPage,
|
|
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
|
// 'none': 공정 미지정 필터 (process_id IS NULL), 'all': 제외
|
|
process_id: params?.processId && params.processId !== 'all' ? String(params.processId) : undefined,
|
|
process_type: params?.processType,
|
|
priority: params?.priority && params.priority !== 'all' ? params.priority : undefined,
|
|
search: params?.search,
|
|
start_date: params?.startDate,
|
|
end_date: params?.endDate,
|
|
});
|
|
|
|
const { response, error } = await serverFetch(url, { method: 'GET' });
|
|
|
|
if (error || !response) {
|
|
return { ...emptyResponse, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.warn('[WorkOrderActions] GET work-orders error:', response.status);
|
|
return { ...emptyResponse, error: `API 오류: ${response.status}` };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
return {
|
|
...emptyResponse,
|
|
error: result.message || '작업지시 목록 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const paginatedData: WorkOrderApiPaginatedResponse = result.data || {
|
|
data: [],
|
|
current_page: 1,
|
|
last_page: 1,
|
|
per_page: 20,
|
|
total: 0,
|
|
};
|
|
|
|
const workOrders = (paginatedData.data || []).map(transformApiToFrontend);
|
|
|
|
return {
|
|
success: true,
|
|
data: workOrders,
|
|
pagination: {
|
|
currentPage: paginatedData.current_page,
|
|
lastPage: paginatedData.last_page,
|
|
perPage: paginatedData.per_page,
|
|
total: paginatedData.total,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getWorkOrders error:', error);
|
|
return { ...emptyResponse, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 작업지시 통계 조회 =====
|
|
export async function getWorkOrderStats(): Promise<{
|
|
success: boolean;
|
|
data?: WorkOrderStats;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const url = buildApiUrl('/api/v1/work-orders/stats');
|
|
|
|
|
|
const { response, error } = await serverFetch(url, { method: 'GET' });
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.warn('[WorkOrderActions] GET stats error:', response.status);
|
|
return { success: false, error: `API 오류: ${response.status}` };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '통계 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const statsApi: WorkOrderStatsApi = result.data;
|
|
|
|
return {
|
|
success: true,
|
|
data: transformStatsApiToFrontend(statsApi),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getWorkOrderStats error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 작업지시 상세 조회 =====
|
|
export async function getWorkOrderById(id: string): Promise<{
|
|
success: boolean;
|
|
data?: WorkOrder;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const url = buildApiUrl(`/api/v1/work-orders/${id}`);
|
|
|
|
|
|
const { response, error } = await serverFetch(url, { method: 'GET' });
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.error('[WorkOrderActions] GET work-order error:', response.status);
|
|
return { success: false, error: `API 오류: ${response.status}` };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success || !result.data) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '작업지시 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getWorkOrderById error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 작업지시 등록 =====
|
|
export async function createWorkOrder(
|
|
data: Partial<WorkOrder> & {
|
|
salesOrderId?: number;
|
|
assigneeId?: number; // 단일 담당자 (하위 호환)
|
|
assigneeIds?: number[]; // 다중 담당자
|
|
teamId?: number;
|
|
items?: Array<{ // 수동 등록 시 품목 목록
|
|
item_id?: number;
|
|
item_name: string;
|
|
specification?: string;
|
|
quantity?: number;
|
|
unit?: string;
|
|
}>;
|
|
}
|
|
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
|
try {
|
|
// 다중 담당자 우선, 없으면 단일 담당자 배열로 변환
|
|
const assigneeIds = data.assigneeIds && data.assigneeIds.length > 0
|
|
? data.assigneeIds
|
|
: data.assigneeId
|
|
? [data.assigneeId]
|
|
: undefined;
|
|
|
|
const apiData = {
|
|
...transformFrontendToApi(data),
|
|
sales_order_id: data.salesOrderId,
|
|
assignee_ids: assigneeIds, // 배열로 전송
|
|
team_id: data.teamId,
|
|
...(data.items && data.items.length > 0 ? { items: data.items } : {}),
|
|
};
|
|
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl('/api/v1/work-orders'),
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(apiData),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '작업지시 등록에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] createWorkOrder error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 품목 검색 (수동 등록용) =====
|
|
export interface ManualItemOption {
|
|
id: number;
|
|
code: string;
|
|
name: string;
|
|
specification: string;
|
|
unit: string;
|
|
itemCategory: string;
|
|
}
|
|
|
|
export async function searchItemsForWorkOrder(
|
|
query?: string,
|
|
itemCategory?: string
|
|
): Promise<{ success: boolean; data: ManualItemOption[] }> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl('/api/v1/items', {
|
|
search: query,
|
|
item_category: itemCategory,
|
|
per_page: 50,
|
|
}),
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, data: [] };
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (!response.ok || !result.success) {
|
|
return { success: false, data: [] };
|
|
}
|
|
|
|
const items: ManualItemOption[] = (result.data?.data || []).map((item: Record<string, unknown>) => ({
|
|
id: item.id as number,
|
|
code: (item.item_code || item.code) as string,
|
|
name: (item.item_name || item.name) as string,
|
|
specification: (item.specification || '') as string,
|
|
unit: (item.unit || 'EA') as string,
|
|
itemCategory: (item.item_category || '') as string,
|
|
}));
|
|
|
|
return { success: true, data: items };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] searchItemsForWorkOrder error:', error);
|
|
return { success: false, data: [] };
|
|
}
|
|
}
|
|
|
|
// ===== 작업지시 수정 =====
|
|
export async function updateWorkOrder(
|
|
id: string,
|
|
data: Partial<WorkOrder> & { assigneeIds?: number[] }
|
|
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
|
try {
|
|
const apiData = transformFrontendToApi(data);
|
|
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${id}`),
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify(apiData),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '작업지시 수정에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] updateWorkOrder error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 작업지시 삭제 =====
|
|
export async function deleteWorkOrder(id: string): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${id}`),
|
|
{ method: 'DELETE' }
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '작업지시 삭제에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] deleteWorkOrder error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 작업지시 상태 변경 =====
|
|
export async function updateWorkOrderStatus(
|
|
id: string,
|
|
status: WorkOrderStatus
|
|
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
|
try {
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${id}/status`),
|
|
{
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ status }),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '상태 변경에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] updateWorkOrderStatus error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 담당자 배정 =====
|
|
export async function assignWorkOrder(
|
|
id: string,
|
|
assigneeIds: number | number[], // 단일 또는 다중 담당자
|
|
teamId?: number
|
|
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
|
try {
|
|
// 배열로 통일
|
|
const ids = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds];
|
|
const body: { assignee_ids: number[]; team_id?: number } = { assignee_ids: ids };
|
|
if (teamId) body.team_id = teamId;
|
|
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${id}/assign`),
|
|
{
|
|
method: 'PATCH',
|
|
body: JSON.stringify(body),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '담당자 배정에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] assignWorkOrder error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 벤딩 필드 토글 =====
|
|
export async function toggleBendingField(
|
|
id: string,
|
|
field: string
|
|
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
|
try {
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${id}/bending/toggle`),
|
|
{
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ field }),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '벤딩 필드 토글에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] toggleBendingField error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 이슈 등록 =====
|
|
export async function addWorkOrderIssue(
|
|
id: string,
|
|
data: {
|
|
title: string;
|
|
description?: string;
|
|
priority?: 'low' | 'medium' | 'high';
|
|
}
|
|
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
|
try {
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${id}/issues`),
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '이슈 등록에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] addWorkOrderIssue error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 이슈 해결 =====
|
|
export async function resolveWorkOrderIssue(
|
|
workOrderId: string,
|
|
issueId: string
|
|
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
|
try {
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`),
|
|
{ method: 'PATCH' }
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '이슈 해결 처리에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] resolveWorkOrderIssue error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 품목 상태 변경 =====
|
|
export type WorkOrderItemStatus = 'waiting' | 'in_progress' | 'completed';
|
|
|
|
export async function updateWorkOrderItemStatus(
|
|
workOrderId: string,
|
|
itemId: number,
|
|
status: WorkOrderItemStatus
|
|
): Promise<{
|
|
success: boolean;
|
|
itemId: number;
|
|
status: WorkOrderItemStatus;
|
|
workOrderStatus?: string;
|
|
workOrderStatusChanged?: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/items/${itemId}/status`),
|
|
{
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ status }),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, itemId, status, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
itemId,
|
|
status,
|
|
error: result.message || '품목 상태 변경에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
itemId,
|
|
status: result.data?.item?.status || status,
|
|
workOrderStatus: result.data?.work_order_status,
|
|
workOrderStatusChanged: result.data?.work_order_status_changed || false,
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] updateWorkOrderItemStatus error:', error);
|
|
return { success: false, itemId, status, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 중간검사 성적서 데이터 조회 =====
|
|
export interface InspectionReportItem {
|
|
id: number;
|
|
item_name: string;
|
|
specification: string;
|
|
quantity: number;
|
|
sort_order: number;
|
|
status: string;
|
|
options: Record<string, unknown> | null;
|
|
inspection_data: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface InspectionReportNodeGroup {
|
|
node_id: number | null;
|
|
node_name: string;
|
|
floor: string;
|
|
code: string;
|
|
width: number;
|
|
height: number;
|
|
total_quantity: number;
|
|
options: Record<string, unknown>;
|
|
items: InspectionReportItem[];
|
|
}
|
|
|
|
export interface InspectionReportData {
|
|
work_order: {
|
|
id: number;
|
|
order_no: string;
|
|
status: string;
|
|
planned_date: string | null;
|
|
due_date: string | null;
|
|
};
|
|
order: {
|
|
id: number;
|
|
order_no: string;
|
|
client_name: string | null;
|
|
site_name: string | null;
|
|
order_date: string | null;
|
|
} | null;
|
|
node_groups?: InspectionReportNodeGroup[];
|
|
items: InspectionReportItem[];
|
|
summary: {
|
|
total_items: number;
|
|
inspected_items: number;
|
|
passed_items: number;
|
|
failed_items: number;
|
|
};
|
|
}
|
|
|
|
export async function getInspectionReport(
|
|
workOrderId: string
|
|
): Promise<{ success: boolean; data?: InspectionReportData; error?: string }> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-report`),
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '검사 성적서 데이터를 불러올 수 없습니다.',
|
|
};
|
|
}
|
|
|
|
return { success: true, data: result.data };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getInspectionReport error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 중간검사 데이터 저장 (deprecated: WorkerScreen/actions.ts의 saveItemInspection 사용) =====
|
|
/** @deprecated WorkerScreen/actions.ts의 saveItemInspection을 사용하세요 */
|
|
export async function saveInspectionData(
|
|
workOrderId: string,
|
|
processType: string,
|
|
data: unknown
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection`),
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
process_type: processType,
|
|
inspection_data: data,
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '검사 데이터 저장에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] saveInspectionData error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 검사 문서 템플릿 조회 (document_template 기반) =====
|
|
import type { InspectionTemplateData } from '@/components/production/WorkerScreen/types';
|
|
|
|
export async function getInspectionTemplate(
|
|
workOrderId: string
|
|
): Promise<{ success: boolean; data?: InspectionTemplateData; error?: string }> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-template`),
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (!response.ok || !result.success) {
|
|
return { success: false, error: result.message || '검사 템플릿을 불러올 수 없습니다.' };
|
|
}
|
|
|
|
return { success: true, data: result.data };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getInspectionTemplate error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 검사 문서 저장 (Document + DocumentData) =====
|
|
export async function saveInspectionDocument(
|
|
workOrderId: string,
|
|
data: {
|
|
step_id?: number;
|
|
title?: string;
|
|
data: Record<string, unknown>[];
|
|
approvers?: { role_name: string; user_id?: number }[];
|
|
}
|
|
): Promise<{
|
|
success: boolean;
|
|
data?: { document_id: number; document_no: string; status: string };
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-document`),
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (!response.ok || !result.success) {
|
|
return { success: false, error: result.message || '검사 문서 저장에 실패했습니다.' };
|
|
}
|
|
|
|
return { success: true, data: result.data };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] saveInspectionDocument error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 검사 문서 resolve (기존 문서/템플릿 조회) =====
|
|
export async function resolveInspectionDocument(
|
|
workOrderId: string,
|
|
params?: { step_id?: number }
|
|
): Promise<{
|
|
success: boolean;
|
|
data?: { mode: 'existing' | 'new'; document?: Record<string, unknown>; template?: Record<string, unknown> };
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-resolve`, { step_id: params?.step_id }),
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (!response.ok || !result.success) {
|
|
return { success: false, error: result.message || '검사 문서 조회에 실패했습니다.' };
|
|
}
|
|
|
|
return { success: true, data: result.data };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] resolveInspectionDocument error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 수주 목록 조회 (작업지시 생성용) =====
|
|
export interface SalesOrderForWorkOrder {
|
|
id: number;
|
|
orderNo: string;
|
|
client: string;
|
|
projectName: string;
|
|
dueDate: string;
|
|
status: string;
|
|
itemCount: number;
|
|
splitCount: number;
|
|
}
|
|
|
|
export async function getSalesOrdersForWorkOrder(params?: {
|
|
q?: string;
|
|
status?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
data: SalesOrderForWorkOrder[];
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
// 작업지시 생성 가능한 상태만 조회 (예: 회계확인 완료)
|
|
const url = buildApiUrl('/api/v1/orders', {
|
|
for_work_order: '1',
|
|
q: params?.q,
|
|
status: params?.status,
|
|
});
|
|
|
|
const { response, error } = await serverFetch(url, { method: 'GET' });
|
|
|
|
if (error || !response) {
|
|
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.warn('[WorkOrderActions] GET orders error:', response.status);
|
|
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
return {
|
|
success: false,
|
|
data: [],
|
|
error: result.message || '수주 목록 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
// API 응답 변환
|
|
const salesOrders: SalesOrderForWorkOrder[] = (result.data?.data || result.data || []).map(
|
|
(item: {
|
|
id: number;
|
|
order_no: string;
|
|
client?: { name: string };
|
|
project_name?: string;
|
|
due_date?: string;
|
|
status: string;
|
|
items_count?: number;
|
|
split_count?: number;
|
|
}) => ({
|
|
id: item.id,
|
|
orderNo: item.order_no,
|
|
client: item.client?.name || '-',
|
|
projectName: item.project_name || '-',
|
|
dueDate: item.due_date || '-',
|
|
status: item.status,
|
|
itemCount: item.items_count || 0,
|
|
splitCount: item.split_count || 0,
|
|
})
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: salesOrders,
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getSalesOrdersForWorkOrder error:', error);
|
|
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 부서 + 사용자 조회 (담당자 선택용) =====
|
|
export interface DepartmentUser {
|
|
id: number;
|
|
name: string;
|
|
email: string;
|
|
}
|
|
|
|
export interface DepartmentWithUsers {
|
|
id: number;
|
|
name: string;
|
|
code: string | null;
|
|
users: DepartmentUser[];
|
|
children: DepartmentWithUsers[];
|
|
}
|
|
|
|
export async function getDepartmentsWithUsers(): Promise<{
|
|
success: boolean;
|
|
data: DepartmentWithUsers[];
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const url = buildApiUrl('/api/v1/departments/tree', { with_users: 1 });
|
|
|
|
|
|
const { response, error } = await serverFetch(url, { method: 'GET' });
|
|
|
|
if (error || !response) {
|
|
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.warn('[WorkOrderActions] GET departments error:', response.status);
|
|
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
return {
|
|
success: false,
|
|
data: [],
|
|
error: result.message || '부서 목록 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
// API 응답을 프론트엔드 형식으로 변환
|
|
const transformDepartment = (dept: {
|
|
id: number;
|
|
name: string;
|
|
code: string | null;
|
|
users?: { id: number; name: string; email: string }[];
|
|
children?: unknown[];
|
|
}): DepartmentWithUsers => ({
|
|
id: dept.id,
|
|
name: dept.name,
|
|
code: dept.code,
|
|
users: (dept.users || []).map((u) => ({
|
|
id: u.id,
|
|
name: u.name,
|
|
email: u.email,
|
|
})),
|
|
children: (dept.children || []).map((child) =>
|
|
transformDepartment(child as typeof dept)
|
|
),
|
|
});
|
|
|
|
const departments = (result.data || []).map(transformDepartment);
|
|
|
|
return {
|
|
success: true,
|
|
data: departments,
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getDepartmentsWithUsers error:', error);
|
|
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 공정 목록 조회 (작업지시 생성용) =====
|
|
export interface ProcessOption {
|
|
id: number;
|
|
processCode: string;
|
|
processName: string;
|
|
}
|
|
|
|
export async function getProcessOptions(): Promise<{
|
|
success: boolean;
|
|
data: ProcessOption[];
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const url = buildApiUrl('/api/v1/processes/options');
|
|
|
|
|
|
const { response, error } = await serverFetch(url, { method: 'GET' });
|
|
|
|
if (error || !response) {
|
|
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.warn('[WorkOrderActions] GET process options error:', response.status);
|
|
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
return {
|
|
success: false,
|
|
data: [],
|
|
error: result.message || '공정 목록 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
// API 응답 변환
|
|
const processes: ProcessOption[] = (result.data || []).map(
|
|
(item: {
|
|
id: number;
|
|
process_code: string;
|
|
process_name: string;
|
|
}) => ({
|
|
id: item.id,
|
|
processCode: item.process_code,
|
|
processName: item.process_name,
|
|
})
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: processes,
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getProcessOptions error:', error);
|
|
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
// ===== 자재 투입 LOT 번호 조회 =====
|
|
export interface MaterialInputLot {
|
|
lot_no: string;
|
|
item_code: string;
|
|
item_name: string;
|
|
total_qty: number;
|
|
input_count: number;
|
|
first_input_at: string;
|
|
}
|
|
|
|
export async function getMaterialInputLots(workOrderId: string): Promise<{
|
|
success: boolean;
|
|
data: MaterialInputLot[];
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/material-input-lots`),
|
|
{ method: 'GET' }
|
|
);
|
|
|
|
if (error || !response) {
|
|
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return { success: false, data: [], error: result.message || '투입 LOT 조회에 실패했습니다.' };
|
|
}
|
|
|
|
return { success: true, data: result.data || [] };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[WorkOrderActions] getMaterialInputLots error:', error);
|
|
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|