From 8b23b471638a155fd1bfa3a8c853b26d9315b272 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 26 Sep 2025 09:57:24 +0000 Subject: (대표님) 권한관리, 문서업로드, rfq첨부, SWP문서룰 등 (최겸) 입찰 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plant/document-stage-actions.ts | 0 .../plant/document-stage-dialogs.tsx | 1433 +++++++------------- .../plant/document-stage-toolbar.tsx | 2 +- .../plant/document-stages-columns.tsx | 316 ++--- .../plant/document-stages-service.ts | 458 +++++-- .../plant/document-stages-table.tsx | 234 ++-- .../plant/excel-import-export.ts | 788 ----------- .../plant/excel-import-stage copy 2.tsx | 899 ++++++++++++ .../plant/excel-import-stage copy.tsx | 908 +++++++++++++ .../plant/excel-import-stage.tsx | 899 ++++++++++++ lib/vendor-document-list/plant/upload/columns.tsx | 13 +- lib/vendor-document-list/plant/upload/table.tsx | 25 + 12 files changed, 3802 insertions(+), 2173 deletions(-) delete mode 100644 lib/vendor-document-list/plant/document-stage-actions.ts delete mode 100644 lib/vendor-document-list/plant/excel-import-export.ts create mode 100644 lib/vendor-document-list/plant/excel-import-stage copy 2.tsx create mode 100644 lib/vendor-document-list/plant/excel-import-stage copy.tsx create mode 100644 lib/vendor-document-list/plant/excel-import-stage.tsx (limited to 'lib/vendor-document-list/plant') diff --git a/lib/vendor-document-list/plant/document-stage-actions.ts b/lib/vendor-document-list/plant/document-stage-actions.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/vendor-document-list/plant/document-stage-dialogs.tsx b/lib/vendor-document-list/plant/document-stage-dialogs.tsx index 14035562..779d31e1 100644 --- a/lib/vendor-document-list/plant/document-stage-dialogs.tsx +++ b/lib/vendor-document-list/plant/document-stage-dialogs.tsx @@ -32,7 +32,7 @@ import { } from "@/components/ui/select" import { Badge } from "@/components/ui/badge" import { StageDocumentsView } from "@/db/schema" -import { Upload, FileSpreadsheet, Calendar, User, Target, Loader2, AlertTriangle, Loader ,Trash, CheckCircle, Download, AlertCircle} from "lucide-react" +import { Upload, FileSpreadsheet, Calendar, User, Target, Loader2, AlertTriangle, Loader ,Trash, CheckCircle, Download, AlertCircle, FileText, FolderOpen} from "lucide-react" import { toast } from "sonner" import { getDocumentNumberTypes, @@ -60,11 +60,11 @@ import { DrawerTrigger, } from "@/components/ui/drawer" import { useRouter } from "next/navigation" -import { cn, formatDate } from "@/lib/utils" -import ExcelJS from 'exceljs' -import { Progress } from "@/components/ui/progress" import { Alert, AlertDescription } from "@/components/ui/alert" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Controller, useForm, useWatch } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" const getStatusVariant = (status: string) => { switch (status) { @@ -88,17 +88,24 @@ const getStatusText = (status: string) => { } -// ============================================================================= -// 1. Add Document Dialog -// ============================================================================= +// Form validation schema +const documentFormSchema = z.object({ + documentClassId: z.string().min(1, "Document class is required"), + title: z.string().min(1, "Document title is required"), + shiFieldValues: z.record(z.string()).optional(), + cpyFieldValues: z.record(z.string()).optional(), + planDates: z.record(z.string()).optional(), +}) + +type DocumentFormValues = z.infer + interface AddDocumentDialogProps { open: boolean onOpenChange: (open: boolean) => void contractId: number - projectType: "ship" | "plant" + projectType?: string } - export function AddDocumentDialog({ open, onOpenChange, @@ -106,113 +113,115 @@ export function AddDocumentDialog({ projectType }: AddDocumentDialogProps) { const [isLoadingInitialData, setIsLoadingInitialData] = React.useState(false) - const [isSubmitting, setIsSubmitting] = React.useState(false) - const [documentNumberTypes, setDocumentNumberTypes] = React.useState([]) const [documentClasses, setDocumentClasses] = React.useState([]) - const [selectedTypeConfigs, setSelectedTypeConfigs] = React.useState([]) - const [comboBoxOptions, setComboBoxOptions] = React.useState>({}) const [documentClassOptions, setDocumentClassOptions] = React.useState([]) - // SHI와 CPY 타입 체크 + // SHI related states const [shiType, setShiType] = React.useState(null) + const [shiTypeConfigs, setShiTypeConfigs] = React.useState([]) + const [shiComboBoxOptions, setShiComboBoxOptions] = React.useState>({}) + + // CPY related states const [cpyType, setCpyType] = React.useState(null) - const [activeTab, setActiveTab] = React.useState<"SHI" | "CPY">("SHI") - const [dataLoaded, setDataLoaded] = React.useState(false) + const [cpyTypeConfigs, setCpyTypeConfigs] = React.useState([]) + const [cpyComboBoxOptions, setCpyComboBoxOptions] = React.useState>({}) + + // Initialize react-hook-form + const form = useForm({ + resolver: zodResolver(documentFormSchema), + defaultValues: { + documentClassId: '', + title: '', + shiFieldValues: {}, + cpyFieldValues: {}, + planDates: {}, + }, + }) - console.log(dataLoaded,"dataLoaded") + // Watch form values for reactive updates + const documentClassId = useWatch({ + control: form.control, + name: 'documentClassId', + }) - const [formData, setFormData] = React.useState({ - documentNumberTypeId: "", - documentClassId: "", - title: "", - fieldValues: {} as Record, - planDates: {} as Record + const shiFieldValues = useWatch({ + control: form.control, + name: 'shiFieldValues', }) - // Load initial data -// Dialog가 닫힐 때 상태 초기화를 확실히 하기 -React.useEffect(() => { - if (!open) { - // Dialog가 닫힐 때만 초기화 - resetForm() - } else if (!dataLoaded) { - // Dialog가 열리고 데이터가 로드되지 않았을 때만 - loadInitialData() - } -}, [open]) + const cpyFieldValues = useWatch({ + control: form.control, + name: 'cpyFieldValues', + }) + // Load initial data when dialog opens + React.useEffect(() => { + if (open) { + loadInitialData() + } else { + // Reset form when dialog closes + form.reset() + setShiTypeConfigs([]) + setCpyTypeConfigs([]) + setShiComboBoxOptions({}) + setCpyComboBoxOptions({}) + setDocumentClassOptions([]) + } + }, [open]) + + // Load document class options when class changes + React.useEffect(() => { + if (documentClassId) { + loadDocumentClassOptions(documentClassId) + } + }, [documentClassId]) const loadInitialData = async () => { setIsLoadingInitialData(true) - let foundShiType = null; - let foundCpyType = null; - try { const [typesResult, classesResult] = await Promise.all([ getDocumentNumberTypes(contractId), getDocumentClasses(contractId) ]) - console.log(typesResult,"typesResult") - if (typesResult.success && typesResult.data) { - setDocumentNumberTypes(typesResult.data) - - // 로컬 변수에 먼저 저장 - foundShiType = typesResult.data.find((type: any) => - type.name?.toUpperCase().trim() === "SHI" + const foundShiType = typesResult.data.find((type: any) => + type.name?.toUpperCase().trim() === 'SHI' ) - foundCpyType = typesResult.data.find((type: any) => - type.name?.toUpperCase().trim() === "CPY" + const foundCpyType = typesResult.data.find((type: any) => + type.name?.toUpperCase().trim() === 'CPY' ) setShiType(foundShiType || null) setCpyType(foundCpyType || null) - - // 로컬 변수 사용 + + // Load configs for both types if (foundShiType) { - await handleTabChange("SHI", String(foundShiType.id)) - } else if (foundCpyType) { - setActiveTab("CPY") - await handleTabChange("CPY", String(foundCpyType.id)) + await loadShiTypeConfigs(foundShiType.id) + } + if (foundCpyType) { + await loadCpyTypeConfigs(foundCpyType.id) } } if (classesResult.success) { setDocumentClasses(classesResult.data) } - - setDataLoaded(true) } catch (error) { - console.error("Error loading data:", error) - toast.error("Error loading data.") + console.error('Error loading initial data:', error) + toast.error('Error loading data.') } finally { - // 로컬 변수를 체크 - if (!foundShiType && !foundCpyType) { - console.error("No types found after loading") - } setIsLoadingInitialData(false) } } - // 탭 변경 처리 - const handleTabChange = async (tab: "SHI" | "CPY", typeId?: string) => { - setActiveTab(tab) - - const documentNumberTypeId = typeId || (tab === "SHI" ? shiType?.id : cpyType?.id) - - if (documentNumberTypeId) { - setFormData(prev => ({ - ...prev, - documentNumberTypeId: String(documentNumberTypeId), - fieldValues: {} - })) - - const configsResult = await getDocumentNumberTypeConfigs(Number(documentNumberTypeId)) + const loadShiTypeConfigs = async (typeId: number) => { + try { + const configsResult = await getDocumentNumberTypeConfigs(typeId) if (configsResult.success) { - setSelectedTypeConfigs(configsResult.data) + setShiTypeConfigs(configsResult.data) - // Pre-load combobox options + // Pre-load combobox options for SHI const comboBoxPromises = configsResult.data .filter(config => config.codeGroup?.controlType === 'combobox') .map(async (config) => { @@ -230,62 +239,82 @@ React.useEffect(() => { newComboBoxOptions[result.codeGroupId] = result.options } }) - setComboBoxOptions(newComboBoxOptions) + setShiComboBoxOptions(newComboBoxOptions) } - } else { - setSelectedTypeConfigs([]) - setComboBoxOptions({}) + } catch (error) { + console.error('Error loading SHI type configs:', error) } } - // Handle field value change - const handleFieldValueChange = (fieldKey: string, value: string) => { - setFormData({ - ...formData, - fieldValues: { - ...formData.fieldValues, - [fieldKey]: value + const loadCpyTypeConfigs = async (typeId: number) => { + try { + const configsResult = await getDocumentNumberTypeConfigs(typeId) + if (configsResult.success) { + setCpyTypeConfigs(configsResult.data) + + // Pre-load combobox options for CPY + const comboBoxPromises = configsResult.data + .filter(config => config.codeGroup?.controlType === 'combobox') + .map(async (config) => { + const optionsResult = await getComboBoxOptions(config.codeGroupId!, contractId) + return { + codeGroupId: config.codeGroupId, + options: optionsResult.success ? optionsResult.data : [] + } + }) + + const comboBoxResults = await Promise.all(comboBoxPromises) + const newComboBoxOptions: Record = {} + comboBoxResults.forEach(result => { + if (result.codeGroupId) { + newComboBoxOptions[result.codeGroupId] = result.options + } + }) + setCpyComboBoxOptions(newComboBoxOptions) } - }) + } catch (error) { + console.error('Error loading CPY type configs:', error) + } } - // Handle document class change - const handleDocumentClassChange = async (documentClassId: string) => { - setFormData({ - ...formData, - documentClassId, - planDates: {} - }) - - if (documentClassId) { - const optionsResult = await getDocumentClassOptions(Number(documentClassId)) + const loadDocumentClassOptions = async (classId: string) => { + try { + const optionsResult = await getDocumentClassOptions(Number(classId)) if (optionsResult.success) { setDocumentClassOptions(optionsResult.data) + // Reset plan dates for new class + form.setValue('planDates', {}) } - } else { - setDocumentClassOptions([]) + } catch (error) { + console.error('Error loading class options:', error) } } - // Handle plan date change - const handlePlanDateChange = (optionId: number, date: string) => { - setFormData({ - ...formData, - planDates: { - ...formData.planDates, - [optionId]: date + // Generate document number preview for SHI + const generateShiPreview = () => { + if (shiTypeConfigs.length === 0) return '' + + let preview = '' + shiTypeConfigs.forEach((config, index) => { + const fieldKey = `field_${config.sdq}` + const value = shiFieldValues?.[fieldKey] || '[value]' + + if (index > 0 && config.delimiter) { + preview += config.delimiter } + preview += value }) + return preview } - // Generate document number preview - const generatePreviewDocNumber = () => { - if (selectedTypeConfigs.length === 0) return "" + // Generate document number preview for CPY + const generateCpyPreview = () => { + if (cpyTypeConfigs.length === 0) return '' - let preview = "" - selectedTypeConfigs.forEach((config, index) => { + let preview = '' + cpyTypeConfigs.forEach((config, index) => { const fieldKey = `field_${config.sdq}` - const value = formData.fieldValues[fieldKey] || "[value]" + const value = cpyFieldValues?.[fieldKey] || '[value]' if (index > 0 && config.delimiter) { preview += config.delimiter @@ -295,228 +324,155 @@ React.useEffect(() => { return preview } - // Check if form is valid for submission - const isFormValid = () => { - if (!formData.documentNumberTypeId || !formData.documentClassId || !formData.title.trim()) { - return false - } + // Check if SHI fields are complete + const isShiComplete = () => { + if (!shiType || shiTypeConfigs.length === 0) return true // Skip if not configured - const requiredConfigs = selectedTypeConfigs.filter(config => config.required) + const requiredConfigs = shiTypeConfigs.filter(config => config.required) for (const config of requiredConfigs) { const fieldKey = `field_${config.sdq}` - const value = formData.fieldValues[fieldKey] + const value = shiFieldValues?.[fieldKey] if (!value || !value.trim()) { return false } } - const docNumber = generatePreviewDocNumber() - if (!docNumber || docNumber === "" || docNumber.includes("[value]")) { - return false + const preview = generateShiPreview() + return preview && preview !== '' && !preview.includes('[value]') + } + + // Check if CPY fields are complete + const isCpyComplete = () => { + if (!cpyType || cpyTypeConfigs.length === 0) return true // Skip if not configured + + const requiredConfigs = cpyTypeConfigs.filter(config => config.required) + for (const config of requiredConfigs) { + const fieldKey = `field_${config.sdq}` + const value = cpyFieldValues?.[fieldKey] + if (!value || !value.trim()) { + return false + } } - return true + const preview = generateCpyPreview() + return preview && preview !== '' && !preview.includes('[value]') } - const handleSubmit = async () => { - if (!isFormValid()) { - toast.error("Please fill in all required fields.") + const onSubmit = async (data: DocumentFormValues) => { + // Validate that at least one document number is configured and complete + if (shiType && !isShiComplete()) { + toast.error('Please fill in all required SHI document number fields.') return } - - const generatedDocNumber = generatePreviewDocNumber() - if (!generatedDocNumber) { - toast.error("Cannot generate document number.") + + if (cpyType && !isCpyComplete()) { + toast.error('Please fill in all required CPY project document number fields.') return } - setIsSubmitting(true) + const shiDocNumber = shiType ? generateShiPreview() : '' + const cpyDocNumber = cpyType ? generateCpyPreview() : '' + try { - // CPY 탭에서는 생성된 문서번호를 vendorDocNumber로 저장 const submitData = { contractId, - documentNumberTypeId: Number(formData.documentNumberTypeId), - documentClassId: Number(formData.documentClassId), - title: formData.title, - docNumber: activeTab === "SHI" ? generatedDocNumber : "", // SHI는 docNumber로 - vendorDocNumber: activeTab === "CPY" ? generatedDocNumber : "", // CPY는 vendorDocNumber로 - fieldValues: formData.fieldValues, - planDates: formData.planDates, + documentClassId: Number(data.documentClassId), + title: data.title, + docNumber: shiDocNumber, + vendorDocNumber: cpyDocNumber, + fieldValues: { + ...data.shiFieldValues, + ...data.cpyFieldValues + }, + planDates: data.planDates || {}, } const result = await createDocument(submitData) if (result.success) { - toast.success("Document added successfully.") + toast.success('Document added successfully.') onOpenChange(false) - resetForm() + form.reset() } else { - toast.error(result.error || "Error adding document.") + toast.error(result.error || 'Error adding document.') } } catch (error) { - toast.error("Error adding document.") - } finally { - setIsSubmitting(false) + console.error('Error submitting document:', error) + toast.error('Error adding document.') } } - const resetForm = () => { - setFormData({ - documentNumberTypeId: "", - documentClassId: "", - title: "", - fieldValues: {}, - planDates: {} - }) - setSelectedTypeConfigs([]) - setComboBoxOptions({}) - setDocumentClassOptions([]) - setActiveTab("SHI") - setDataLoaded(false) - } + // Check if we have at least one type available + const hasAvailableTypes = shiType || cpyType - // 공통 폼 컴포넌트 - const DocumentForm = () => ( -
- {/* Dynamic Fields */} - {selectedTypeConfigs.length > 0 && ( -
- -
- {selectedTypeConfigs.map((config) => ( -
- + // Render field component + const renderField = ( + config: any, + fieldType: 'SHI' | 'CPY', + comboBoxOptions: Record + ) => { + const fieldKey = `field_${config.sdq}` + const fieldName = fieldType === 'SHI' + ? `shiFieldValues.${fieldKey}` + : `cpyFieldValues.${fieldKey}` - {config.codeGroup?.controlType === 'combobox' ? ( - - ) : config.documentClass ? ( -
- {config.documentClass.code} - {config.documentClass.description} -
- ) : ( - handleFieldValueChange(`field_${config.sdq}`, e.target.value)} - placeholder="Enter value" - /> - )} -
- ))} -
- - {/* Document Number Preview */} -
- -
- {generatePreviewDocNumber()} -
-
-
- )} - - {/* Document Class Selection */} -
-