fix(WEB): 작업지시 상세/생성 화면 버그 수정
- 작업지시 상세: 우선순위, 비고(메모), 수정버튼 표시 추가 - 공정 진행 상태: pending/unassigned 시 첫 단계 미완료로 표시 - 생산지시 생성: 담당자 선택 시 users.id 사용하도록 수정 - Employee 타입에 userId 필드 추가 - 시스템 계정이 있는 직원만 담당자로 선택 가능
This commit is contained in:
@@ -27,7 +27,9 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Factory, ArrowLeft, BarChart3, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Factory, ArrowLeft, BarChart3, CheckCircle2, AlertCircle, User } from "lucide-react";
|
||||
import { PageLayout } from "@/components/organisms/PageLayout";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -48,6 +50,8 @@ import {
|
||||
type CreateProductionOrderData,
|
||||
} from "@/components/orders/actions";
|
||||
import { getProcessList } from "@/components/process-management/actions";
|
||||
import { getEmployees } from "@/components/hr/EmployeeManagement/actions";
|
||||
import type { Employee } from "@/components/hr/EmployeeManagement/types";
|
||||
import type { Process } from "@/types/process";
|
||||
import { formatAmount } from "@/utils/formatAmount";
|
||||
|
||||
@@ -348,25 +352,28 @@ export default function ProductionOrderCreatePage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [order, setOrder] = useState<Order | null>(null);
|
||||
const [processes, setProcesses] = useState<Process[]>([]);
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 우선순위 상태
|
||||
const [selectedPriority, setSelectedPriority] = useState<PriorityLevel>("normal");
|
||||
const [selectedAssignee, setSelectedAssignee] = useState<string>("");
|
||||
const [memo, setMemo] = useState("");
|
||||
|
||||
// 성공 다이얼로그 상태
|
||||
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
|
||||
const [generatedWorkOrders, setGeneratedWorkOrders] = useState<Array<{ workOrderNo: string; processName?: string }>>([]);
|
||||
|
||||
// 수주 데이터 및 공정 목록 로드
|
||||
// 수주 데이터, 공정 목록, 직원 목록 로드
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// 수주 정보와 공정 목록을 병렬로 로드
|
||||
const [orderResult, processResult] = await Promise.all([
|
||||
// 수주 정보, 공정 목록, 직원 목록을 병렬로 로드
|
||||
const [orderResult, processResult, employeeResult] = await Promise.all([
|
||||
getOrderById(orderId),
|
||||
getProcessList({ status: "사용중" }),
|
||||
getEmployees({ status: "active", per_page: 100 }),
|
||||
]);
|
||||
|
||||
if (orderResult.success && orderResult.data) {
|
||||
@@ -378,6 +385,10 @@ export default function ProductionOrderCreatePage() {
|
||||
if (processResult.success && processResult.data) {
|
||||
setProcesses(processResult.data.items);
|
||||
}
|
||||
|
||||
if (employeeResult.data) {
|
||||
setEmployees(employeeResult.data);
|
||||
}
|
||||
} catch {
|
||||
setError("서버 오류가 발생했습니다.");
|
||||
} finally {
|
||||
@@ -400,6 +411,12 @@ export default function ProductionOrderCreatePage() {
|
||||
const handleConfirm = async () => {
|
||||
if (!order) return;
|
||||
|
||||
// 담당자 필수 검증
|
||||
if (!selectedAssignee) {
|
||||
setError("담당자를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -421,6 +438,7 @@ export default function ProductionOrderCreatePage() {
|
||||
|
||||
const productionData: CreateProductionOrderData = {
|
||||
priority: selectedPriority,
|
||||
assigneeId: parseInt(selectedAssignee, 10),
|
||||
memo: memo || undefined,
|
||||
processIds: allProcessIds.length > 0 ? allProcessIds : undefined,
|
||||
};
|
||||
@@ -708,6 +726,31 @@ export default function ProductionOrderCreatePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 담당자 선택 */}
|
||||
<div>
|
||||
<Label className="text-sm font-medium flex items-center gap-2 mb-2">
|
||||
<User className="h-4 w-4" />
|
||||
담당자 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={selectedAssignee} onValueChange={setSelectedAssignee}>
|
||||
<SelectTrigger className={cn("w-full md:w-[300px]", !selectedAssignee && "border-red-300")}>
|
||||
<SelectValue placeholder="담당자를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{employees
|
||||
.filter((emp) => emp.userId) // 시스템 계정이 있는 직원만
|
||||
.map((emp) => (
|
||||
<SelectItem key={emp.id} value={String(emp.userId)}>
|
||||
{emp.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{!selectedAssignee && (
|
||||
<p className="text-xs text-red-500 mt-1">담당자는 필수 선택 항목입니다.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 메모 */}
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">메모</p>
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface UserInfo {
|
||||
|
||||
export interface Employee {
|
||||
id: string;
|
||||
userId?: number; // users 테이블 ID (담당자 선택 등에 사용)
|
||||
|
||||
// 기본 정보 (필수)
|
||||
name: string;
|
||||
|
||||
@@ -196,6 +196,7 @@ export function transformApiToFrontend(api: EmployeeApiData): Employee {
|
||||
|
||||
return {
|
||||
id: String(api.id),
|
||||
userId: api.user_id, // users 테이블 ID
|
||||
name: api.user?.name || api.display_name || '',
|
||||
employeeCode: extra.employee_code,
|
||||
residentNumber: extra.resident_number,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2, Undo2 } from 'lucide-react';
|
||||
import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2, Undo2, Pencil } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -388,6 +388,10 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
되돌리기
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => router.push(`/production/work-orders/${orderId}/edit`)}>
|
||||
<Pencil className="w-4 h-4 mr-1.5" />
|
||||
수정
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsWorkLogOpen(true)}>
|
||||
<FileText className="w-4 h-4 mr-1.5" />
|
||||
작업일지
|
||||
@@ -442,6 +446,18 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
: order.assignee}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">우선순위</p>
|
||||
<p className="font-medium">
|
||||
{order.priority === 1 ? '1 (긴급)' : order.priority === 5 ? '5 (일반)' : order.priority || '-'}
|
||||
</p>
|
||||
</div>
|
||||
{order.note && (
|
||||
<div className="col-span-4 mt-2 pt-4 border-t">
|
||||
<p className="text-sm text-muted-foreground mb-1">비고</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{order.note}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -449,20 +449,21 @@ function transformBendingDetail(api: WorkOrderBendingDetailApi): BendingDetail[]
|
||||
}
|
||||
|
||||
// 상태 → 단계 변환
|
||||
// 작업 진행 표시용: 실제 작업이 시작되기 전에는 -1 (아무것도 완료/진행 없음)
|
||||
function getStatusStep(status: WorkOrderStatus): number {
|
||||
const stepMap: Record<WorkOrderStatus, number> = {
|
||||
unassigned: 0,
|
||||
pending: 1,
|
||||
waiting: 2,
|
||||
in_progress: 3,
|
||||
completed: 4,
|
||||
shipped: 5,
|
||||
unassigned: -1, // 미배정: 아무 단계도 시작 안함
|
||||
pending: -1, // 대기: 배정됨, 작업 미시작
|
||||
waiting: 0, // 준비중: 첫 단계 진행 중
|
||||
in_progress: 0, // 작업중: 첫 단계 진행 중 (TODO: 실제 단계 추적 필요)
|
||||
completed: 999, // 완료: 모든 단계 완료
|
||||
shipped: 999, // 출하: 모든 단계 완료
|
||||
};
|
||||
return stepMap[status] || 0;
|
||||
return stepMap[status] ?? -1;
|
||||
}
|
||||
|
||||
// Frontend → API 변환 (등록/수정용)
|
||||
export function transformFrontendToApi(data: Partial<WorkOrder> & { processId?: number }): Record<string, unknown> {
|
||||
export function transformFrontendToApi(data: Partial<WorkOrder> & { processId?: number; assigneeIds?: number[] }): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (data.projectName !== undefined) result.project_name = data.projectName;
|
||||
@@ -471,6 +472,8 @@ export function transformFrontendToApi(data: Partial<WorkOrder> & { processId?:
|
||||
if (data.scheduledDate !== undefined) result.scheduled_date = data.scheduledDate;
|
||||
if (data.dueDate !== undefined && data.scheduledDate === undefined) result.scheduled_date = data.dueDate;
|
||||
if (data.note !== undefined) result.memo = data.note;
|
||||
if (data.priority !== undefined) result.priority = data.priority;
|
||||
if (data.assigneeIds !== undefined) result.assignee_ids = data.assigneeIds;
|
||||
|
||||
// items 변환
|
||||
if (data.items !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user