Files
vexplor/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardComponent.tsx
kjs 199fa60ef5 feat: add BOM materials retrieval functionality
- Implemented a new API endpoint for retrieving BOM materials based on item codes, enhancing the ability to manage and view component materials.
- Added necessary SQL queries to fetch BOM details, ensuring that the data is filtered by company code for multi-tenancy support.
- Updated frontend components to integrate BOM materials fetching, allowing for better visibility and management of materials in the process workflow.

These changes aim to streamline the management of BOM materials, facilitating better tracking and organization within the application.
2026-03-20 13:46:30 +09:00

256 lines
8.4 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;
screenId?: number | string;
}
export function ProcessWorkStandardComponent({
config: configProp,
isPreview,
screenId,
}: ProcessWorkStandardComponentProps) {
const resolvedConfig = useMemo(() => {
const merged = {
...configProp,
};
if (merged.itemListMode === "registered" && !merged.screenCode && screenId) {
merged.screenCode = `screen_${screenId}`;
}
return merged;
}, [configProp, screenId]);
const config: ProcessWorkStandardConfig = useMemo(
() => ({
...defaultConfig,
...resolvedConfig,
dataSource: { ...defaultConfig.dataSource, ...resolvedConfig?.dataSource },
phases: resolvedConfig?.phases?.length
? resolvedConfig.phases
: defaultConfig.phases,
detailTypes: resolvedConfig?.detailTypes?.length
? resolvedConfig.detailTypes
: defaultConfig.detailTypes,
}),
[resolvedConfig]
);
const {
items,
routings,
workItems,
selectedWorkItemIdByPhase,
selectedDetailsByPhase,
selection,
loading,
isRegisteredMode,
loadItems,
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(() => {
loadItems();
}, [loadItems]);
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) => loadItems(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={selectedWorkItemIdByPhase[phase.key] || null}
selectedWorkItemDetails={selectedDetailsByPhase[phase.key] || []}
detailTypes={config.detailTypes}
readonly={config.readonly}
selectedItemCode={selection.itemCode || undefined}
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>
);
}