214 lines
7.7 KiB
TypeScript
214 lines
7.7 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,
|
|
selectedWorkItemIdByPhase,
|
|
selectedDetailsByPhase,
|
|
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, phaseKey: string) => {
|
|
fetchWorkItemDetails(workItemId, phaseKey);
|
|
},
|
|
[fetchWorkItemDetails],
|
|
);
|
|
|
|
const handleInit = useCallback(() => {
|
|
fetchItems();
|
|
}, [fetchItems]);
|
|
|
|
const splitRatio = config.splitRatio || 30;
|
|
|
|
if (isPreview) {
|
|
return (
|
|
<div className="border-muted-foreground/20 bg-muted/10 flex h-full items-center justify-center rounded-lg border-2 border-dashed p-4">
|
|
<div className="text-center">
|
|
<ClipboardCheck className="text-muted-foreground/50 mx-auto mb-2 h-8 w-8" />
|
|
<p className="text-muted-foreground text-sm font-medium">공정 작업기준</p>
|
|
<p className="text-muted-foreground/70 mt-1 text-xs">{sortedPhases.map((p) => p.label).join(" / ")}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-background flex h-full flex-col overflow-hidden rounded-lg border">
|
|
{/* 메인 콘텐츠 */}
|
|
<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="text-muted-foreground mt-0.5 flex items-center gap-2 text-xs">
|
|
<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={selectedWorkItemIdByPhase[phase.key] || null}
|
|
selectedWorkItemDetails={selectedDetailsByPhase[phase.key] || []}
|
|
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="text-muted-foreground/30 mb-3 h-12 w-12" />
|
|
<p className="text-muted-foreground text-sm font-medium">좌측에서 품목과 공정을 선택하세요</p>
|
|
<p className="text-muted-foreground/70 mt-1 text-xs">
|
|
품목을 펼쳐 라우팅별 공정을 선택하면 작업기준을 관리할 수 있습니다
|
|
</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>
|
|
);
|
|
}
|