242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState, useMemo, useCallback } from "react";
|
||
|
|
import { Save, Loader2, ClipboardCheck } from "lucide-react";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { ProcessWorkStandardConfig, WorkItem } from "./types";
|
||
|
|
import { defaultConfig } from "./config";
|
||
|
|
import { useProcessWorkStandard } from "./hooks/useProcessWorkStandard";
|
||
|
|
import { ItemProcessSelector } from "./components/ItemProcessSelector";
|
||
|
|
import { WorkPhaseSection } from "./components/WorkPhaseSection";
|
||
|
|
import { WorkItemAddModal } from "./components/WorkItemAddModal";
|
||
|
|
|
||
|
|
interface ProcessWorkStandardComponentProps {
|
||
|
|
config?: Partial<ProcessWorkStandardConfig>;
|
||
|
|
formData?: Record<string, any>;
|
||
|
|
isPreview?: boolean;
|
||
|
|
tableName?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ProcessWorkStandardComponent({
|
||
|
|
config: configProp,
|
||
|
|
isPreview,
|
||
|
|
}: ProcessWorkStandardComponentProps) {
|
||
|
|
const config: ProcessWorkStandardConfig = useMemo(
|
||
|
|
() => ({
|
||
|
|
...defaultConfig,
|
||
|
|
...configProp,
|
||
|
|
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource },
|
||
|
|
phases: configProp?.phases?.length
|
||
|
|
? configProp.phases
|
||
|
|
: defaultConfig.phases,
|
||
|
|
detailTypes: configProp?.detailTypes?.length
|
||
|
|
? configProp.detailTypes
|
||
|
|
: defaultConfig.detailTypes,
|
||
|
|
}),
|
||
|
|
[configProp]
|
||
|
|
);
|
||
|
|
|
||
|
|
const {
|
||
|
|
items,
|
||
|
|
routings,
|
||
|
|
workItems,
|
||
|
|
selectedWorkItemDetails,
|
||
|
|
selectedWorkItemId,
|
||
|
|
selection,
|
||
|
|
loading,
|
||
|
|
fetchItems,
|
||
|
|
selectItem,
|
||
|
|
selectProcess,
|
||
|
|
fetchWorkItemDetails,
|
||
|
|
createWorkItem,
|
||
|
|
updateWorkItem,
|
||
|
|
deleteWorkItem,
|
||
|
|
createDetail,
|
||
|
|
updateDetail,
|
||
|
|
deleteDetail,
|
||
|
|
} = useProcessWorkStandard(config);
|
||
|
|
|
||
|
|
// 모달 상태
|
||
|
|
const [modalOpen, setModalOpen] = useState(false);
|
||
|
|
const [modalPhaseKey, setModalPhaseKey] = useState("");
|
||
|
|
const [editingItem, setEditingItem] = useState<WorkItem | null>(null);
|
||
|
|
|
||
|
|
// phase별 작업 항목 그룹핑
|
||
|
|
const workItemsByPhase = useMemo(() => {
|
||
|
|
const map: Record<string, WorkItem[]> = {};
|
||
|
|
for (const phase of config.phases) {
|
||
|
|
map[phase.key] = workItems.filter((wi) => wi.work_phase === phase.key);
|
||
|
|
}
|
||
|
|
return map;
|
||
|
|
}, [workItems, config.phases]);
|
||
|
|
|
||
|
|
const sortedPhases = useMemo(
|
||
|
|
() => [...config.phases].sort((a, b) => a.sortOrder - b.sortOrder),
|
||
|
|
[config.phases]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleAddWorkItem = useCallback((phaseKey: string) => {
|
||
|
|
setModalPhaseKey(phaseKey);
|
||
|
|
setEditingItem(null);
|
||
|
|
setModalOpen(true);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleEditWorkItem = useCallback((item: WorkItem) => {
|
||
|
|
setModalPhaseKey(item.work_phase);
|
||
|
|
setEditingItem(item);
|
||
|
|
setModalOpen(true);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleModalSave = useCallback(
|
||
|
|
async (data: Parameters<typeof createWorkItem>[0]) => {
|
||
|
|
if (editingItem) {
|
||
|
|
await updateWorkItem(editingItem.id, {
|
||
|
|
title: data.title,
|
||
|
|
is_required: data.is_required,
|
||
|
|
description: data.description,
|
||
|
|
} as any);
|
||
|
|
} else {
|
||
|
|
await createWorkItem(data);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[editingItem, createWorkItem, updateWorkItem]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleSelectWorkItem = useCallback(
|
||
|
|
(workItemId: string) => {
|
||
|
|
fetchWorkItemDetails(workItemId);
|
||
|
|
},
|
||
|
|
[fetchWorkItemDetails]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleInit = useCallback(() => {
|
||
|
|
fetchItems();
|
||
|
|
}, [fetchItems]);
|
||
|
|
|
||
|
|
const splitRatio = config.splitRatio || 30;
|
||
|
|
|
||
|
|
if (isPreview) {
|
||
|
|
return (
|
||
|
|
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/20 bg-muted/10 p-4">
|
||
|
|
<div className="text-center">
|
||
|
|
<ClipboardCheck className="mx-auto mb-2 h-8 w-8 text-muted-foreground/50" />
|
||
|
|
<p className="text-sm font-medium text-muted-foreground">
|
||
|
|
공정 작업기준
|
||
|
|
</p>
|
||
|
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
||
|
|
{sortedPhases.map((p) => p.label).join(" / ")}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-full flex-col overflow-hidden rounded-lg border bg-background">
|
||
|
|
{/* 메인 콘텐츠 */}
|
||
|
|
<div className="flex flex-1 overflow-hidden">
|
||
|
|
{/* 좌측 패널 */}
|
||
|
|
<div style={{ width: `${splitRatio}%` }} className="shrink-0 overflow-hidden">
|
||
|
|
<ItemProcessSelector
|
||
|
|
title={config.leftPanelTitle || "품목 및 공정 선택"}
|
||
|
|
items={items}
|
||
|
|
routings={routings}
|
||
|
|
selection={selection}
|
||
|
|
onSearch={(keyword) => fetchItems(keyword)}
|
||
|
|
onSelectItem={selectItem}
|
||
|
|
onSelectProcess={selectProcess}
|
||
|
|
onInit={handleInit}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 우측 패널 */}
|
||
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||
|
|
{/* 우측 헤더 */}
|
||
|
|
{selection.routingDetailId ? (
|
||
|
|
<>
|
||
|
|
<div className="flex items-center justify-between border-b px-4 py-2.5">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-base font-bold">
|
||
|
|
{selection.itemName} - {selection.processName}
|
||
|
|
</h2>
|
||
|
|
<div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
||
|
|
<span>품목: {selection.itemCode}</span>
|
||
|
|
<span>공정: {selection.processName}</span>
|
||
|
|
<span>버전: {selection.routingVersionName}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{!config.readonly && (
|
||
|
|
<Button
|
||
|
|
variant="default"
|
||
|
|
size="sm"
|
||
|
|
className="gap-1.5"
|
||
|
|
disabled={loading}
|
||
|
|
>
|
||
|
|
{loading ? (
|
||
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Save className="h-3.5 w-3.5" />
|
||
|
|
)}
|
||
|
|
전체 저장
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 작업 단계별 섹션 */}
|
||
|
|
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||
|
|
{sortedPhases.map((phase) => (
|
||
|
|
<WorkPhaseSection
|
||
|
|
key={phase.key}
|
||
|
|
phase={phase}
|
||
|
|
items={workItemsByPhase[phase.key] || []}
|
||
|
|
selectedWorkItemId={selectedWorkItemId}
|
||
|
|
selectedWorkItemDetails={selectedWorkItemDetails}
|
||
|
|
detailTypes={config.detailTypes}
|
||
|
|
readonly={config.readonly}
|
||
|
|
onSelectWorkItem={handleSelectWorkItem}
|
||
|
|
onAddWorkItem={handleAddWorkItem}
|
||
|
|
onEditWorkItem={handleEditWorkItem}
|
||
|
|
onDeleteWorkItem={deleteWorkItem}
|
||
|
|
onCreateDetail={createDetail}
|
||
|
|
onUpdateDetail={updateDetail}
|
||
|
|
onDeleteDetail={deleteDetail}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<div className="flex flex-1 flex-col items-center justify-center text-center">
|
||
|
|
<ClipboardCheck className="mb-3 h-12 w-12 text-muted-foreground/30" />
|
||
|
|
<p className="text-sm font-medium text-muted-foreground">
|
||
|
|
좌측에서 품목과 공정을 선택하세요
|
||
|
|
</p>
|
||
|
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
||
|
|
품목을 펼쳐 라우팅별 공정을 선택하면 작업기준을 관리할 수
|
||
|
|
있습니다
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 작업 항목 추가/수정 모달 */}
|
||
|
|
<WorkItemAddModal
|
||
|
|
open={modalOpen}
|
||
|
|
onClose={() => {
|
||
|
|
setModalOpen(false);
|
||
|
|
setEditingItem(null);
|
||
|
|
}}
|
||
|
|
onSave={handleModalSave}
|
||
|
|
phaseKey={modalPhaseKey}
|
||
|
|
phaseLabel={
|
||
|
|
config.phases.find((p) => p.key === modalPhaseKey)?.label || ""
|
||
|
|
}
|
||
|
|
detailTypes={config.detailTypes}
|
||
|
|
editItem={editingItem}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|