feat: [생산] 작업지시/작업자화면/대시보드 개선
- 검사문서 모달 및 템플릿 기능 확장 - WorkOrders actions 추가 - 작업자화면 WorkOrderListPanel 개선 - 생산대시보드 actions/타입 보강 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,11 @@ interface WorkOrderApiItem {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
sales_order?: {
|
sales_order?: {
|
||||||
id: number; order_no: string; client_id?: number; client_name?: string;
|
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;
|
client?: { id: number; name: string }; root_nodes_count?: number;
|
||||||
};
|
};
|
||||||
assignee?: { id: number; name: string };
|
assignee?: { id: number; name: string };
|
||||||
items?: { id: number; item_name: string; quantity: number }[];
|
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 }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 상태 변환 =====
|
// ===== 상태 변환 =====
|
||||||
@@ -41,7 +42,8 @@ function mapApiStatus(status: WorkOrderApiItem['status']): 'waiting' | 'inProgre
|
|||||||
// ===== API → WorkOrder 변환 =====
|
// ===== API → WorkOrder 변환 =====
|
||||||
function transformToProductionFormat(api: WorkOrderApiItem): WorkOrder {
|
function transformToProductionFormat(api: WorkOrderApiItem): WorkOrder {
|
||||||
const totalQuantity = (api.items || []).reduce((sum, item) => sum + Number(item.quantity), 0);
|
const totalQuantity = (api.items || []).reduce((sum, item) => sum + Number(item.quantity), 0);
|
||||||
const productName = api.items?.[0]?.item_name || '-';
|
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 dueDate = api.scheduled_date || '';
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
@@ -57,6 +59,7 @@ function transformToProductionFormat(api: WorkOrderApiItem): WorkOrder {
|
|||||||
return {
|
return {
|
||||||
id: String(api.id),
|
id: String(api.id),
|
||||||
orderNo: api.work_order_no,
|
orderNo: api.work_order_no,
|
||||||
|
productCode,
|
||||||
productName,
|
productName,
|
||||||
processCode: api.process?.process_code || '-',
|
processCode: api.process?.process_code || '-',
|
||||||
processName: api.process?.process_name || '-',
|
processName: api.process?.process_name || '-',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface ProcessOption {
|
|||||||
export interface WorkOrder {
|
export interface WorkOrder {
|
||||||
id: string;
|
id: string;
|
||||||
orderNo: string; // KD-WO-251216-01
|
orderNo: string; // KD-WO-251216-01
|
||||||
|
productCode: string; // 제품코드 (KQTS01 등)
|
||||||
productName: string; // 스크린 서터 (표준형) - 추가
|
productName: string; // 스크린 서터 (표준형) - 추가
|
||||||
processCode: string; // 공정 코드 (P-001, P-002, ...)
|
processCode: string; // 공정 코드 (P-001, P-002, ...)
|
||||||
processName: string; // 공정명 (슬랫, 스크린, 절곡, ...)
|
processName: string; // 공정명 (슬랫, 스크린, 절곡, ...)
|
||||||
|
|||||||
@@ -773,6 +773,53 @@ export async function saveInspectionData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 검사 설정 조회 (공정 판별 + 구성품 목록) =====
|
||||||
|
export interface InspectionConfigGapPoint {
|
||||||
|
point: string;
|
||||||
|
design_value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InspectionConfigItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
gap_points: InspectionConfigGapPoint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InspectionConfigData {
|
||||||
|
work_order_id: number;
|
||||||
|
process_type: string;
|
||||||
|
product_code: string | null;
|
||||||
|
finishing_type: string | null;
|
||||||
|
template_id: number | null;
|
||||||
|
items: InspectionConfigItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInspectionConfig(
|
||||||
|
workOrderId: string | number
|
||||||
|
): Promise<{ success: boolean; data?: InspectionConfigData; error?: string }> {
|
||||||
|
try {
|
||||||
|
const { response, error } = await serverFetch(
|
||||||
|
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-config`),
|
||||||
|
{ 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] getInspectionConfig error:', error);
|
||||||
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 검사 문서 템플릿 조회 (document_template 기반) =====
|
// ===== 검사 문서 템플릿 조회 (document_template 기반) =====
|
||||||
import type { InspectionTemplateData } from '@/components/production/WorkerScreen/types';
|
import type { InspectionTemplateData } from '@/components/production/WorkerScreen/types';
|
||||||
|
|
||||||
@@ -874,6 +921,33 @@ export async function resolveInspectionDocument(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 문서 결재 상신 =====
|
||||||
|
export async function submitDocumentForApproval(
|
||||||
|
documentId: number
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
const { response, error } = await serverFetch(
|
||||||
|
buildApiUrl(`/api/v1/documents/${documentId}/submit`),
|
||||||
|
{ method: 'POST' }
|
||||||
|
);
|
||||||
|
|
||||||
|
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] submitDocumentForApproval error:', error);
|
||||||
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 수주 목록 조회 (작업지시 생성용) =====
|
// ===== 수주 목록 조회 (작업지시 생성용) =====
|
||||||
export interface SalesOrderForWorkOrder {
|
export interface SalesOrderForWorkOrder {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Loader2, Save } from 'lucide-react';
|
import { Loader2, Save, Send } from 'lucide-react';
|
||||||
import { DocumentViewer } from '@/components/document-system';
|
import { DocumentViewer } from '@/components/document-system';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
getInspectionTemplate,
|
getInspectionTemplate,
|
||||||
saveInspectionDocument,
|
saveInspectionDocument,
|
||||||
resolveInspectionDocument,
|
resolveInspectionDocument,
|
||||||
|
submitDocumentForApproval,
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
import type { WorkOrder, ProcessType } from '../types';
|
import type { WorkOrder, ProcessType } from '../types';
|
||||||
import type { InspectionReportData, InspectionReportNodeGroup } from '../actions';
|
import type { InspectionReportData, InspectionReportNodeGroup } from '../actions';
|
||||||
@@ -178,6 +179,10 @@ export function InspectionReportModal({
|
|||||||
field_key: string;
|
field_key: string;
|
||||||
field_value: string | null;
|
field_value: string | null;
|
||||||
}> | null>(null);
|
}> | null>(null);
|
||||||
|
// 기존 문서 ID/상태 (결재 상신용)
|
||||||
|
const [savedDocumentId, setSavedDocumentId] = useState<number | null>(null);
|
||||||
|
const [savedDocumentStatus, setSavedDocumentStatus] = useState<string | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
// props에서 목업 제외한 실제 개소만 사용 (WorkerScreen에서 apiItems + mockItems가 합쳐져 전달됨)
|
// props에서 목업 제외한 실제 개소만 사용 (WorkerScreen에서 apiItems + mockItems가 합쳐져 전달됨)
|
||||||
// ★ 반드시 workItems와 inspectionDataMap을 같은 소스에서 가져와야 key 포맷이 일치함
|
// ★ 반드시 workItems와 inspectionDataMap을 같은 소스에서 가져와야 key 포맷이 일치함
|
||||||
@@ -289,18 +294,23 @@ export function InspectionReportModal({
|
|||||||
setSelfTemplateData(templateResult.data);
|
setSelfTemplateData(templateResult.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) 기존 문서의 document_data EAV 레코드 추출
|
// 4) 기존 문서의 document_data EAV 레코드 + ID/상태 추출
|
||||||
if (resolveResult?.success && resolveResult.data) {
|
if (resolveResult?.success && resolveResult.data) {
|
||||||
const existingDoc = (resolveResult.data as Record<string, unknown>).existing_document as
|
const existingDoc = (resolveResult.data as Record<string, unknown>).existing_document as
|
||||||
| { data?: Array<{ section_id: number | null; column_id: number | null; row_index: number; field_key: string; field_value: string | null }> }
|
| { id?: number; status?: string; data?: Array<{ section_id: number | null; column_id: number | null; row_index: number; field_key: string; field_value: string | null }> }
|
||||||
| null;
|
| null;
|
||||||
if (existingDoc?.data && existingDoc.data.length > 0) {
|
if (existingDoc?.data && existingDoc.data.length > 0) {
|
||||||
setDocumentRecords(existingDoc.data);
|
setDocumentRecords(existingDoc.data);
|
||||||
} else {
|
} else {
|
||||||
setDocumentRecords(null);
|
setDocumentRecords(null);
|
||||||
}
|
}
|
||||||
|
// 문서 ID/상태 저장 (결재 상신용)
|
||||||
|
setSavedDocumentId(existingDoc?.id ?? null);
|
||||||
|
setSavedDocumentStatus(existingDoc?.status ?? null);
|
||||||
} else {
|
} else {
|
||||||
setDocumentRecords(null);
|
setDocumentRecords(null);
|
||||||
|
setSavedDocumentId(null);
|
||||||
|
setSavedDocumentStatus(null);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -316,6 +326,8 @@ export function InspectionReportModal({
|
|||||||
setReportSummary(null);
|
setReportSummary(null);
|
||||||
setSelfTemplateData(null);
|
setSelfTemplateData(null);
|
||||||
setDocumentRecords(null);
|
setDocumentRecords(null);
|
||||||
|
setSavedDocumentId(null);
|
||||||
|
setSavedDocumentStatus(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, [open, workOrderId, processType, templateData]);
|
}, [open, workOrderId, processType, templateData]);
|
||||||
@@ -350,6 +362,12 @@ export function InspectionReportModal({
|
|||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('검사 문서가 저장되었습니다.');
|
toast.success('검사 문서가 저장되었습니다.');
|
||||||
|
// 저장 후 문서 ID/상태 갱신 (결재 상신 활성화용)
|
||||||
|
const docData = result.data as { id?: number; status?: string } | undefined;
|
||||||
|
if (docData?.id) {
|
||||||
|
setSavedDocumentId(docData.id);
|
||||||
|
setSavedDocumentStatus(docData.status ?? 'DRAFT');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.error || '저장에 실패했습니다.');
|
toast.error(result.error || '저장에 실패했습니다.');
|
||||||
}
|
}
|
||||||
@@ -369,6 +387,27 @@ export function InspectionReportModal({
|
|||||||
}
|
}
|
||||||
}, [workOrderId, processType, activeTemplate, activeStepId]);
|
}, [workOrderId, processType, activeTemplate, activeStepId]);
|
||||||
|
|
||||||
|
// 결재 상신 핸들러
|
||||||
|
const handleSubmitForApproval = useCallback(async () => {
|
||||||
|
if (!savedDocumentId) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result = await submitDocumentForApproval(savedDocumentId);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('결재 상신이 완료되었습니다.');
|
||||||
|
setSavedDocumentStatus('PENDING');
|
||||||
|
onOpenChange(false);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || '결재 상신에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('결재 상신 중 오류가 발생했습니다.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [savedDocumentId, onOpenChange]);
|
||||||
|
|
||||||
if (!workOrderId) return null;
|
if (!workOrderId) return null;
|
||||||
|
|
||||||
const processLabel = PROCESS_LABELS[processType] || '스크린';
|
const processLabel = PROCESS_LABELS[processType] || '스크린';
|
||||||
@@ -426,7 +465,11 @@ export function InspectionReportModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 결재 상신 가능 여부: 저장된 DRAFT 문서가 있을 때
|
||||||
|
const canSubmitForApproval = savedDocumentId != null && savedDocumentStatus === 'DRAFT';
|
||||||
|
|
||||||
const toolbarExtra = !readOnly ? (
|
const toolbarExtra = !readOnly ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Button onClick={handleSave} disabled={isSaving} size="sm">
|
<Button onClick={handleSave} disabled={isSaving} size="sm">
|
||||||
{isSaving ? (
|
{isSaving ? (
|
||||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||||
@@ -435,6 +478,22 @@ export function InspectionReportModal({
|
|||||||
)}
|
)}
|
||||||
{isSaving ? '저장 중...' : '저장'}
|
{isSaving ? '저장 중...' : '저장'}
|
||||||
</Button>
|
</Button>
|
||||||
|
{canSubmitForApproval && (
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmitForApproval}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
size="sm"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="w-4 h-4 mr-1.5" />
|
||||||
|
)}
|
||||||
|
{isSubmitting ? '상신 중...' : '결재 상신'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
// 검사 진행 상태 표시 (summary 있을 때)
|
// 검사 진행 상태 표시 (summary 있을 때)
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import {
|
|||||||
} from './inspection-shared';
|
} from './inspection-shared';
|
||||||
import { formatNumber } from '@/lib/utils/amount';
|
import { formatNumber } from '@/lib/utils/amount';
|
||||||
import type { BendingInfoExtended } from './bending/types';
|
import type { BendingInfoExtended } from './bending/types';
|
||||||
|
import { getInspectionConfig } from '../actions';
|
||||||
|
import type { InspectionConfigData } from '../actions';
|
||||||
|
|
||||||
export type { InspectionContentRef };
|
export type { InspectionContentRef };
|
||||||
|
|
||||||
@@ -375,10 +377,60 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
|
|||||||
)?.id ?? null;
|
)?.id ?? null;
|
||||||
}, [isBending, template.columns]);
|
}, [isBending, template.columns]);
|
||||||
|
|
||||||
|
// ===== inspection-config API 연동 =====
|
||||||
|
const [inspectionConfig, setInspectionConfig] = useState<InspectionConfigData | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isBending || !order.id) return;
|
||||||
|
let cancelled = false;
|
||||||
|
getInspectionConfig(order.id).then(result => {
|
||||||
|
if (!cancelled && result.success && result.data) {
|
||||||
|
setInspectionConfig(result.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [isBending, order.id]);
|
||||||
|
|
||||||
const bendingProducts = useMemo(() => {
|
const bendingProducts = useMemo(() => {
|
||||||
if (!isBending) return [];
|
if (!isBending) return [];
|
||||||
|
|
||||||
|
// API 응답이 있고 items가 있으면 API 기반 구성품 사용
|
||||||
|
if (inspectionConfig?.items?.length) {
|
||||||
|
const productCode = inspectionConfig.product_code || '';
|
||||||
|
// bending_info에서 dimension 보조 데이터 추출
|
||||||
|
const bi = order.bendingInfo as BendingInfoExtended | undefined;
|
||||||
|
const wallLen = bi?.guideRail?.wall?.lengthData?.[0]?.length;
|
||||||
|
const sideLen = bi?.guideRail?.side?.lengthData?.[0]?.length;
|
||||||
|
|
||||||
|
return inspectionConfig.items.map((item): BendingProduct => {
|
||||||
|
// API id → 표시용 매핑 (이름, 타입, 치수)
|
||||||
|
const displayMap: Record<string, { name: string; type: string; len: string; wid: string }> = {
|
||||||
|
guide_rail_wall: { name: '가이드레일', type: '벽면형', len: String(wallLen || 3500), wid: 'N/A' },
|
||||||
|
guide_rail_side: { name: '가이드레일', type: '측면형', len: String(sideLen || 3000), wid: 'N/A' },
|
||||||
|
bottom_bar: { name: '하단마감재', type: '60×40', len: '3000', wid: 'N/A' },
|
||||||
|
case_box: { name: '케이스', type: '양면', len: '3000', wid: 'N/A' },
|
||||||
|
smoke_w50: { name: '연기차단재', type: '화이바 W50\n가이드레일용', len: '-', wid: '50' },
|
||||||
|
smoke_w80: { name: '연기차단재', type: '화이바 W80\n케이스용', len: '-', wid: '80' },
|
||||||
|
};
|
||||||
|
const d = displayMap[item.id] || { name: item.name, type: '', len: '-', wid: 'N/A' };
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
category: productCode,
|
||||||
|
productName: d.name,
|
||||||
|
productType: d.type,
|
||||||
|
lengthDesign: d.len,
|
||||||
|
widthDesign: d.wid,
|
||||||
|
gapPoints: item.gap_points.map(gp => ({
|
||||||
|
point: gp.point,
|
||||||
|
designValue: gp.design_value,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback: 기존 프론트 로직 사용
|
||||||
return buildBendingProducts(order);
|
return buildBendingProducts(order);
|
||||||
}, [isBending, order]);
|
}, [isBending, order, inspectionConfig]);
|
||||||
|
|
||||||
const bendingExpandedRows = useMemo(() => {
|
const bendingExpandedRows = useMemo(() => {
|
||||||
if (!isBending) return [];
|
if (!isBending) return [];
|
||||||
@@ -740,8 +792,8 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비-Bending 모드: 개소(WorkItem)별 데이터
|
// 개소(WorkItem)별 데이터: 비-Bending 또는 Bending이지만 구성품이 없는 경우 (AS-IS)
|
||||||
if (!isBending) effectiveWorkItems.forEach((wi, rowIdx) => {
|
if (!isBending || bendingProducts.length === 0) effectiveWorkItems.forEach((wi, rowIdx) => {
|
||||||
for (const col of template.columns) {
|
for (const col of template.columns) {
|
||||||
// 일련번호 컬럼 → 저장 (mng show에서 표시용)
|
// 일련번호 컬럼 → 저장 (mng show에서 표시용)
|
||||||
if (isSerialColumn(col.label)) {
|
if (isSerialColumn(col.label)) {
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ export function WorkOrderListPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 품목명 */}
|
{/* 제품코드 - 제품명 */}
|
||||||
<p className="text-sm text-gray-600 truncate ml-8">{order.productName}</p>
|
<p className="text-sm text-gray-600 truncate ml-8">{order.productCode} - {order.productName}</p>
|
||||||
|
|
||||||
{/* 현장명 + 수량 */}
|
{/* 현장명 + 수량 */}
|
||||||
<div className="flex items-center justify-between mt-1.5 ml-8">
|
<div className="flex items-center justify-between mt-1.5 ml-8">
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface WorkOrderApiItem {
|
|||||||
sales_order?: {
|
sales_order?: {
|
||||||
id: number;
|
id: number;
|
||||||
order_no: string;
|
order_no: string;
|
||||||
|
item?: { id: number; code: string; name: string } | null;
|
||||||
client?: { id: number; name: string };
|
client?: { id: number; name: string };
|
||||||
client_contact?: string;
|
client_contact?: string;
|
||||||
options?: { manager_name?: string; [key: string]: unknown };
|
options?: { manager_name?: string; [key: string]: unknown };
|
||||||
@@ -50,6 +51,8 @@ interface WorkOrderApiItem {
|
|||||||
items?: {
|
items?: {
|
||||||
id: number;
|
id: number;
|
||||||
item_name: string;
|
item_name: string;
|
||||||
|
item_id?: number | null;
|
||||||
|
item?: { id: number; code: string; name: string } | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
specification?: string | null;
|
specification?: string | null;
|
||||||
options?: Record<string, unknown> | null;
|
options?: Record<string, unknown> | null;
|
||||||
@@ -89,7 +92,8 @@ function mapApiStatus(status: WorkOrderApiItem['status']): WorkOrderStatus {
|
|||||||
// ===== API → WorkOrder 변환 =====
|
// ===== API → WorkOrder 변환 =====
|
||||||
function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
|
function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
|
||||||
const totalQuantity = (api.items || []).reduce((sum, item) => sum + Number(item.quantity), 0);
|
const totalQuantity = (api.items || []).reduce((sum, item) => sum + Number(item.quantity), 0);
|
||||||
const productName = api.items?.[0]?.item_name || '-';
|
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 dueDate = api.scheduled_date || '';
|
||||||
@@ -173,6 +177,7 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
|
|||||||
return {
|
return {
|
||||||
id: String(api.id),
|
id: String(api.id),
|
||||||
orderNo: api.work_order_no,
|
orderNo: api.work_order_no,
|
||||||
|
productCode,
|
||||||
productName,
|
productName,
|
||||||
processCode: processInfo.code,
|
processCode: processInfo.code,
|
||||||
processName: processInfo.name,
|
processName: processInfo.name,
|
||||||
|
|||||||
@@ -714,7 +714,7 @@ export default function WorkerScreen() {
|
|||||||
workOrderId: selectedOrder.id,
|
workOrderId: selectedOrder.id,
|
||||||
itemNo: index + 1,
|
itemNo: index + 1,
|
||||||
itemCode: selectedOrder.orderNo || '-',
|
itemCode: selectedOrder.orderNo || '-',
|
||||||
itemName: itemSummary,
|
itemName: `${selectedOrder.productCode !== '-' ? selectedOrder.productCode + ' - ' : ''}${itemSummary}`,
|
||||||
floor: (opts.floor as string) || '-',
|
floor: (opts.floor as string) || '-',
|
||||||
code: (opts.code as string) || '-',
|
code: (opts.code as string) || '-',
|
||||||
width: (opts.width as number) || 0,
|
width: (opts.width as number) || 0,
|
||||||
@@ -774,7 +774,7 @@ export default function WorkerScreen() {
|
|||||||
workOrderId: selectedOrder.id,
|
workOrderId: selectedOrder.id,
|
||||||
itemNo: 1,
|
itemNo: 1,
|
||||||
itemCode: selectedOrder.orderNo || '-',
|
itemCode: selectedOrder.orderNo || '-',
|
||||||
itemName: selectedOrder.productName || '-',
|
itemName: `${selectedOrder.productCode !== '-' ? selectedOrder.productCode + ' - ' : ''}${selectedOrder.productName || '-'}`,
|
||||||
floor: '-',
|
floor: '-',
|
||||||
code: '-',
|
code: '-',
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -940,6 +940,7 @@ export default function WorkerScreen() {
|
|||||||
const syntheticOrder: WorkOrder = {
|
const syntheticOrder: WorkOrder = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
orderNo: item.itemCode,
|
orderNo: item.itemCode,
|
||||||
|
productCode: item.itemCode,
|
||||||
productName: item.itemName,
|
productName: item.itemName,
|
||||||
processCode: item.processType,
|
processCode: item.processType,
|
||||||
processName: PROCESS_TAB_LABELS[item.processType],
|
processName: PROCESS_TAB_LABELS[item.processType],
|
||||||
@@ -999,6 +1000,7 @@ export default function WorkerScreen() {
|
|||||||
const syntheticOrder: WorkOrder = {
|
const syntheticOrder: WorkOrder = {
|
||||||
id: mockItem.id,
|
id: mockItem.id,
|
||||||
orderNo: mockItem.itemCode,
|
orderNo: mockItem.itemCode,
|
||||||
|
productCode: mockItem.itemCode,
|
||||||
productName: mockItem.itemName,
|
productName: mockItem.itemName,
|
||||||
processCode: mockItem.processType,
|
processCode: mockItem.processType,
|
||||||
processName: PROCESS_TAB_LABELS[mockItem.processType],
|
processName: PROCESS_TAB_LABELS[mockItem.processType],
|
||||||
@@ -1239,6 +1241,7 @@ export default function WorkerScreen() {
|
|||||||
return {
|
return {
|
||||||
id: mockItem.id,
|
id: mockItem.id,
|
||||||
orderNo: mockItem.itemCode,
|
orderNo: mockItem.itemCode,
|
||||||
|
productCode: mockItem.itemCode,
|
||||||
productName: mockItem.itemName,
|
productName: mockItem.itemName,
|
||||||
processCode: mockItem.processType,
|
processCode: mockItem.processType,
|
||||||
processName: PROCESS_TAB_LABELS[mockItem.processType],
|
processName: PROCESS_TAB_LABELS[mockItem.processType],
|
||||||
|
|||||||
Reference in New Issue
Block a user