feat(WEB): 부실채권, 재고, 입고, 수주 UI 개선
- BadDebtCollection 액션/타입 리팩토링 - ReceivingProcessDialog 입고처리 개선 - StockStatusList 재고현황 UI 개선 - OrderSalesDetailView 수주 상세 수정 - UniversalListPage 범용 리스트 개선 - production-order 페이지 수정
This commit is contained in:
@@ -27,9 +27,9 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
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 { AssigneeSelectModal } from "@/components/production/WorkOrders/AssigneeSelectModal";
|
||||
import { PageLayout } from "@/components/organisms/PageLayout";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -50,8 +50,6 @@ 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";
|
||||
|
||||
@@ -352,28 +350,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 [selectedAssignees, setSelectedAssignees] = useState<string[]>([]);
|
||||
const [assigneeNames, setAssigneeNames] = useState<string[]>([]);
|
||||
const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false);
|
||||
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, employeeResult] = await Promise.all([
|
||||
// 수주 정보, 공정 목록을 병렬로 로드
|
||||
const [orderResult, processResult] = await Promise.all([
|
||||
getOrderById(orderId),
|
||||
getProcessList({ status: "사용중" }),
|
||||
getEmployees({ status: "active", per_page: 100 }),
|
||||
]);
|
||||
|
||||
if (orderResult.success && orderResult.data) {
|
||||
@@ -385,10 +383,6 @@ export default function ProductionOrderCreatePage() {
|
||||
if (processResult.success && processResult.data) {
|
||||
setProcesses(processResult.data.items);
|
||||
}
|
||||
|
||||
if (employeeResult.data) {
|
||||
setEmployees(employeeResult.data);
|
||||
}
|
||||
} catch {
|
||||
setError("서버 오류가 발생했습니다.");
|
||||
} finally {
|
||||
@@ -412,7 +406,7 @@ export default function ProductionOrderCreatePage() {
|
||||
if (!order) return;
|
||||
|
||||
// 담당자 필수 검증
|
||||
if (!selectedAssignee) {
|
||||
if (selectedAssignees.length === 0) {
|
||||
setError("담당자를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
@@ -436,9 +430,14 @@ export default function ProductionOrderCreatePage() {
|
||||
.map((g) => parseInt(g.process.id, 10))
|
||||
.filter((id) => !isNaN(id));
|
||||
|
||||
// 담당자 ID 배열 변환 (string[] → number[])
|
||||
const assigneeIds = selectedAssignees
|
||||
.map(id => parseInt(id, 10))
|
||||
.filter(id => !isNaN(id));
|
||||
|
||||
const productionData: CreateProductionOrderData = {
|
||||
priority: selectedPriority,
|
||||
assigneeId: parseInt(selectedAssignee, 10),
|
||||
assigneeIds: assigneeIds.length > 0 ? assigneeIds : undefined,
|
||||
memo: memo || undefined,
|
||||
processIds: allProcessIds.length > 0 ? allProcessIds : undefined,
|
||||
};
|
||||
@@ -726,27 +725,26 @@ 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>
|
||||
담당자 (다중선택 가능) <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 && (
|
||||
<div
|
||||
onClick={() => setIsAssigneeModalOpen(true)}
|
||||
className={cn(
|
||||
"flex min-h-10 w-full md:w-[400px] cursor-pointer items-center rounded-md border bg-white px-3 py-2 text-sm ring-offset-background hover:bg-accent/50",
|
||||
selectedAssignees.length === 0 ? "border-red-300" : "border-input"
|
||||
)}
|
||||
>
|
||||
{assigneeNames.length > 0 ? (
|
||||
<span>{assigneeNames.join(', ')}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">담당자를 선택하세요 (팀/개인)</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedAssignees.length === 0 && (
|
||||
<p className="text-xs text-red-500 mt-1">담당자는 필수 선택 항목입니다.</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -1024,6 +1022,17 @@ export default function ProductionOrderCreatePage() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 담당자 선택 모달 */}
|
||||
<AssigneeSelectModal
|
||||
open={isAssigneeModalOpen}
|
||||
onOpenChange={setIsAssigneeModalOpen}
|
||||
selectedIds={selectedAssignees}
|
||||
onSelect={(ids, names) => {
|
||||
setSelectedAssignees(ids);
|
||||
setAssigneeNames(names);
|
||||
}}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -494,7 +494,7 @@ export default function OrderManagementSalesPage() {
|
||||
<TableCell>{order.siteName}</TableCell>
|
||||
<TableCell>{getOrderStatusBadge(order.status)}</TableCell>
|
||||
<TableCell>{order.expectedShipDate || "-"}</TableCell>
|
||||
<TableCell>{order.deliveryMethod || "-"}</TableCell>
|
||||
<TableCell>{order.deliveryMethodLabel || "-"}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
{isSelected && (
|
||||
<div className="flex gap-1">
|
||||
@@ -564,7 +564,7 @@ export default function OrderManagementSalesPage() {
|
||||
<InfoField label="현장명" value={order.siteName} />
|
||||
<InfoField label="견적번호" value={order.quoteNumber} />
|
||||
<InfoField label="출고예정일" value={order.expectedShipDate || "-"} />
|
||||
<InfoField label="배송방식" value={order.deliveryMethod || "-"} />
|
||||
<InfoField label="배송방식" value={order.deliveryMethodLabel || "-"} />
|
||||
<InfoField
|
||||
label="금액"
|
||||
value={formatAmount(order.amount)}
|
||||
|
||||
Reference in New Issue
Block a user