Merge remote-tracking branch 'origin/master'
# Conflicts: # src/components/production/WorkOrders/WorkOrderList.tsx
This commit is contained in:
@@ -11,6 +11,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, FileText, X, Edit2, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
@@ -455,11 +456,9 @@ export function WorkOrderCreate() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>출고예정일 *</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DatePicker
|
||||
value={formData.shipmentDate}
|
||||
onChange={(e) => setFormData({ ...formData, shipmentDate: e.target.value })}
|
||||
className="bg-white"
|
||||
onChange={(date) => setFormData({ ...formData, shipmentDate: date })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 md:p-6">
|
||||
<h3 className="font-semibold mb-4">기본 정보</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-4 md:gap-x-6 gap-y-4">
|
||||
{/* 1행: 작업번호 | 수주일 | 공정구분 | 로트번호 */}
|
||||
{/* 1행: 작업번호 | 수주일 | 공정 | 구분 */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">작업번호</p>
|
||||
<p className="font-medium truncate">{order.workOrderNo}</p>
|
||||
@@ -379,15 +379,19 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
<p className="font-medium">{order.salesOrderDate || '-'}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">공정구분</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">공정</p>
|
||||
<p className="font-medium">{order.processName}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">구분</p>
|
||||
<p className="font-medium">-</p>
|
||||
</div>
|
||||
|
||||
{/* 2행: 로트번호 | 수주처 | 현장명 | 수주 담당자 */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">로트번호</p>
|
||||
<p className="font-medium truncate">{order.lotNo}</p>
|
||||
</div>
|
||||
|
||||
{/* 2행: 수주처 | 현장명 | 수주 담당자 | 담당자 연락처 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">수주처</p>
|
||||
<p className="font-medium">{order.client}</p>
|
||||
@@ -400,12 +404,12 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
<p className="text-sm text-muted-foreground mb-1">수주 담당자</p>
|
||||
<p className="font-medium">-</p>
|
||||
</div>
|
||||
|
||||
{/* 3행: 담당자 연락처 | 출고예정일 | 틀수 | 우선순위 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">담당자 연락처</p>
|
||||
<p className="font-medium">-</p>
|
||||
</div>
|
||||
|
||||
{/* 3행: 출고예정일 | 틀수 | 우선순위 | 부서 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">출고예정일</p>
|
||||
<p className="font-medium">{order.shipmentDate || '-'}</p>
|
||||
@@ -418,12 +422,12 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
<p className="text-sm text-muted-foreground mb-1">우선순위</p>
|
||||
<p className="font-medium">{order.priorityLabel || '-'}</p>
|
||||
</div>
|
||||
|
||||
{/* 4행: 부서 | 생산 담당자 | 상태 | 비고 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">부서</p>
|
||||
<p className="font-medium">{order.department || '-'}</p>
|
||||
</div>
|
||||
|
||||
{/* 4행: 생산 담당자 | 상태 | 비고 (colspan 2) */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">생산 담당자</p>
|
||||
<p className="font-medium">
|
||||
@@ -438,7 +442,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
{WORK_ORDER_STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">비고</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{order.note || '-'}</p>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -365,7 +366,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
<section className="bg-amber-50 border border-amber-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">기본 정보</h3>
|
||||
<div className="grid grid-cols-4 gap-x-6 gap-y-4">
|
||||
{/* 1행: 작업번호(읽기) | 수주일(읽기) | 공정구분(셀렉트) | 로트번호(읽기) */}
|
||||
{/* 1행: 작업번호(읽기) | 수주일(읽기) | 공정(셀렉트) | 구분(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">작업번호</Label>
|
||||
<Input value={workOrder?.workOrderNo || '-'} disabled className="bg-muted" />
|
||||
@@ -375,7 +376,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
<Input value={workOrder?.salesOrderDate || '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">공정구분 *</Label>
|
||||
<Label className="text-sm text-muted-foreground">공정 *</Label>
|
||||
<Select
|
||||
value={formData.processId?.toString() || ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, processId: parseInt(value) })}
|
||||
@@ -393,12 +394,16 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">구분</Label>
|
||||
<Input value="-" disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
{/* 2행: 로트번호(읽기) | 수주처(읽기) | 현장명(입력) | 수주 담당자(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">로트번호</Label>
|
||||
<Input value={formData.orderNo || '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
{/* 2행: 수주처(읽기) | 현장명(입력) | 수주 담당자(읽기) | 담당자 연락처(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">수주처</Label>
|
||||
<Input value={formData.client} disabled className="bg-muted" />
|
||||
@@ -415,19 +420,17 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
<Label className="text-sm text-muted-foreground">수주 담당자</Label>
|
||||
<Input value="-" disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
{/* 3행: 담당자 연락처(읽기) | 출고예정일(입력) | 틀수(읽기) | 우선순위(셀렉트) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">담당자 연락처</Label>
|
||||
<Input value="-" disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
{/* 3행: 출고예정일(입력) | 틀수(읽기) | 우선순위(셀렉트) | 부서(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">출고예정일 *</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DatePicker
|
||||
value={formData.scheduledDate}
|
||||
onChange={(e) => setFormData({ ...formData, scheduledDate: e.target.value })}
|
||||
className="bg-white"
|
||||
onChange={(date) => setFormData({ ...formData, scheduledDate: date })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -452,12 +455,12 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 4행: 부서(읽기) | 생산 담당자(선택) | 상태(읽기) | 비고(입력) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">부서</Label>
|
||||
<Input value={workOrder?.department || '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
{/* 4행: 생산 담당자(선택) | 상태(읽기) | 비고(입력, colspan 2) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">생산 담당자</Label>
|
||||
<div
|
||||
@@ -475,7 +478,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
<Label className="text-sm text-muted-foreground">상태</Label>
|
||||
<Input value={workOrder ? (workOrder.status === 'waiting' ? '작업대기' : workOrder.status === 'in_progress' ? '작업중' : workOrder.status === 'completed' ? '작업완료' : workOrder.status) : '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">비고</Label>
|
||||
<Textarea
|
||||
value={formData.note}
|
||||
|
||||
@@ -57,12 +57,11 @@ export function WorkOrderList() {
|
||||
const router = useRouter();
|
||||
|
||||
// ===== 활성 탭 및 재공품 모달 =====
|
||||
const [activeTab, setActiveTab] = useState(TAB_ALL);
|
||||
const [activeTab, setActiveTab] = useState('screen');
|
||||
const [isWipModalOpen, setIsWipModalOpen] = useState(false);
|
||||
|
||||
// ===== 공정 목록 및 ID 매핑 (API에서 동적 로드) =====
|
||||
const [processList, setProcessList] = useState<ProcessOption[]>([]);
|
||||
const [processMap, setProcessMap] = useState<Record<string, number | string>>({});
|
||||
// ===== 공정 ID 매핑 (getProcessOptions) =====
|
||||
const [processMap, setProcessMap] = useState<Record<string, number>>({});
|
||||
const [processMapLoaded, setProcessMapLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -70,42 +69,17 @@ export function WorkOrderList() {
|
||||
try {
|
||||
const result = await getProcessOptions();
|
||||
if (result.success && result.data) {
|
||||
setProcessList(result.data);
|
||||
|
||||
// 공정 ID → 탭 value 매핑 (공정 ID를 탭 value로 사용)
|
||||
const map: Record<string, number | string> = {
|
||||
[TAB_ALL]: TAB_ALL, // 전체: 필터 없음
|
||||
[TAB_OTHER]: 'none', // 기타: process_id IS NULL
|
||||
};
|
||||
const map: Record<string, number> = {};
|
||||
result.data.forEach((process: ProcessOption) => {
|
||||
// 탭 value = 공정 ID (문자열)
|
||||
map[String(process.id)] = process.id;
|
||||
// process_name 또는 process_code로 탭 매핑
|
||||
const tabKeyByName = PROCESS_NAME_TO_TAB[process.processName];
|
||||
const tabKeyByCode = PROCESS_CODE_TO_TAB[process.processCode];
|
||||
const tabKey = tabKeyByName || tabKeyByCode;
|
||||
if (tabKey) {
|
||||
map[tabKey] = process.id;
|
||||
}
|
||||
});
|
||||
setProcessMap(map);
|
||||
|
||||
// 탭 카운트 조회 (전체 + 기타 + 각 공정)
|
||||
const tabKeys = [TAB_ALL, TAB_OTHER, ...result.data.map(p => String(p.id))];
|
||||
const countPromises = tabKeys.map(async (tabKey) => {
|
||||
let res;
|
||||
if (tabKey === TAB_ALL) {
|
||||
// 전체: processId 파라미터 없이
|
||||
res = await getWorkOrders({ page: 1, perPage: 1 });
|
||||
} else if (tabKey === TAB_OTHER) {
|
||||
// 기타: process_id = none (NULL)
|
||||
res = await getWorkOrders({ page: 1, perPage: 1, processId: 'none' as unknown as number });
|
||||
} else {
|
||||
// 공정별: 해당 공정 ID
|
||||
res = await getWorkOrders({ page: 1, perPage: 1, processId: Number(tabKey) });
|
||||
}
|
||||
return { tabKey, count: res.success ? res.pagination.total : 0 };
|
||||
});
|
||||
|
||||
const counts = await Promise.all(countPromises);
|
||||
const newTabCounts: Record<string, number> = {};
|
||||
counts.forEach(({ tabKey, count }) => {
|
||||
newTabCounts[tabKey] = count;
|
||||
});
|
||||
setTabCounts(newTabCounts);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
@@ -156,24 +130,20 @@ export function WorkOrderList() {
|
||||
router.push('/ko/production/work-orders?mode=new');
|
||||
}, [router]);
|
||||
|
||||
// ===== 탭 옵션 (전체 + 공정들 + 기타) — 카운트는 API 응답으로 동적 업데이트 =====
|
||||
// ===== 탭 옵션 (공정 기반 3개) — 카운트는 API 응답으로 동적 업데이트 =====
|
||||
const [tabCounts, setTabCounts] = useState<Record<string, number>>({
|
||||
[TAB_ALL]: 0,
|
||||
[TAB_OTHER]: 0,
|
||||
screen: 0,
|
||||
slat: 0,
|
||||
bending: 0,
|
||||
});
|
||||
|
||||
const tabs: TabOption[] = useMemo(
|
||||
() => [
|
||||
{ value: TAB_ALL, label: '전체', count: tabCounts[TAB_ALL] },
|
||||
// 공정 목록에서 동적 생성
|
||||
...processList.map((process) => ({
|
||||
value: String(process.id),
|
||||
label: process.processName,
|
||||
count: tabCounts[String(process.id)] || 0,
|
||||
})),
|
||||
{ value: TAB_OTHER, label: '기타', count: tabCounts[TAB_OTHER] },
|
||||
{ value: 'screen', label: '스크린 공정', count: tabCounts.screen },
|
||||
{ value: 'slat', label: '슬랫 공정', count: tabCounts.slat },
|
||||
{ value: 'bending', label: '절곡 공정', count: tabCounts.bending },
|
||||
],
|
||||
[tabCounts, processList]
|
||||
[tabCounts]
|
||||
);
|
||||
|
||||
// ===== 통계 카드 6개 (기획서 기반) =====
|
||||
@@ -235,31 +205,29 @@ export function WorkOrderList() {
|
||||
actions: {
|
||||
getList: async (params?: ListParams) => {
|
||||
try {
|
||||
// 탭 value 확인
|
||||
const tabValue = params?.tab || TAB_ALL;
|
||||
// 탭 → processId 매핑
|
||||
const tabValue = params?.tab || 'screen';
|
||||
setActiveTab(tabValue);
|
||||
const processId = processMap[tabValue];
|
||||
|
||||
// 해당 공정이 DB에 없으면 빈 목록 반환
|
||||
if (!processId) {
|
||||
return {
|
||||
success: true,
|
||||
data: [],
|
||||
totalCount: 0,
|
||||
totalPages: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 필터 값 추출
|
||||
const statusFilter = params?.filters?.status as string | undefined;
|
||||
const priorityFilter = params?.filters?.priority as string | undefined;
|
||||
|
||||
// processId 결정
|
||||
let processIdParam: number | 'none' | undefined;
|
||||
if (tabValue === TAB_ALL) {
|
||||
// 전체: processId 파라미터 없음
|
||||
processIdParam = undefined;
|
||||
} else if (tabValue === TAB_OTHER) {
|
||||
// 기타: process_id IS NULL
|
||||
processIdParam = 'none';
|
||||
} else {
|
||||
// 공정별: 해당 공정 ID
|
||||
processIdParam = Number(tabValue);
|
||||
}
|
||||
|
||||
const result = await getWorkOrders({
|
||||
page: params?.page || 1,
|
||||
perPage: params?.pageSize || ITEMS_PER_PAGE,
|
||||
processId: processIdParam as number | undefined,
|
||||
processId,
|
||||
status: statusFilter && statusFilter !== 'all'
|
||||
? (statusFilter as WorkOrderStatus)
|
||||
: undefined,
|
||||
@@ -306,8 +274,8 @@ export function WorkOrderList() {
|
||||
{ key: 'lotNo', label: '로트번호', className: 'min-w-[120px]' },
|
||||
{ key: 'client', label: '수주처', className: 'min-w-[120px]' },
|
||||
{ key: 'projectName', label: '현장명', className: 'min-w-[150px]' },
|
||||
{ key: 'processName', label: '공정', className: 'w-[80px]' },
|
||||
{ key: 'itemCount', label: '틀수', className: 'w-[70px] text-center' },
|
||||
{ key: 'shutterCount', label: '틀수', className: 'w-[70px] text-center' },
|
||||
{ key: 'category', label: '구분', className: 'w-[80px]' },
|
||||
{ key: 'status', label: '상태', className: 'w-[90px]' },
|
||||
{ key: 'priority', label: '우선순위', className: 'w-[80px]' },
|
||||
{ key: 'department', label: '부서', className: 'w-[90px]' },
|
||||
@@ -401,18 +369,8 @@ export function WorkOrderList() {
|
||||
<TableCell>{item.lotNo}</TableCell>
|
||||
<TableCell>{item.client}</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate">{item.projectName}</TableCell>
|
||||
<TableCell>
|
||||
{item.processName && item.processName !== '-' ? (
|
||||
<Badge variant="outline" className="text-xs font-medium">
|
||||
{item.processName}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
미지정
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{item.items?.length || item.shutterCount || '-'}</TableCell>
|
||||
<TableCell className="text-center">{item.shutterCount ?? '-'}</TableCell>
|
||||
<TableCell>{'-'}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${WORK_ORDER_STATUS_COLORS[item.status]} border-0`}>
|
||||
{WORK_ORDER_STATUS_LABELS[item.status]}
|
||||
|
||||
Reference in New Issue
Block a user