ERP-node/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardComponen...

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>
);
}