463 lines
15 KiB
TypeScript
463 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import "./styles.css";
|
|
|
|
import {
|
|
AppState,
|
|
ModalState,
|
|
PanelState,
|
|
StatusType,
|
|
ProductionType,
|
|
WorkOrder,
|
|
WorkStep,
|
|
Equipment,
|
|
Process,
|
|
} from "./types";
|
|
import { WORK_ORDERS, EQUIPMENTS, PROCESSES, WORK_STEP_TEMPLATES, STATUS_TEXT } from "./data";
|
|
|
|
import { PopHeader } from "./PopHeader";
|
|
import { PopStatusTabs } from "./PopStatusTabs";
|
|
import { PopWorkCard } from "./PopWorkCard";
|
|
import { PopBottomNav } from "./PopBottomNav";
|
|
import { PopEquipmentModal } from "./PopEquipmentModal";
|
|
import { PopProcessModal } from "./PopProcessModal";
|
|
import { PopAcceptModal } from "./PopAcceptModal";
|
|
import { PopSettingsModal } from "./PopSettingsModal";
|
|
import { PopProductionPanel } from "./PopProductionPanel";
|
|
|
|
export function PopApp() {
|
|
// 앱 상태
|
|
const [appState, setAppState] = useState<AppState>({
|
|
currentStatus: "waiting",
|
|
selectedEquipment: null,
|
|
selectedProcess: null,
|
|
selectedWorkOrder: null,
|
|
showMyWorkOnly: false,
|
|
currentWorkSteps: [],
|
|
currentStepIndex: 0,
|
|
currentProductionType: "work-order",
|
|
selectionMode: "single",
|
|
completionAction: "close",
|
|
acceptTargetWorkOrder: null,
|
|
acceptQuantity: 0,
|
|
theme: "dark",
|
|
});
|
|
|
|
// 모달 상태
|
|
const [modalState, setModalState] = useState<ModalState>({
|
|
equipment: false,
|
|
process: false,
|
|
accept: false,
|
|
settings: false,
|
|
});
|
|
|
|
// 패널 상태
|
|
const [panelState, setPanelState] = useState<PanelState>({
|
|
production: false,
|
|
});
|
|
|
|
// 현재 시간 (hydration 에러 방지를 위해 초기값 null)
|
|
const [currentDateTime, setCurrentDateTime] = useState<Date | null>(null);
|
|
const [isClient, setIsClient] = useState(false);
|
|
|
|
// 작업지시 목록 (상태 변경을 위해 로컬 상태로 관리)
|
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>(WORK_ORDERS);
|
|
|
|
// 클라이언트 마운트 확인 및 시계 업데이트
|
|
useEffect(() => {
|
|
setIsClient(true);
|
|
setCurrentDateTime(new Date());
|
|
|
|
const timer = setInterval(() => {
|
|
setCurrentDateTime(new Date());
|
|
}, 1000);
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
// 로컬 스토리지에서 설정 로드
|
|
useEffect(() => {
|
|
const savedSelectionMode = localStorage.getItem("selectionMode") as "single" | "multi" | null;
|
|
const savedCompletionAction = localStorage.getItem("completionAction") as "close" | "stay" | null;
|
|
const savedTheme = localStorage.getItem("popTheme") as "dark" | "light" | null;
|
|
|
|
setAppState((prev) => ({
|
|
...prev,
|
|
selectionMode: savedSelectionMode || "single",
|
|
completionAction: savedCompletionAction || "close",
|
|
theme: savedTheme || "dark",
|
|
}));
|
|
}, []);
|
|
|
|
// 상태별 카운트 계산
|
|
const getStatusCounts = useCallback(() => {
|
|
const myProcessId = appState.selectedProcess?.id;
|
|
|
|
let waitingCount = 0;
|
|
let pendingAcceptCount = 0;
|
|
let inProgressCount = 0;
|
|
let completedCount = 0;
|
|
|
|
workOrders.forEach((wo) => {
|
|
if (!wo.processFlow) return;
|
|
|
|
const myProcessIndex = myProcessId
|
|
? wo.processFlow.findIndex((step) => step.id === myProcessId)
|
|
: -1;
|
|
|
|
if (wo.status === "completed") {
|
|
completedCount++;
|
|
} else if (wo.status === "in-progress" && wo.accepted) {
|
|
inProgressCount++;
|
|
} else if (myProcessIndex >= 0) {
|
|
const currentProcessIndex = wo.currentProcessIndex || 0;
|
|
const myStep = wo.processFlow[myProcessIndex];
|
|
|
|
if (currentProcessIndex < myProcessIndex) {
|
|
waitingCount++;
|
|
} else if (currentProcessIndex === myProcessIndex && myStep.status !== "completed") {
|
|
pendingAcceptCount++;
|
|
} else if (myStep.status === "completed") {
|
|
completedCount++;
|
|
}
|
|
} else {
|
|
if (wo.status === "waiting") waitingCount++;
|
|
else if (wo.status === "in-progress") inProgressCount++;
|
|
}
|
|
});
|
|
|
|
return { waitingCount, pendingAcceptCount, inProgressCount, completedCount };
|
|
}, [workOrders, appState.selectedProcess]);
|
|
|
|
// 필터링된 작업 목록
|
|
const getFilteredWorkOrders = useCallback(() => {
|
|
const myProcessId = appState.selectedProcess?.id;
|
|
let filtered: WorkOrder[] = [];
|
|
|
|
workOrders.forEach((wo) => {
|
|
if (!wo.processFlow) return;
|
|
|
|
const myProcessIndex = myProcessId
|
|
? wo.processFlow.findIndex((step) => step.id === myProcessId)
|
|
: -1;
|
|
const currentProcessIndex = wo.currentProcessIndex || 0;
|
|
const myStep = myProcessIndex >= 0 ? wo.processFlow[myProcessIndex] : null;
|
|
|
|
switch (appState.currentStatus) {
|
|
case "waiting":
|
|
if (myProcessIndex >= 0 && currentProcessIndex < myProcessIndex) {
|
|
filtered.push(wo);
|
|
} else if (!myProcessId && wo.status === "waiting") {
|
|
filtered.push(wo);
|
|
}
|
|
break;
|
|
|
|
case "pending-accept":
|
|
if (
|
|
myProcessIndex >= 0 &&
|
|
currentProcessIndex === myProcessIndex &&
|
|
myStep &&
|
|
myStep.status !== "completed" &&
|
|
!wo.accepted
|
|
) {
|
|
filtered.push(wo);
|
|
}
|
|
break;
|
|
|
|
case "in-progress":
|
|
if (wo.accepted && wo.status === "in-progress") {
|
|
filtered.push(wo);
|
|
} else if (!myProcessId && wo.status === "in-progress") {
|
|
filtered.push(wo);
|
|
}
|
|
break;
|
|
|
|
case "completed":
|
|
if (wo.status === "completed") {
|
|
filtered.push(wo);
|
|
} else if (myStep && myStep.status === "completed") {
|
|
filtered.push(wo);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
// 내 작업만 보기 필터
|
|
if (appState.showMyWorkOnly && myProcessId) {
|
|
filtered = filtered.filter((wo) => {
|
|
const mySteps = wo.processFlow.filter((step) => step.id === myProcessId);
|
|
if (mySteps.length === 0) return false;
|
|
return !mySteps.every((step) => step.status === "completed");
|
|
});
|
|
}
|
|
|
|
return filtered;
|
|
}, [workOrders, appState.currentStatus, appState.selectedProcess, appState.showMyWorkOnly]);
|
|
|
|
// 상태 탭 변경
|
|
const handleStatusChange = (status: StatusType) => {
|
|
setAppState((prev) => ({ ...prev, currentStatus: status }));
|
|
};
|
|
|
|
// 생산 유형 변경
|
|
const handleProductionTypeChange = (type: ProductionType) => {
|
|
setAppState((prev) => ({ ...prev, currentProductionType: type }));
|
|
};
|
|
|
|
// 내 작업만 보기 토글
|
|
const handleMyWorkToggle = () => {
|
|
setAppState((prev) => ({ ...prev, showMyWorkOnly: !prev.showMyWorkOnly }));
|
|
};
|
|
|
|
// 테마 토글
|
|
const handleThemeToggle = () => {
|
|
const newTheme = appState.theme === "dark" ? "light" : "dark";
|
|
setAppState((prev) => ({ ...prev, theme: newTheme }));
|
|
localStorage.setItem("popTheme", newTheme);
|
|
};
|
|
|
|
// 모달 열기/닫기
|
|
const openModal = (type: keyof ModalState) => {
|
|
setModalState((prev) => ({ ...prev, [type]: true }));
|
|
};
|
|
|
|
const closeModal = (type: keyof ModalState) => {
|
|
setModalState((prev) => ({ ...prev, [type]: false }));
|
|
};
|
|
|
|
// 설비 선택
|
|
const handleEquipmentSelect = (equipment: Equipment) => {
|
|
setAppState((prev) => ({
|
|
...prev,
|
|
selectedEquipment: equipment,
|
|
// 공정이 1개면 자동 선택
|
|
selectedProcess:
|
|
equipment.processIds.length === 1
|
|
? PROCESSES.find((p) => p.id === equipment.processIds[0]) || null
|
|
: null,
|
|
}));
|
|
};
|
|
|
|
// 공정 선택
|
|
const handleProcessSelect = (process: Process) => {
|
|
setAppState((prev) => ({ ...prev, selectedProcess: process }));
|
|
};
|
|
|
|
// 작업 접수 모달 열기
|
|
const handleOpenAcceptModal = (workOrder: WorkOrder) => {
|
|
const acceptedQty = workOrder.acceptedQuantity || 0;
|
|
const remainingQty = workOrder.orderQuantity - acceptedQty;
|
|
|
|
setAppState((prev) => ({
|
|
...prev,
|
|
acceptTargetWorkOrder: workOrder,
|
|
acceptQuantity: remainingQty,
|
|
}));
|
|
openModal("accept");
|
|
};
|
|
|
|
// 접수 확인
|
|
const handleConfirmAccept = (quantity: number) => {
|
|
if (!appState.acceptTargetWorkOrder) return;
|
|
|
|
setWorkOrders((prev) =>
|
|
prev.map((wo) => {
|
|
if (wo.id === appState.acceptTargetWorkOrder!.id) {
|
|
const previousAccepted = wo.acceptedQuantity || 0;
|
|
const newAccepted = previousAccepted + quantity;
|
|
return {
|
|
...wo,
|
|
acceptedQuantity: newAccepted,
|
|
remainingQuantity: wo.orderQuantity - newAccepted,
|
|
accepted: true,
|
|
status: "in-progress" as const,
|
|
isPartialAccept: newAccepted < wo.orderQuantity,
|
|
};
|
|
}
|
|
return wo;
|
|
})
|
|
);
|
|
|
|
closeModal("accept");
|
|
setAppState((prev) => ({
|
|
...prev,
|
|
acceptTargetWorkOrder: null,
|
|
acceptQuantity: 0,
|
|
}));
|
|
};
|
|
|
|
// 접수 취소
|
|
const handleCancelAccept = (workOrderId: string) => {
|
|
setWorkOrders((prev) =>
|
|
prev.map((wo) => {
|
|
if (wo.id === workOrderId) {
|
|
return {
|
|
...wo,
|
|
accepted: false,
|
|
acceptedQuantity: 0,
|
|
remainingQuantity: wo.orderQuantity,
|
|
isPartialAccept: false,
|
|
status: "waiting" as const,
|
|
};
|
|
}
|
|
return wo;
|
|
})
|
|
);
|
|
};
|
|
|
|
// 생산진행 패널 열기
|
|
const handleOpenProductionPanel = (workOrder: WorkOrder) => {
|
|
const template = WORK_STEP_TEMPLATES[workOrder.process] || WORK_STEP_TEMPLATES["default"];
|
|
const workSteps: WorkStep[] = template.map((step) => ({
|
|
...step,
|
|
status: "pending" as const,
|
|
startTime: null,
|
|
endTime: null,
|
|
data: {},
|
|
}));
|
|
|
|
setAppState((prev) => ({
|
|
...prev,
|
|
selectedWorkOrder: workOrder,
|
|
currentWorkSteps: workSteps,
|
|
currentStepIndex: 0,
|
|
}));
|
|
setPanelState((prev) => ({ ...prev, production: true }));
|
|
};
|
|
|
|
// 생산진행 패널 닫기
|
|
const handleCloseProductionPanel = () => {
|
|
setPanelState((prev) => ({ ...prev, production: false }));
|
|
setAppState((prev) => ({
|
|
...prev,
|
|
selectedWorkOrder: null,
|
|
currentWorkSteps: [],
|
|
currentStepIndex: 0,
|
|
}));
|
|
};
|
|
|
|
// 설정 저장
|
|
const handleSaveSettings = (selectionMode: "single" | "multi", completionAction: "close" | "stay") => {
|
|
setAppState((prev) => ({ ...prev, selectionMode, completionAction }));
|
|
localStorage.setItem("selectionMode", selectionMode);
|
|
localStorage.setItem("completionAction", completionAction);
|
|
closeModal("settings");
|
|
};
|
|
|
|
const statusCounts = getStatusCounts();
|
|
const filteredWorkOrders = getFilteredWorkOrders();
|
|
|
|
return (
|
|
<div className={`pop-container ${appState.theme === "light" ? "light" : ""}`}>
|
|
<div className="pop-app">
|
|
{/* 헤더 */}
|
|
<PopHeader
|
|
currentDateTime={currentDateTime || new Date()}
|
|
productionType={appState.currentProductionType}
|
|
selectedEquipment={appState.selectedEquipment}
|
|
selectedProcess={appState.selectedProcess}
|
|
showMyWorkOnly={appState.showMyWorkOnly}
|
|
theme={appState.theme}
|
|
onProductionTypeChange={handleProductionTypeChange}
|
|
onEquipmentClick={() => openModal("equipment")}
|
|
onProcessClick={() => openModal("process")}
|
|
onMyWorkToggle={handleMyWorkToggle}
|
|
onSearchClick={() => {
|
|
/* 조회 */
|
|
}}
|
|
onSettingsClick={() => openModal("settings")}
|
|
onThemeToggle={handleThemeToggle}
|
|
/>
|
|
|
|
{/* 상태 탭 */}
|
|
<PopStatusTabs
|
|
currentStatus={appState.currentStatus}
|
|
counts={statusCounts}
|
|
onStatusChange={handleStatusChange}
|
|
/>
|
|
|
|
{/* 메인 콘텐츠 */}
|
|
<div className="pop-main-content">
|
|
{filteredWorkOrders.length === 0 ? (
|
|
<div className="pop-empty-state">
|
|
<div className="pop-empty-state-text">작업이 없습니다</div>
|
|
<div className="pop-empty-state-desc">
|
|
{appState.currentStatus === "waiting" && "대기 중인 작업이 없습니다"}
|
|
{appState.currentStatus === "pending-accept" && "접수 대기 작업이 없습니다"}
|
|
{appState.currentStatus === "in-progress" && "진행 중인 작업이 없습니다"}
|
|
{appState.currentStatus === "completed" && "완료된 작업이 없습니다"}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="pop-work-list">
|
|
{filteredWorkOrders.map((workOrder) => (
|
|
<PopWorkCard
|
|
key={workOrder.id}
|
|
workOrder={workOrder}
|
|
currentStatus={appState.currentStatus}
|
|
selectedProcess={appState.selectedProcess}
|
|
onAccept={() => handleOpenAcceptModal(workOrder)}
|
|
onCancelAccept={() => handleCancelAccept(workOrder.id)}
|
|
onStartProduction={() => handleOpenProductionPanel(workOrder)}
|
|
onClick={() => handleOpenProductionPanel(workOrder)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 하단 네비게이션 */}
|
|
<PopBottomNav />
|
|
</div>
|
|
|
|
{/* 모달들 */}
|
|
<PopEquipmentModal
|
|
isOpen={modalState.equipment}
|
|
equipments={EQUIPMENTS}
|
|
selectedEquipment={appState.selectedEquipment}
|
|
onSelect={handleEquipmentSelect}
|
|
onClose={() => closeModal("equipment")}
|
|
/>
|
|
|
|
<PopProcessModal
|
|
isOpen={modalState.process}
|
|
selectedEquipment={appState.selectedEquipment}
|
|
selectedProcess={appState.selectedProcess}
|
|
processes={PROCESSES}
|
|
onSelect={handleProcessSelect}
|
|
onClose={() => closeModal("process")}
|
|
/>
|
|
|
|
<PopAcceptModal
|
|
isOpen={modalState.accept}
|
|
workOrder={appState.acceptTargetWorkOrder}
|
|
quantity={appState.acceptQuantity}
|
|
onQuantityChange={(qty) => setAppState((prev) => ({ ...prev, acceptQuantity: qty }))}
|
|
onConfirm={handleConfirmAccept}
|
|
onClose={() => closeModal("accept")}
|
|
/>
|
|
|
|
<PopSettingsModal
|
|
isOpen={modalState.settings}
|
|
selectionMode={appState.selectionMode}
|
|
completionAction={appState.completionAction}
|
|
onSave={handleSaveSettings}
|
|
onClose={() => closeModal("settings")}
|
|
/>
|
|
|
|
{/* 생산진행 패널 */}
|
|
<PopProductionPanel
|
|
isOpen={panelState.production}
|
|
workOrder={appState.selectedWorkOrder}
|
|
workSteps={appState.currentWorkSteps}
|
|
currentStepIndex={appState.currentStepIndex}
|
|
currentDateTime={currentDateTime || new Date()}
|
|
onStepChange={(index) => setAppState((prev) => ({ ...prev, currentStepIndex: index }))}
|
|
onStepsUpdate={(steps) => setAppState((prev) => ({ ...prev, currentWorkSteps: steps }))}
|
|
onClose={handleCloseProductionPanel}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|