453 lines
16 KiB
TypeScript
453 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Search } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { WorkItemDetail, DetailTypeDefinition, InspectionStandard } from "../types";
|
|
import { InspectionStandardLookup } from "./InspectionStandardLookup";
|
|
|
|
interface DetailFormModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (data: Partial<WorkItemDetail>) => void;
|
|
detailTypes: DetailTypeDefinition[];
|
|
editData?: WorkItemDetail | null;
|
|
mode: "add" | "edit";
|
|
}
|
|
|
|
const LOOKUP_TARGETS = [
|
|
{ value: "equipment", label: "설비정보" },
|
|
{ value: "material", label: "자재정보" },
|
|
{ value: "worker", label: "작업자정보" },
|
|
{ value: "tool", label: "공구정보" },
|
|
{ value: "document", label: "문서정보" },
|
|
];
|
|
|
|
const INPUT_TYPES = [
|
|
{ value: "text", label: "텍스트" },
|
|
{ value: "number", label: "숫자" },
|
|
{ value: "date", label: "날짜" },
|
|
{ value: "textarea", label: "장문텍스트" },
|
|
{ value: "select", label: "선택형" },
|
|
];
|
|
|
|
export function DetailFormModal({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
detailTypes,
|
|
editData,
|
|
mode,
|
|
}: DetailFormModalProps) {
|
|
const [formData, setFormData] = useState<Partial<WorkItemDetail>>({});
|
|
const [inspectionLookupOpen, setInspectionLookupOpen] = useState(false);
|
|
const [selectedInspection, setSelectedInspection] = useState<InspectionStandard | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
if (mode === "edit" && editData) {
|
|
setFormData({ ...editData });
|
|
if (editData.inspection_code) {
|
|
setSelectedInspection({
|
|
id: "",
|
|
inspection_code: editData.inspection_code,
|
|
inspection_item: editData.content || "",
|
|
inspection_method: editData.inspection_method || "",
|
|
unit: editData.unit || "",
|
|
lower_limit: editData.lower_limit || "",
|
|
upper_limit: editData.upper_limit || "",
|
|
});
|
|
}
|
|
} else {
|
|
setFormData({
|
|
detail_type: detailTypes[0]?.value || "",
|
|
content: "",
|
|
is_required: "Y",
|
|
});
|
|
setSelectedInspection(null);
|
|
}
|
|
}
|
|
}, [open, mode, editData, detailTypes]);
|
|
|
|
const updateField = (field: string, value: any) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const handleInspectionSelect = (item: InspectionStandard) => {
|
|
setSelectedInspection(item);
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
inspection_code: item.inspection_code,
|
|
content: item.inspection_item,
|
|
inspection_method: item.inspection_method,
|
|
unit: item.unit,
|
|
lower_limit: item.lower_limit || "",
|
|
upper_limit: item.upper_limit || "",
|
|
}));
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (!formData.detail_type) return;
|
|
|
|
const type = formData.detail_type;
|
|
|
|
if (type === "check" && !formData.content?.trim()) return;
|
|
if (type === "inspect" && !formData.content?.trim()) return;
|
|
if (type === "procedure" && !formData.content?.trim()) return;
|
|
if (type === "input" && !formData.content?.trim()) return;
|
|
if (type === "info" && !formData.lookup_target) return;
|
|
|
|
const submitData = { ...formData };
|
|
|
|
if (type === "info" && !submitData.content?.trim()) {
|
|
const targetLabel = LOOKUP_TARGETS.find(t => t.value === submitData.lookup_target)?.label || submitData.lookup_target;
|
|
submitData.content = `${targetLabel} 조회`;
|
|
}
|
|
|
|
onSubmit(submitData);
|
|
onClose();
|
|
};
|
|
|
|
const currentType = formData.detail_type || "";
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">
|
|
상세 항목 {mode === "add" ? "추가" : "수정"}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
상세 항목의 유형을 선택하고 내용을 입력하세요
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* 유형 선택 */}
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">
|
|
유형 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select
|
|
value={currentType}
|
|
onValueChange={(v) => {
|
|
updateField("detail_type", v);
|
|
setSelectedInspection(null);
|
|
setFormData((prev) => ({
|
|
detail_type: v,
|
|
is_required: prev.is_required || "Y",
|
|
}));
|
|
}}
|
|
>
|
|
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{detailTypes.map((t) => (
|
|
<SelectItem key={t.value} value={t.value}>
|
|
{t.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 체크리스트 */}
|
|
{currentType === "check" && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">
|
|
체크 내용 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
value={formData.content || ""}
|
|
onChange={(e) => updateField("content", e.target.value)}
|
|
placeholder="예: 전원 상태 확인"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 검사항목 */}
|
|
{currentType === "inspect" && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">
|
|
검사기준 선택 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<div className="mt-1 flex gap-2">
|
|
<Select value="_placeholder" disabled>
|
|
<SelectTrigger className="h-8 flex-1 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue>
|
|
{selectedInspection
|
|
? `${selectedInspection.inspection_code} - ${selectedInspection.inspection_item}`
|
|
: "검사기준을 선택하세요"}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="_placeholder">선택</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
variant="secondary"
|
|
className="h-8 shrink-0 gap-1 text-xs sm:h-10 sm:text-sm"
|
|
onClick={() => setInspectionLookupOpen(true)}
|
|
>
|
|
<Search className="h-3.5 w-3.5" />
|
|
조회
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedInspection && (
|
|
<div className="rounded border bg-muted/30 p-3">
|
|
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
|
선택된 검사기준 정보
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
|
<p>
|
|
<strong>검사코드:</strong> {selectedInspection.inspection_code}
|
|
</p>
|
|
<p>
|
|
<strong>검사항목:</strong> {selectedInspection.inspection_item}
|
|
</p>
|
|
<p>
|
|
<strong>검사방법:</strong> {selectedInspection.inspection_method || "-"}
|
|
</p>
|
|
<p>
|
|
<strong>단위:</strong> {selectedInspection.unit || "-"}
|
|
</p>
|
|
<p>
|
|
<strong>하한값:</strong> {selectedInspection.lower_limit || "-"}
|
|
</p>
|
|
<p>
|
|
<strong>상한값:</strong> {selectedInspection.upper_limit || "-"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">
|
|
검사 항목명 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
value={formData.content || ""}
|
|
onChange={(e) => updateField("content", e.target.value)}
|
|
placeholder="예: 외경 치수"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">검사 방법</Label>
|
|
<Input
|
|
value={formData.inspection_method || ""}
|
|
onChange={(e) => updateField("inspection_method", e.target.value)}
|
|
placeholder="예: 마이크로미터"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">단위</Label>
|
|
<Input
|
|
value={formData.unit || ""}
|
|
onChange={(e) => updateField("unit", e.target.value)}
|
|
placeholder="예: mm"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">하한값</Label>
|
|
<Input
|
|
value={formData.lower_limit || ""}
|
|
onChange={(e) => updateField("lower_limit", e.target.value)}
|
|
placeholder="예: 7.95"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">상한값</Label>
|
|
<Input
|
|
value={formData.upper_limit || ""}
|
|
onChange={(e) => updateField("upper_limit", e.target.value)}
|
|
placeholder="예: 8.05"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 작업절차 */}
|
|
{currentType === "procedure" && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">
|
|
작업 내용 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
value={formData.content || ""}
|
|
onChange={(e) => updateField("content", e.target.value)}
|
|
placeholder="예: 자재 투입"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">소요 시간 (분)</Label>
|
|
<Input
|
|
type="number"
|
|
value={formData.duration_minutes ?? ""}
|
|
onChange={(e) =>
|
|
updateField(
|
|
"duration_minutes",
|
|
e.target.value ? Number(e.target.value) : undefined
|
|
)
|
|
}
|
|
placeholder="예: 5"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 직접입력 */}
|
|
{currentType === "input" && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">
|
|
입력 항목명 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
value={formData.content || ""}
|
|
onChange={(e) => updateField("content", e.target.value)}
|
|
placeholder="예: 작업자 의견"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">입력 타입</Label>
|
|
<Select
|
|
value={formData.input_type || "text"}
|
|
onValueChange={(v) => updateField("input_type", v)}
|
|
>
|
|
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{INPUT_TYPES.map((t) => (
|
|
<SelectItem key={t.value} value={t.value}>
|
|
{t.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 정보조회 */}
|
|
{currentType === "info" && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">
|
|
조회 대상 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select
|
|
value={formData.lookup_target || ""}
|
|
onValueChange={(v) => updateField("lookup_target", v)}
|
|
>
|
|
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{LOOKUP_TARGETS.map((t) => (
|
|
<SelectItem key={t.value} value={t.value}>
|
|
{t.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">표시 항목</Label>
|
|
<Input
|
|
value={formData.display_fields || ""}
|
|
onChange={(e) => updateField("display_fields", e.target.value)}
|
|
placeholder="예: 설비명, 설비코드"
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 필수 여부 (모든 유형 공통) */}
|
|
{currentType && (
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">필수 여부</Label>
|
|
<Select
|
|
value={formData.is_required || "Y"}
|
|
onValueChange={(v) => updateField("is_required", v)}
|
|
>
|
|
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Y">필수</SelectItem>
|
|
<SelectItem value="N">선택</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{mode === "add" ? "추가" : "수정"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<InspectionStandardLookup
|
|
open={inspectionLookupOpen}
|
|
onClose={() => setInspectionLookupOpen(false)}
|
|
onSelect={handleInspectionSelect}
|
|
/>
|
|
</>
|
|
);
|
|
}
|