From 10f90dc68dec42e9a64e081cc0dce6a484447290 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 29 Jul 2025 11:48:59 +0000 Subject: (대표님, 박서영, 최겸) document-list-only, gtc, vendorDocu, docu-list-rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enhanced-document-service.ts | 10 +- .../plant/document-stage-actions.ts | 0 .../plant/document-stage-dialogs.tsx | 789 ++++++++++++++ .../plant/document-stage-validations.ts | 339 ++++++ .../plant/document-stages-columns.tsx | 521 ++++++++++ .../plant/document-stages-expanded-content.tsx | 136 +++ .../plant/document-stages-service.ts | 1097 ++++++++++++++++++++ .../plant/document-stages-table.tsx | 449 ++++++++ .../plant/excel-import-export.ts | 788 ++++++++++++++ lib/vendor-document-list/repository.ts | 36 +- lib/vendor-document-list/validations.ts | 2 - 11 files changed, 4161 insertions(+), 6 deletions(-) create mode 100644 lib/vendor-document-list/plant/document-stage-actions.ts create mode 100644 lib/vendor-document-list/plant/document-stage-dialogs.tsx create mode 100644 lib/vendor-document-list/plant/document-stage-validations.ts create mode 100644 lib/vendor-document-list/plant/document-stages-columns.tsx create mode 100644 lib/vendor-document-list/plant/document-stages-expanded-content.tsx create mode 100644 lib/vendor-document-list/plant/document-stages-service.ts create mode 100644 lib/vendor-document-list/plant/document-stages-table.tsx create mode 100644 lib/vendor-document-list/plant/excel-import-export.ts (limited to 'lib/vendor-document-list') diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index b78d0fc3..9eaa2a40 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -2,9 +2,9 @@ "use server" import { revalidatePath, unstable_cache } from "next/cache" -import { and, asc, desc, eq, ilike, or, count, avg, inArray } from "drizzle-orm" +import { and, asc, desc, eq, ilike, or, count, avg, inArray, sql } from "drizzle-orm" import db from "@/db/db" -import { documentAttachments, documents, enhancedDocumentsView, issueStages, revisions, simplifiedDocumentsView, type EnhancedDocumentsView } from "@/db/schema/vendorDocu" +import { documentAttachments, documentStagesOnlyView, documents, enhancedDocumentsView, issueStages, revisions, simplifiedDocumentsView, type EnhancedDocumentsView } from "@/db/schema/vendorDocu" import { filterColumns } from "@/lib/filter-columns" import type { CreateDocumentInput, @@ -23,6 +23,7 @@ import { GetVendorShipDcoumentsSchema } from "./validations" import { contracts, users, vendors } from "@/db/schema" import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { countDocumentStagesOnly, selectDocumentStagesOnly } from "./repository" // 스키마 타입 정의 export interface GetEnhancedDocumentsSchema { @@ -1181,4 +1182,7 @@ export async function getDocumentDetails(documentId: number) { console.error("Error fetching user vendor document stats:", err) return { stats: {}, totalDocuments: 0, primaryDrawingKind: null } } - } \ No newline at end of file + } + + + diff --git a/lib/vendor-document-list/plant/document-stage-actions.ts b/lib/vendor-document-list/plant/document-stage-actions.ts new file mode 100644 index 00000000..e69de29b diff --git a/lib/vendor-document-list/plant/document-stage-dialogs.tsx b/lib/vendor-document-list/plant/document-stage-dialogs.tsx new file mode 100644 index 00000000..732a4bed --- /dev/null +++ b/lib/vendor-document-list/plant/document-stage-dialogs.tsx @@ -0,0 +1,789 @@ +"use client" + +import React from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Badge } from "@/components/ui/badge" +import { DocumentStagesOnlyView } from "@/db/schema" +import { Upload, FileSpreadsheet, Calendar, User, Target, Loader2 } from "lucide-react" +import { toast } from "sonner" +import { + getDocumentNumberTypes, + getDocumentNumberTypeConfigs, + getComboBoxOptions, + getDocumentClasses, + createDocument, + updateStage +} from "./document-stages-service" + +// ============================================================================= +// 1. Add Document Dialog (Updated with fixed header/footer and English text) +// ============================================================================= +interface AddDocumentDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + contractId: number + projectType: "ship" | "plant" +} + +export function AddDocumentDialog({ + open, + onOpenChange, + contractId, + projectType +}: AddDocumentDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [documentNumberTypes, setDocumentNumberTypes] = React.useState([]) + const [documentClasses, setDocumentClasses] = React.useState([]) + const [selectedTypeConfigs, setSelectedTypeConfigs] = React.useState([]) + const [comboBoxOptions, setComboBoxOptions] = React.useState>({}) + + const [formData, setFormData] = React.useState({ + documentNumberTypeId: "", + documentClassId: "", + title: "", + pic: "", + vendorDocNumber: "", + fieldValues: {} as Record + }) + + // Load initial data + React.useEffect(() => { + if (open) { + loadInitialData() + } + }, [open]) + + const loadInitialData = async () => { + setIsLoading(true) + try { + const [typesResult, classesResult] = await Promise.all([ + getDocumentNumberTypes(), + getDocumentClasses() + ]) + + if (typesResult.success) { + setDocumentNumberTypes(typesResult.data) + } + if (classesResult.success) { + setDocumentClasses(classesResult.data) + } + } catch (error) { + toast.error("Error loading data.") + } finally { + setIsLoading(false) + } + } + + // Handle document type change + const handleDocumentTypeChange = async (documentNumberTypeId: string) => { + setFormData({ + ...formData, + documentNumberTypeId, + fieldValues: {} + }) + + if (documentNumberTypeId) { + const configsResult = await getDocumentNumberTypeConfigs(Number(documentNumberTypeId)) + if (configsResult.success) { + setSelectedTypeConfigs(configsResult.data) + + // Pre-load combobox options + const comboBoxPromises = configsResult.data + .filter(config => config.codeGroup?.controlType === 'combobox') + .map(async (config) => { + const optionsResult = await getComboBoxOptions(config.codeGroupId!) + 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 + } + }) + setComboBoxOptions(newComboBoxOptions) + } + } else { + setSelectedTypeConfigs([]) + setComboBoxOptions({}) + } + } + + // Handle field value change + const handleFieldValueChange = (fieldKey: string, value: string) => { + setFormData({ + ...formData, + fieldValues: { + ...formData.fieldValues, + [fieldKey]: value + } + }) + } + + // Generate document number preview + const generatePreviewDocNumber = () => { + if (selectedTypeConfigs.length === 0) return "" + + let preview = "" + selectedTypeConfigs.forEach((config, index) => { + const fieldKey = `field_${config.sdq}` + const value = formData.fieldValues[fieldKey] || "[value]" + preview += value + if (index < selectedTypeConfigs.length - 1) { + preview += "-" + } + }) + return preview + } + + const handleSubmit = async () => { + if (!formData.documentNumberTypeId || !formData.documentClassId || !formData.title) { + toast.error("Please fill in all required fields.") + return + } + + setIsLoading(true) + try { + const result = await createDocument({ + contractId, + documentNumberTypeId: Number(formData.documentNumberTypeId), + documentClassId: Number(formData.documentClassId), + title: formData.title, + fieldValues: formData.fieldValues, + pic: formData.pic, + vendorDocNumber: formData.vendorDocNumber, + }) + + if (result.success) { + toast.success("Document added successfully.") + onOpenChange(false) + resetForm() + } else { + toast.error(result.error || "Error adding document.") + } + } catch (error) { + toast.error("Error adding document.") + } finally { + setIsLoading(false) + } + } + + const resetForm = () => { + setFormData({ + documentNumberTypeId: "", + documentClassId: "", + title: "", + pic: "", + vendorDocNumber: "", + fieldValues: {} + }) + setSelectedTypeConfigs([]) + setComboBoxOptions({}) + } + + const isPlantProject = projectType === "plant" + + return ( + + + + Add New Document + + Enter the basic information for the new document. + + + + {isLoading ? ( +
+ +
+ ) : ( +
+
+ {/* Document Number Type Selection */} +
+ + +
+ + {/* Dynamic Fields */} + {selectedTypeConfigs.length > 0 && ( +
+ +
+ {selectedTypeConfigs.map((config) => ( +
+ + + {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 */} +
+ + + {formData.documentClassId && ( +

+ Options from the selected class will be automatically created as stages. +

+ )} +
+ + {/* Document Title */} +
+ + setFormData({ ...formData, title: e.target.value })} + placeholder="Enter document title" + /> +
+ + {/* Additional Information */} + {isPlantProject && ( +
+ + setFormData({ ...formData, vendorDocNumber: e.target.value })} + placeholder="Vendor provided document number" + /> +
+ )} +
+
+ )} + + + + + +
+
+ ) +} + +// ============================================================================= +// 2. Edit Document Dialog (Updated with English text) +// ============================================================================= +interface EditDocumentDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + document: DocumentStagesOnlyView | null + contractId: number + projectType: "ship" | "plant" +} + +export function EditDocumentDialog({ + open, + onOpenChange, + document, + contractId, + projectType +}: EditDocumentDialogProps) { + const [formData, setFormData] = React.useState({ + title: "", + pic: "", + vendorDocNumber: "", + }) + + React.useEffect(() => { + if (document) { + setFormData({ + title: document.title || "", + pic: document.pic || "", + vendorDocNumber: document.vendorDocNumber || "", + }) + } + }, [document]) + + const handleSubmit = async () => { + try { + // TODO: API call to update document + toast.success("Document updated successfully.") + onOpenChange(false) + } catch (error) { + toast.error("Error updating document.") + } + } + + const isPlantProject = projectType === "plant" + + return ( + + + + Edit Document + + You can modify the basic information of the document. + + + +
+
+ +
+ {document?.docNumber} +
+
+ +
+ + setFormData({ ...formData, title: e.target.value })} + placeholder="Enter document title" + /> +
+ +
+ {isPlantProject && ( +
+ + setFormData({ ...formData, vendorDocNumber: e.target.value })} + placeholder="Vendor provided document number" + /> +
+ )} +
+ + setFormData({ ...formData, pic: e.target.value })} + placeholder="Person in charge" + /> +
+
+
+ + + + + +
+
+ ) +} + +// ============================================================================= +// 3. Edit Stage Dialog (Updated with English text) +// ============================================================================= +interface EditStageDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + document: DocumentStagesOnlyView | null + stageId: number | null +} + +export function EditStageDialog({ + open, + onOpenChange, + document, + stageId +}: EditStageDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [formData, setFormData] = React.useState({ + stageName: "", + planDate: "", + actualDate: "", + stageStatus: "PLANNED", + assigneeName: "", + priority: "MEDIUM", + notes: "" + }) + + // Load stage information by stageId + React.useEffect(() => { + if (document && stageId) { + const stage = document.allStages?.find(s => s.id === stageId) + if (stage) { + setFormData({ + stageName: stage.stageName || "", + planDate: stage.planDate || "", + actualDate: stage.actualDate || "", + stageStatus: stage.stageStatus || "PLANNED", + assigneeName: stage.assigneeName || "", + priority: stage.priority || "MEDIUM", + notes: stage.notes || "" + }) + } + } + }, [document, stageId]) + + const handleSubmit = async () => { + if (!stageId) return + + setIsLoading(true) + try { + const result = await updateStage({ + stageId, + ...formData + }) + + if (result.success) { + toast.success("Stage updated successfully.") + onOpenChange(false) + } else { + toast.error(result.error || "Error updating stage.") + } + } catch (error) { + toast.error("Error updating stage.") + } finally { + setIsLoading(false) + } + } + + return ( + + + + Edit Stage + + You can modify stage information. + + + +
+
+
+ +
+ {formData.stageName} +
+
+ +
+
+ + setFormData({ ...formData, planDate: e.target.value })} + /> +
+
+ + setFormData({ ...formData, actualDate: e.target.value })} + /> +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + setFormData({ ...formData, assigneeName: e.target.value })} + placeholder="Enter assignee name" + /> +
+ +
+ +