|
|
|
|
@@ -13,6 +13,13 @@
|
|
|
|
|
* - 하단 고정 버튼 (작업일지보기 / 중간검사하기)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** 품목명에서 길이(mm) 추출: "가이드레일(측면) 본체(철재) 2438mm" → 2438 */
|
|
|
|
|
function extractLengthFromName(name?: string | null): number {
|
|
|
|
|
if (!name) return 0;
|
|
|
|
|
const m = name.match(/(\d{3,5})\s*mm/i);
|
|
|
|
|
return m ? parseInt(m[1], 10) : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
|
|
|
import dynamic from 'next/dynamic';
|
|
|
|
|
import { useSidebarCollapsed } from '@/stores/menuStore';
|
|
|
|
|
@@ -47,6 +54,7 @@ import type { InspectionTemplateData } from './types';
|
|
|
|
|
import { getProcessList } from '@/components/process-management/actions';
|
|
|
|
|
import type { InspectionSetting, InspectionScope, Process } from '@/types/process';
|
|
|
|
|
import type { WorkOrder } from '../ProductionDashboard/types';
|
|
|
|
|
import { BENDING_STEP_MAP, extractBendingTypeCode } from '../WorkOrders/types';
|
|
|
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
|
|
|
import type {
|
|
|
|
|
WorkerStats,
|
|
|
|
|
@@ -90,6 +98,7 @@ interface SidebarOrder {
|
|
|
|
|
shutterCount: number;
|
|
|
|
|
priority: 'urgent' | 'priority' | 'normal';
|
|
|
|
|
subType?: 'slat' | 'jointbar' | 'bending' | 'wip';
|
|
|
|
|
bdCode?: string; // 재공품 BD- 코드 (예: BD-ST-24)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SUB_TYPE_TAGS: Record<string, { label: string; className: string }> = {
|
|
|
|
|
@@ -363,14 +372,23 @@ export default function WorkerScreen() {
|
|
|
|
|
|
|
|
|
|
// ===== API WorkOrders → SidebarOrder 변환 =====
|
|
|
|
|
const apiSidebarOrders: SidebarOrder[] = useMemo(() => {
|
|
|
|
|
return filteredWorkOrders.map((wo) => ({
|
|
|
|
|
id: wo.id,
|
|
|
|
|
siteName: wo.projectName || wo.client || '-',
|
|
|
|
|
date: wo.dueDate || (wo.createdAt ? wo.createdAt.slice(0, 10) : '-'),
|
|
|
|
|
quantity: wo.quantity,
|
|
|
|
|
shutterCount: wo.shutterCount || 0,
|
|
|
|
|
priority: (wo.isUrgent ? 'urgent' : (wo.priority <= 3 ? 'priority' : 'normal')) as SidebarOrder['priority'],
|
|
|
|
|
}));
|
|
|
|
|
return filteredWorkOrders.map((wo) => {
|
|
|
|
|
const isWip = wo.projectName === '재고생산' || wo.salesOrderNo?.startsWith('STK');
|
|
|
|
|
// 재공품: 첫 번째 item의 BD- 코드 추출
|
|
|
|
|
const bdCode = isWip
|
|
|
|
|
? wo.nodeGroups?.flatMap(g => g.items).find(it => it.itemCode?.startsWith('BD-'))?.itemCode ?? undefined
|
|
|
|
|
: undefined;
|
|
|
|
|
return {
|
|
|
|
|
id: wo.id,
|
|
|
|
|
siteName: wo.projectName || wo.client || '-',
|
|
|
|
|
date: wo.dueDate || (wo.createdAt ? wo.createdAt.slice(0, 10) : '-'),
|
|
|
|
|
quantity: wo.quantity,
|
|
|
|
|
shutterCount: wo.shutterCount || 0,
|
|
|
|
|
priority: (wo.isUrgent ? 'urgent' : (wo.priority <= 3 ? 'priority' : 'normal')) as SidebarOrder['priority'],
|
|
|
|
|
subType: isWip ? 'wip' as const : undefined,
|
|
|
|
|
bdCode,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}, [filteredWorkOrders]);
|
|
|
|
|
|
|
|
|
|
// ===== 탭 변경/데이터 로드 시 최상위 우선순위 작업 자동 선택 =====
|
|
|
|
|
@@ -480,7 +498,42 @@ export default function WorkerScreen() {
|
|
|
|
|
const nodeKey = group.nodeId != null ? String(group.nodeId) : `unassigned-${index}`;
|
|
|
|
|
const firstItem = group.items[0];
|
|
|
|
|
const firstItemId = firstItem?.id as number | undefined;
|
|
|
|
|
const steps: WorkStepData[] = stepsTemplate.map((st, si) => {
|
|
|
|
|
|
|
|
|
|
// 절곡 공정: BD 코드에 따라 필요한 단계만 필터링
|
|
|
|
|
let itemStepsTemplate = stepsTemplate;
|
|
|
|
|
if (activeProcessTabKey === 'bending') {
|
|
|
|
|
const itemCodes = group.items.map((it) => it.itemCode).filter(Boolean) as string[];
|
|
|
|
|
if (itemCodes.length > 0) {
|
|
|
|
|
const neededKeys = new Set<string>();
|
|
|
|
|
let hasNonBd = false;
|
|
|
|
|
for (const code of itemCodes) {
|
|
|
|
|
const typeCode = extractBendingTypeCode(code);
|
|
|
|
|
if (typeCode && BENDING_STEP_MAP[typeCode]) {
|
|
|
|
|
BENDING_STEP_MAP[typeCode].forEach((k) => neededKeys.add(k));
|
|
|
|
|
} else {
|
|
|
|
|
hasNonBd = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!hasNonBd && neededKeys.size > 0) {
|
|
|
|
|
// 단계명 매핑: guide_rail→가이드레일 제작, case→케이스 제작, bottom_finish→하단마감재 제작, inspection→검사/중간검사
|
|
|
|
|
const stepNameMap: Record<string, string[]> = {
|
|
|
|
|
guide_rail: ['가이드레일 제작', '가이드레일'],
|
|
|
|
|
case: ['케이스 제작', '케이스'],
|
|
|
|
|
bottom_finish: ['하단마감재 제작', '하단마감재'],
|
|
|
|
|
inspection: ['검사', '중간검사'],
|
|
|
|
|
};
|
|
|
|
|
const allowedNames = new Set<string>();
|
|
|
|
|
// 자재투입은 항상 포함
|
|
|
|
|
allowedNames.add('자재투입');
|
|
|
|
|
for (const key of neededKeys) {
|
|
|
|
|
(stepNameMap[key] || []).forEach((n) => allowedNames.add(n));
|
|
|
|
|
}
|
|
|
|
|
itemStepsTemplate = stepsTemplate.filter((st) => allowedNames.has(st.name));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const steps: WorkStepData[] = itemStepsTemplate.map((st, si) => {
|
|
|
|
|
const stepKey = `${selectedOrder.id}-${nodeKey}-${st.name}`;
|
|
|
|
|
return enrichStep(st, `${selectedOrder.id}-${nodeKey}-step-${si}`, stepKey, firstItemId);
|
|
|
|
|
});
|
|
|
|
|
@@ -529,8 +582,8 @@ export default function WorkerScreen() {
|
|
|
|
|
itemName: selectedOrder.productCode !== '-' ? selectedOrder.productCode : itemSummary,
|
|
|
|
|
floor: (opts.floor as string) || '-',
|
|
|
|
|
code: (opts.code as string) || '-',
|
|
|
|
|
width: (opts.width as number) || 0,
|
|
|
|
|
height: (opts.height as number) || 0,
|
|
|
|
|
width: (opts.bending_width as number) || (opts.width as number) || 0,
|
|
|
|
|
height: (opts.height as number) || extractLengthFromName(firstItem?.itemName) || 0,
|
|
|
|
|
quantity: group.totalQuantity,
|
|
|
|
|
processType: activeProcessTabKey,
|
|
|
|
|
steps,
|
|
|
|
|
@@ -556,6 +609,11 @@ export default function WorkerScreen() {
|
|
|
|
|
detailParts: (bi.detail_parts || []).map(dp => ({ partName: dp.part_name, material: dp.material, barcyInfo: dp.barcy_info })),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
// BD- 코드 추출 (재공품/절곡품 참조용)
|
|
|
|
|
const bdItemCode = firstItem?.itemCode;
|
|
|
|
|
if (bdItemCode?.startsWith('BD-')) {
|
|
|
|
|
workItem.bdCode = bdItemCode;
|
|
|
|
|
}
|
|
|
|
|
if (opts.is_wip) {
|
|
|
|
|
workItem.isWip = true;
|
|
|
|
|
const wi = opts.wip_info as { specification: string; length_quantity: string } | undefined;
|
|
|
|
|
@@ -1617,6 +1675,27 @@ function SidebarContent({
|
|
|
|
|
onSelectOrder,
|
|
|
|
|
apiOrders,
|
|
|
|
|
}: SidebarContentProps) {
|
|
|
|
|
const [sidebarTab, setSidebarTab] = useState<'orders' | 'wip'>('orders');
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
|
|
|
|
|
// 수주목록 / 재공품 분리
|
|
|
|
|
const regularOrders = useMemo(() =>
|
|
|
|
|
apiOrders.filter((o) => o.subType !== 'wip'),
|
|
|
|
|
[apiOrders]);
|
|
|
|
|
const wipOrders = useMemo(() =>
|
|
|
|
|
apiOrders.filter((o) => o.subType === 'wip'),
|
|
|
|
|
[apiOrders]);
|
|
|
|
|
|
|
|
|
|
// 검색 필터링
|
|
|
|
|
const displayOrders = useMemo(() => {
|
|
|
|
|
const source = sidebarTab === 'orders' ? regularOrders : wipOrders;
|
|
|
|
|
if (!searchTerm.trim()) return source;
|
|
|
|
|
const q = searchTerm.toLowerCase();
|
|
|
|
|
return source.filter((o) =>
|
|
|
|
|
o.siteName.toLowerCase().includes(q) || o.date.includes(q)
|
|
|
|
|
);
|
|
|
|
|
}, [sidebarTab, regularOrders, wipOrders, searchTerm]);
|
|
|
|
|
|
|
|
|
|
const renderOrders = (orders: SidebarOrder[]) => (
|
|
|
|
|
<>
|
|
|
|
|
{PRIORITY_GROUPS.map((group) => {
|
|
|
|
|
@@ -1648,6 +1727,9 @@ function SidebarContent({
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{order.bdCode && (
|
|
|
|
|
<p className="text-[10px] text-blue-600 font-mono mt-0.5">{order.bdCode}</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex items-center justify-between mt-1 text-gray-500">
|
|
|
|
|
<span>{order.date}</span>
|
|
|
|
|
<span>{order.shutterCount}개소</span>
|
|
|
|
|
@@ -1664,12 +1746,48 @@ function SidebarContent({
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h3 className="text-sm font-semibold text-gray-900">수주 목록</h3>
|
|
|
|
|
{/* 탭: 수주목록 / 재공품 */}
|
|
|
|
|
<div className="flex border-b">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => { setSidebarTab('orders'); setSearchTerm(''); }}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex-1 text-sm font-medium py-2 border-b-2 transition-colors',
|
|
|
|
|
sidebarTab === 'orders'
|
|
|
|
|
? 'border-primary text-primary'
|
|
|
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
수주목록 ({regularOrders.length})
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => { setSidebarTab('wip'); setSearchTerm(''); }}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex-1 text-sm font-medium py-2 border-b-2 transition-colors',
|
|
|
|
|
sidebarTab === 'wip'
|
|
|
|
|
? 'border-primary text-primary'
|
|
|
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
재공품 ({wipOrders.length})
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{apiOrders.length > 0 ? (
|
|
|
|
|
renderOrders(apiOrders)
|
|
|
|
|
{/* 검색 */}
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="현장명 검색..."
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{displayOrders.length > 0 ? (
|
|
|
|
|
renderOrders(displayOrders)
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-xs text-gray-400 text-center py-4">수주 데이터가 없습니다.</p>
|
|
|
|
|
<p className="text-xs text-gray-400 text-center py-4">
|
|
|
|
|
{searchTerm ? '검색 결과가 없습니다.' : '데이터가 없습니다.'}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|