diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-23 07:53:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-23 07:53:01 +0000 |
| commit | ee52d354c7f44052c585a27f4974a9f6512c1196 (patch) | |
| tree | 27e6956b37104a36b9ffaa6ee8c05033f61abf0e /lib | |
| parent | 4eca28c080c5afcbe71afbe53d5fdb0859cc28c9 (diff) | |
(대표님, 임수민) 도큐먼트 리스트 role 수정
Diffstat (limited to 'lib')
12 files changed, 453 insertions, 242 deletions
diff --git a/lib/docu-list-rule/number-type-configs/service.ts b/lib/docu-list-rule/number-type-configs/service.ts index f4cadc70..c29af464 100644 --- a/lib/docu-list-rule/number-type-configs/service.ts +++ b/lib/docu-list-rule/number-type-configs/service.ts @@ -121,6 +121,7 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { codeGroupId: documentNumberTypeConfigs.codeGroupId, sdq: documentNumberTypeConfigs.sdq, description: documentNumberTypeConfigs.description, + delimiter: documentNumberTypeConfigs.delimiter, remark: documentNumberTypeConfigs.remark, isActive: documentNumberTypeConfigs.isActive, createdAt: documentNumberTypeConfigs.createdAt, diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx index 260c50cd..27a9f707 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx @@ -64,6 +64,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<NumberT minSize: 50 }, { + accessorKey: "delimiter", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="delimiter" /> + ), + meta: { + excelHeader: "delimiter", + type: "text", + }, + cell: ({ row }) => row.getValue("delimiter") ?? "", + minSize: 50 + }, + { accessorKey: "codeGroupName", enableResizing: true, header: ({ column }) => ( diff --git a/lib/docu-list-rule/number-types/service.ts b/lib/docu-list-rule/number-types/service.ts index 3fb51abd..c4a0cdac 100644 --- a/lib/docu-list-rule/number-types/service.ts +++ b/lib/docu-list-rule/number-types/service.ts @@ -524,4 +524,22 @@ export async function getNumberTypesWithConfigs(input: { pageCount: 0, } } -}
\ No newline at end of file +} + +export async function checkNumberTypesExist(projectId: number, names: string[]): Promise<boolean> { + try { + const existing = await db + .select({ name: documentNumberTypes.name }) + .from(documentNumberTypes) + .where(and( + eq(documentNumberTypes.projectId, projectId), + inArray(documentNumberTypes.name, names) + )) + + // 모든 name이 존재하는지 체크 + return existing.length === names.length + } catch (error) { + console.error("Error checking number types:", error) + return false + } +}
\ No newline at end of file diff --git a/lib/docu-list-rule/number-types/table/number-type-initialize-button.tsx b/lib/docu-list-rule/number-types/table/number-type-initialize-button.tsx new file mode 100644 index 00000000..904cdda5 --- /dev/null +++ b/lib/docu-list-rule/number-types/table/number-type-initialize-button.tsx @@ -0,0 +1,99 @@ +// number-type-initialize-button.tsx +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { Plus } from "lucide-react" +import { toast } from "sonner" +import { createNumberType } from "@/lib/docu-list-rule/number-types/service" +import { useParams } from "next/navigation" +import { checkNumberTypesExist } from "@/lib/docu-list-rule/number-types/service" + +interface NumberTypeInitializeButtonProps { + onSuccess: () => void +} + +export function NumberTypeInitializeButton({ onSuccess }: NumberTypeInitializeButtonProps) { + const params = useParams() + const projectId = Number(params?.projectId) + const [loading, setLoading] = React.useState(false) + const [isInitialized, setIsInitialized] = React.useState(false) + + console.log(projectId,"projectId") + + // 컴포넌트 마운트 시 이미 존재하는지 체크 + React.useEffect(() => { + const checkExisting = async () => { + if (!projectId) return + + const exists = await checkNumberTypesExist(projectId, ["SHI", "CPY"]) + setIsInitialized(exists) + } + + checkExisting() + }, [projectId]) + + const handleInitialize = async () => { + if (isInitialized) return + + setLoading(true) + try { + const numberTypes = [ + { + projectId: projectId, + name: "SHI", + description: "삼성중공업 도서번호", + }, + { + projectId: projectId, + name: "CPY", + description: "프로젝트 문서번호", + } + ] + + let allSuccess = true + let createdCount = 0 + + for (const numberType of numberTypes) { + const result = await createNumberType(numberType) + + if (result.success) { + createdCount++ + } else if (result.error?.includes("already exists")) { + // 이미 존재하는 경우는 무시 + continue + } else { + allSuccess = false + toast.error(`${numberType.name} 생성 실패: ${result.error}`) + } + } + + if (createdCount > 0) { + toast.success(`${createdCount}개의 Number Type이 생성되었습니다.`) + setIsInitialized(true) + onSuccess() + } else if (allSuccess) { + toast.info("이미 모든 Number Type이 존재합니다.") + setIsInitialized(true) + } + + } catch (error) { + console.error("Number Type 초기화 실패:", error) + toast.error("Number Type 초기화에 실패했습니다.") + } finally { + setLoading(false) + } + } + + return ( + <Button + variant="outline" + size="sm" + onClick={handleInitialize} + disabled={loading || isInitialized} + > + <Plus className="mr-2 h-4 w-4" /> + {loading ? "초기화 중..." : isInitialized ? "초기화 완료" : "기본 Type 생성"} + </Button> + ) +}
\ No newline at end of file diff --git a/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx b/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx index 59403f3d..56708f57 100644 --- a/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx +++ b/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx @@ -159,6 +159,6 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<NumberT return [ selectColumn, ...dataColumns, - actionsColumn, + // actionsColumn, ] }
\ No newline at end of file diff --git a/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx b/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx index 3d2d9312..863088de 100644 --- a/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx +++ b/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx @@ -1,3 +1,4 @@ +// number-types-table-toolbar-actions.tsx "use client" import * as React from "react" @@ -7,7 +8,7 @@ import { Download } from "lucide-react" import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" import { DeleteNumberTypesDialog } from "@/lib/docu-list-rule/number-types/table/delete-number-types-dialog" -import { NumberTypeAddDialog } from "@/lib/docu-list-rule/number-types/table/number-type-add-dialog" +import { NumberTypeInitializeButton } from "@/lib/docu-list-rule/number-types/table/number-type-initialize-button" import { documentNumberTypes } from "@/db/schema/docu-list-rule" // Number Type with configs 타입 정의 @@ -39,8 +40,8 @@ export function NumberTypesTableToolbarActions({ table, onSuccess }: NumberTypes /> ) : null} - <NumberTypeAddDialog onSuccess={onSuccess || (() => {})} /> - + {/** 2) 기본 Type 생성 버튼 */} + <NumberTypeInitializeButton onSuccess={onSuccess || (() => {})} /> </div> ) -}
\ No newline at end of file +}
\ No newline at end of file diff --git a/lib/pq/service.ts b/lib/pq/service.ts index 172542a3..7aa80dfa 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -5,7 +5,7 @@ import { CopyPqListInput, CreatePqListInput, copyPqListSchema, createPqListSchem import { unstable_cache } from "@/lib/unstable-cache";
import { filterColumns } from "@/lib/filter-columns";
import { getErrorMessage } from "@/lib/handle-error";
-import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, count,isNull,SQL, sql, lt, isNotNull} from "drizzle-orm";
+import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, ne, count,isNull,SQL, sql, lt, isNotNull} from "drizzle-orm";
import { z } from "zod"
import { revalidateTag, unstable_noStore, revalidatePath} from "next/cache";
import { format } from "date-fns"
@@ -2865,26 +2865,22 @@ function getInvestigationMethodLabel(method: string): string { // }
export async function getQMManagers() {
try {
- // QM 역할이 할당된 사용자들을 조회
+ // domain이 'partners'가 아니고, isActive가 true인 사용자만 조회
const qmUsers = await db
.select({
id: users.id,
name: users.name,
email: users.email,
- employeeNumber: users.employeeNumber,
- deptName: users.deptName,
isActive: users.isActive,
})
.from(users)
- .innerJoin(userRoles, eq(users.id, userRoles.userId))
- .innerJoin(roles, eq(userRoles.roleId, roles.id))
.where(
and(
- ilike(roles.name, "%QM%"), // "QM"이 포함된 역할명
- eq(users.isActive, true) // 활성 사용자만
+ eq(users.isActive, true),
+ ne(users.domain, "partners")
)
)
- .orderBy(users.name)
+ .orderBy(users.name);
return {
data: qmUsers,
diff --git a/lib/vendor-document-list/plant/document-stage-dialogs.tsx b/lib/vendor-document-list/plant/document-stage-dialogs.tsx index f49d7d47..4c1861b9 100644 --- a/lib/vendor-document-list/plant/document-stage-dialogs.tsx +++ b/lib/vendor-document-list/plant/document-stage-dialogs.tsx @@ -64,6 +64,7 @@ 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" const getStatusVariant = (status: string) => { switch (status) { @@ -97,69 +98,116 @@ interface AddDocumentDialogProps { projectType: "ship" | "plant" } + export function AddDocumentDialog({ open, onOpenChange, contractId, projectType }: AddDocumentDialogProps) { - const [isLoading, setIsLoading] = React.useState(false) + const [isLoadingInitialData, setIsLoadingInitialData] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) const [documentNumberTypes, setDocumentNumberTypes] = React.useState<any[]>([]) const [documentClasses, setDocumentClasses] = React.useState<any[]>([]) const [selectedTypeConfigs, setSelectedTypeConfigs] = React.useState<any[]>([]) const [comboBoxOptions, setComboBoxOptions] = React.useState<Record<number, any[]>>({}) const [documentClassOptions, setDocumentClassOptions] = React.useState<any[]>([]) - const [isLoadingInitialData, setIsLoadingInitialData] = React.useState(false) - const [isSubmitting, setIsSubmitting] = React.useState(false) + + // SHI와 CPY 타입 체크 + const [shiType, setShiType] = React.useState<any>(null) + const [cpyType, setCpyType] = React.useState<any>(null) + const [activeTab, setActiveTab] = React.useState<"SHI" | "CPY">("SHI") + const [dataLoaded, setDataLoaded] = React.useState(false) + console.log(dataLoaded,"dataLoaded") const [formData, setFormData] = React.useState({ documentNumberTypeId: "", documentClassId: "", title: "", - vendorDocNumber: "", fieldValues: {} as Record<string, string>, - planDates: {} as Record<number, string> // optionId -> planDate + planDates: {} as Record<number, string> }) // Load initial data - React.useEffect(() => { - if (open) { - resetForm() // 폼 리셋 추가 - loadInitialData() - } - }, [open]) +// Dialog가 닫힐 때 상태 초기화를 확실히 하기 +React.useEffect(() => { + if (!open) { + // Dialog가 닫힐 때만 초기화 + resetForm() + } else if (!dataLoaded) { + // Dialog가 열리고 데이터가 로드되지 않았을 때만 + loadInitialData() + } +}, [open]) + const loadInitialData = async () => { - setIsLoadingInitialData(true) // isLoading 대신 + setIsLoadingInitialData(true) + let foundShiType = null; + let foundCpyType = null; + try { const [typesResult, classesResult] = await Promise.all([ getDocumentNumberTypes(contractId), getDocumentClasses(contractId) ]) - if (typesResult.success) { + console.log(typesResult,"typesResult") + + if (typesResult.success && typesResult.data) { setDocumentNumberTypes(typesResult.data) + + // 로컬 변수에 먼저 저장 + foundShiType = typesResult.data.find((type: any) => + type.name?.toUpperCase().trim() === "SHI" + ) + foundCpyType = typesResult.data.find((type: any) => + type.name?.toUpperCase().trim() === "CPY" + ) + + setShiType(foundShiType || null) + setCpyType(foundCpyType || null) + + // 로컬 변수 사용 + if (foundShiType) { + await handleTabChange("SHI", String(foundShiType.id)) + } else if (foundCpyType) { + setActiveTab("CPY") + await handleTabChange("CPY", String(foundCpyType.id)) + } } + if (classesResult.success) { setDocumentClasses(classesResult.data) } + + setDataLoaded(true) } catch (error) { + console.error("Error loading data:", error) toast.error("Error loading data.") } finally { + // 로컬 변수를 체크 + if (!foundShiType && !foundCpyType) { + console.error("No types found after loading") + } setIsLoadingInitialData(false) } } - // Handle document type change - const handleDocumentTypeChange = async (documentNumberTypeId: string) => { - setFormData({ - ...formData, - documentNumberTypeId, - fieldValues: {} - }) - + // 탭 변경 처리 + 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)) if (configsResult.success) { setSelectedTypeConfigs(configsResult.data) @@ -238,22 +286,21 @@ export function AddDocumentDialog({ selectedTypeConfigs.forEach((config, index) => { const fieldKey = `field_${config.sdq}` const value = formData.fieldValues[fieldKey] || "[value]" - preview += value - if (index < selectedTypeConfigs.length - 1) { - preview += "-" + + if (index > 0 && config.delimiter) { + preview += config.delimiter } + preview += value }) return preview } // Check if form is valid for submission const isFormValid = () => { - // Check basic required fields if (!formData.documentNumberTypeId || !formData.documentClassId || !formData.title.trim()) { return false } - // Check if all required document number components are filled const requiredConfigs = selectedTypeConfigs.filter(config => config.required) for (const config of requiredConfigs) { const fieldKey = `field_${config.sdq}` @@ -263,7 +310,6 @@ export function AddDocumentDialog({ } } - // Check if document number can be generated const docNumber = generatePreviewDocNumber() if (!docNumber || docNumber === "" || docNumber.includes("[value]")) { return false @@ -278,24 +324,27 @@ export function AddDocumentDialog({ return } - const docNumber = generatePreviewDocNumber() - if (!docNumber) { + const generatedDocNumber = generatePreviewDocNumber() + if (!generatedDocNumber) { toast.error("Cannot generate document number.") return } - setIsSubmitting(true) // isLoading 대신 + setIsSubmitting(true) try { - const result = await createDocument({ + // CPY 탭에서는 생성된 문서번호를 vendorDocNumber로 저장 + const submitData = { contractId, documentNumberTypeId: Number(formData.documentNumberTypeId), documentClassId: Number(formData.documentClassId), title: formData.title, - docNumber: docNumber, // 미리 생성된 문서번호 전송 + docNumber: activeTab === "SHI" ? generatedDocNumber : "", // SHI는 docNumber로 + vendorDocNumber: activeTab === "CPY" ? generatedDocNumber : "", // CPY는 vendorDocNumber로 fieldValues: formData.fieldValues, planDates: formData.planDates, - vendorDocNumber: formData.vendorDocNumber, - }) + } + + const result = await createDocument(submitData) if (result.success) { toast.success("Document added successfully.") @@ -307,7 +356,7 @@ export function AddDocumentDialog({ } catch (error) { toast.error("Error adding document.") } finally { - setIsSubmitting(false) // isLoading 대신 + setIsSubmitting(false) } } @@ -316,16 +365,165 @@ export function AddDocumentDialog({ documentNumberTypeId: "", documentClassId: "", title: "", - vendorDocNumber: "", fieldValues: {}, planDates: {} }) setSelectedTypeConfigs([]) setComboBoxOptions({}) setDocumentClassOptions([]) + setActiveTab("SHI") + setDataLoaded(false) } - const isPlantProject = projectType === "plant" + // 공통 폼 컴포넌트 + const DocumentForm = () => ( + <div className="grid gap-4"> + {/* Dynamic Fields */} + {selectedTypeConfigs.length > 0 && ( + <div className="border rounded-lg p-4 bg-blue-50/30 dark:bg-blue-950/30"> + <Label className="text-sm font-medium text-blue-800 dark:text-blue-200 mb-3 block"> + Document Number Components + </Label> + <div className="grid gap-3"> + {selectedTypeConfigs.map((config) => ( + <div key={config.id} className="grid gap-2"> + <Label className="text-sm"> + {config.codeGroup?.description || config.description} + {config.required && <span className="text-red-500 ml-1">*</span>} + {config.remark && ( + <span className="text-xs text-gray-500 dark:text-gray-400 ml-2">({config.remark})</span> + )} + </Label> + + {config.codeGroup?.controlType === 'combobox' ? ( + <Select + value={formData.fieldValues[`field_${config.sdq}`] || ""} + onValueChange={(value) => handleFieldValueChange(`field_${config.sdq}`, value)} + > + <SelectTrigger> + <SelectValue placeholder="Select option" /> + </SelectTrigger> + <SelectContent> + {(comboBoxOptions[config.codeGroupId!] || []).map((option) => ( + <SelectItem key={option.id} value={option.code}> + {option.code} - {option.description} + </SelectItem> + ))} + </SelectContent> + </Select> + ) : config.documentClass ? ( + <div className="p-2 bg-gray-100 dark:bg-gray-800 rounded text-sm"> + {config.documentClass.code} - {config.documentClass.description} + </div> + ) : ( + <Input + value={formData.fieldValues[`field_${config.sdq}`] || ""} + onChange={(e) => handleFieldValueChange(`field_${config.sdq}`, e.target.value)} + placeholder="Enter value" + /> + )} + </div> + ))} + </div> + + {/* Document Number Preview */} + <div className="mt-3 p-2 bg-white dark:bg-gray-900 border rounded"> + <Label className="text-xs text-gray-600 dark:text-gray-400"> + {activeTab === "SHI" ? "Document Number" : "Vendor Document Number"} Preview: + </Label> + <div className="font-mono text-sm font-medium text-blue-600 dark:text-blue-400"> + {generatePreviewDocNumber()} + </div> + </div> + </div> + )} + + {/* Document Class Selection */} + <div className="grid gap-2"> + <Label htmlFor="documentClassId"> + Document Class <span className="text-red-500">*</span> + </Label> + <Select + value={formData.documentClassId} + onValueChange={handleDocumentClassChange} + > + <SelectTrigger> + <SelectValue placeholder="Select document class" /> + </SelectTrigger> + <SelectContent> + {documentClasses.map((cls) => ( + <SelectItem key={cls.id} value={String(cls.id)}> + {cls.value} + </SelectItem> + ))} + </SelectContent> + </Select> + {formData.documentClassId && ( + <p className="text-xs text-gray-600 dark:text-gray-400"> + Options from the selected class will be automatically created as stages. + </p> + )} + </div> + + {/* Document Class Options with Plan Dates */} + {documentClassOptions.length > 0 && ( + <div className="border rounded-lg p-4 bg-green-50/30 dark:bg-green-950/30"> + <Label className="text-sm font-medium text-green-800 dark:text-green-200 mb-3 block"> + Document Class Stages with Plan Dates + </Label> + <div className="grid gap-3"> + {documentClassOptions.map((option) => ( + <div key={option.id} className="grid grid-cols-2 gap-3 items-center"> + <div> + <Label className="text-sm font-medium"> + {option.optionValue} + </Label> + {option.optionCode && ( + <p className="text-xs text-gray-500 dark:text-gray-400">Code: {option.optionCode}</p> + )} + </div> + <div className="grid gap-1"> + <Label className="text-xs text-gray-600 dark:text-gray-400">Plan Date</Label> + <Input + type="date" + value={formData.planDates[option.id] || ""} + onChange={(e) => handlePlanDateChange(option.id, e.target.value)} + className="text-sm" + /> + </div> + </div> + ))} + </div> + </div> + )} + + {/* Document Title */} + <div className="grid gap-2"> + <Label htmlFor="title"> + Document Title <span className="text-red-500">*</span> + </Label> + <Input + id="title" + value={formData.title} + onChange={(e) => setFormData({ ...formData, title: e.target.value })} + placeholder="Enter document title" + /> + </div> + </div> + ) + + // 로딩 중이거나 데이터 체크 중일 때 표시 + if (isLoadingInitialData) { + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[700px] h-[80vh] flex flex-col"> + <div className="flex items-center justify-center py-8 flex-1"> + <Loader2 className="h-8 w-8 animate-spin" /> + </div> + </DialogContent> + </Dialog> + ) + } return ( <Dialog open={open} onOpenChange={onOpenChange}> @@ -337,195 +535,77 @@ export function AddDocumentDialog({ </DialogDescription> </DialogHeader> - {isLoading ? ( - <div className="flex items-center justify-center py-8 flex-1"> - <Loader2 className="h-8 w-8 animate-spin" /> + {!shiType && !cpyType ? ( + <div className="flex-1 flex items-center justify-center"> + <Alert className="max-w-md"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + 필수 Document Number Type (SHI, CPY)이 설정되지 않았습니다. + 먼저 Number Types 관리에서 설정해주세요. + </AlertDescription> + </Alert> </div> ) : ( - <div className="flex-1 overflow-y-auto pr-2"> - <div className="grid gap-4 py-4"> - {/* Document Number Type Selection */} - <div className="grid gap-2"> - <Label htmlFor="documentNumberTypeId"> - Document Number Type <span className="text-red-500">*</span> - </Label> - <Select - value={formData.documentNumberTypeId} - onValueChange={handleDocumentTypeChange} - > - <SelectTrigger> - <SelectValue placeholder="Select document number type" /> - </SelectTrigger> - <SelectContent> - {documentNumberTypes.map((type) => ( - <SelectItem key={type.id} value={String(type.id)}> - {type.name} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - {/* Dynamic Fields */} - {selectedTypeConfigs.length > 0 && ( - <div className="border rounded-lg p-4 bg-blue-50/30 dark:bg-blue-950/30"> - <Label className="text-sm font-medium text-blue-800 dark:text-blue-200 mb-3 block"> - Document Number Components - </Label> - <div className="grid gap-3"> - {selectedTypeConfigs.map((config) => ( - <div key={config.id} className="grid gap-2"> - <Label className="text-sm"> - {config.codeGroup?.description || config.description} - {config.required && <span className="text-red-500 ml-1">*</span>} - {config.remark && ( - <span className="text-xs text-gray-500 dark:text-gray-400 ml-2">({config.remark})</span> - )} - </Label> - - {config.codeGroup?.controlType === 'combobox' ? ( - <Select - value={formData.fieldValues[`field_${config.sdq}`] || ""} - onValueChange={(value) => handleFieldValueChange(`field_${config.sdq}`, value)} - > - <SelectTrigger> - <SelectValue placeholder="Select option" /> - </SelectTrigger> - <SelectContent> - {(comboBoxOptions[config.codeGroupId!] || []).map((option) => ( - <SelectItem key={option.id} value={option.code}> - {option.code} - {option.description} - </SelectItem> - ))} - </SelectContent> - </Select> - ) : config.documentClass ? ( - <div className="p-2 bg-gray-100 dark:bg-gray-800 rounded text-sm"> - {config.documentClass.code} - {config.documentClass.description} - </div> - ) : ( - <Input - value={formData.fieldValues[`field_${config.sdq}`] || ""} - onChange={(e) => handleFieldValueChange(`field_${config.sdq}`, e.target.value)} - placeholder="Enter value" - /> - )} - </div> - ))} - </div> - - {/* Document Number Preview */} - <div className="mt-3 p-2 bg-white dark:bg-gray-900 border rounded"> - <Label className="text-xs text-gray-600 dark:text-gray-400">Document Number Preview:</Label> - <div className="font-mono text-sm font-medium text-blue-600 dark:text-blue-400"> - {generatePreviewDocNumber()} - </div> - </div> - </div> - )} - - {/* Document Class Selection */} - <div className="grid gap-2"> - <Label htmlFor="documentClassId"> - Document Class <span className="text-red-500">*</span> - </Label> - <Select - value={formData.documentClassId} - onValueChange={handleDocumentClassChange} - > - <SelectTrigger> - <SelectValue placeholder="Select document class" /> - </SelectTrigger> - <SelectContent> - {documentClasses.map((cls) => ( - <SelectItem key={cls.id} value={String(cls.id)}> - {cls.value} - </SelectItem> - ))} - </SelectContent> - </Select> - {formData.documentClassId && ( - <p className="text-xs text-gray-600 dark:text-gray-400"> - Options from the selected class will be automatically created as stages. - </p> - )} - </div> - - {/* Document Class Options with Plan Dates */} - {documentClassOptions.length > 0 && ( - <div className="border rounded-lg p-4 bg-green-50/30 dark:bg-green-950/30"> - <Label className="text-sm font-medium text-green-800 dark:text-green-200 mb-3 block"> - Document Class Stages with Plan Dates - </Label> - <div className="grid gap-3"> - {documentClassOptions.map((option) => ( - <div key={option.id} className="grid grid-cols-2 gap-3 items-center"> - <div> - <Label className="text-sm font-medium"> - {option.optionValue} - </Label> - {option.optionCode && ( - <p className="text-xs text-gray-500 dark:text-gray-400">Code: {option.optionCode}</p> - )} - </div> - <div className="grid gap-1"> - <Label className="text-xs text-gray-600 dark:text-gray-400">Plan Date</Label> - <Input - type="date" - value={formData.planDates[option.id] || ""} - onChange={(e) => handlePlanDateChange(option.id, e.target.value)} - className="text-sm" - /> - </div> - </div> - ))} - </div> - </div> - )} - - {/* Document Title */} - <div className="grid gap-2"> - <Label htmlFor="title"> - Document Title <span className="text-red-500">*</span> - </Label> - <Input - id="title" - value={formData.title} - onChange={(e) => setFormData({ ...formData, title: e.target.value })} - placeholder="Enter document title" - /> + <> + <Tabs value={activeTab} onValueChange={(v) => handleTabChange(v as "SHI" | "CPY")} className="flex-1 flex flex-col"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="SHI" disabled={!shiType}> + SHI (삼성중공업 도서번호) + {!shiType && <AlertTriangle className="ml-2 h-3 w-3" />} + </TabsTrigger> + <TabsTrigger value="CPY" disabled={!cpyType}> + CPY (프로젝트 문서번호) + {!cpyType && <AlertTriangle className="ml-2 h-3 w-3" />} + </TabsTrigger> + </TabsList> + + <div className="flex-1 overflow-y-auto pr-2 mt-4"> + <TabsContent value="SHI" className="mt-0"> + {shiType ? ( + <DocumentForm /> + ) : ( + <Alert> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + SHI Document Number Type이 설정되지 않았습니다. + </AlertDescription> + </Alert> + )} + </TabsContent> + + <TabsContent value="CPY" className="mt-0"> + {cpyType ? ( + <DocumentForm /> + ) : ( + <Alert> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + CPY Document Number Type이 설정되지 않았습니다. + </AlertDescription> + </Alert> + )} + </TabsContent> </div> + </Tabs> - {/* Additional Information */} - {isPlantProject && ( - <div className="grid gap-2"> - <Label htmlFor="vendorDocNumber">Vendor Document Number</Label> - <Input - id="vendorDocNumber" - value={formData.vendorDocNumber} - onChange={(e) => setFormData({ ...formData, vendorDocNumber: e.target.value })} - placeholder="Vendor provided document number" - /> - </div> - )} - </div> - </div> + <DialogFooter className="flex-shrink-0"> + <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}> + Cancel + </Button> + <Button + onClick={handleSubmit} + disabled={isSubmitting || !isFormValid() || (!shiType && !cpyType)} + > + {isSubmitting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null} + Add Document + </Button> + </DialogFooter> + </> )} - - <DialogFooter className="flex-shrink-0"> - <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}> - Cancel - </Button> - <Button onClick={handleSubmit} disabled={isSubmitting || !isFormValid()}> - {isSubmitting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null} - Add Document - </Button> - </DialogFooter> </DialogContent> </Dialog> ) } - // ============================================================================= // Edit Document Dialog (with improved stage plan date editing) // ============================================================================= @@ -1236,7 +1316,7 @@ export function ExcelImportDialog({ <li>Document Name* (문서명)</li> <li>Document Class* (문서클래스 - 드롭다운 선택)</li> {projectType === "plant" && ( - <li>Vendor Doc No. (벤더문서번호)</li> + <li>Project Doc No. (벤더문서번호)</li> )} </ul> <p className="mt-2"><strong>Stage Plan Dates 시트 (선택사항):</strong></p> @@ -1513,7 +1593,7 @@ async function createImportTemplate(projectType: "ship" | "plant", contractId: n "Document Number*", "Document Name*", "Document Class*", - ...(projectType === "plant" ? ["Vendor Doc No."] : []), + ...(projectType === "plant" ? ["Project Doc No."] : []), "Notes", ]; const documentHeaderRow = documentsSheet.addRow(documentHeaders); @@ -1596,7 +1676,7 @@ async function createImportTemplate(projectType: "ship" | "plant", contractId: n [" - Document Number*: 고유한 문서 번호를 입력하세요"], [" - Document Name*: 문서명을 입력하세요"], [" - Document Class*: 드롭다운에서 문서 클래스를 선택하세요"], - [" - Vendor Doc No.: 벤더 문서 번호"], + [" - Project Doc No.: 벤더 문서 번호"], [" - Notes: 참고사항"], [""], ["2. Stage Plan Dates 시트 (선택사항)"], diff --git a/lib/vendor-document-list/plant/document-stage-toolbar.tsx b/lib/vendor-document-list/plant/document-stage-toolbar.tsx index 601a9152..ccb9e15c 100644 --- a/lib/vendor-document-list/plant/document-stage-toolbar.tsx +++ b/lib/vendor-document-list/plant/document-stage-toolbar.tsx @@ -18,7 +18,7 @@ import { sendDocumentsToSHI } from "./document-stages-service" import { useDocumentPolling } from "@/hooks/use-document-polling" import { cn } from "@/lib/utils" import { MultiUploadDialog } from "./upload/components/multi-upload-dialog" -// import { useRouter } from "next/navigation" +import { useRouter } from "next/navigation" // 서버 액션 import (필요한 경우) // import { importDocumentsExcel } from "./document-stages-service" diff --git a/lib/vendor-document-list/plant/document-stages-columns.tsx b/lib/vendor-document-list/plant/document-stages-columns.tsx index 2f8fd482..0b85c3f8 100644 --- a/lib/vendor-document-list/plant/document-stages-columns.tsx +++ b/lib/vendor-document-list/plant/document-stages-columns.tsx @@ -298,7 +298,7 @@ export function getDocumentStagesColumns({ { accessorKey: "vendorDocNumber", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="Vendor Doc No." /> + <DataTableColumnHeaderSimple column={column} title="Project Doc No." /> ), cell: ({ row }) => { const doc = row.original @@ -311,7 +311,7 @@ export function getDocumentStagesColumns({ size: 120, enableResizing: true, meta: { - excelHeader: "Vendor Doc No." + excelHeader: "Project Doc No." }, }, ) diff --git a/lib/vendor-document-list/plant/document-stages-service.ts b/lib/vendor-document-list/plant/document-stages-service.ts index 30a235c3..2c65b4e6 100644 --- a/lib/vendor-document-list/plant/document-stages-service.ts +++ b/lib/vendor-document-list/plant/document-stages-service.ts @@ -689,6 +689,8 @@ export async function getDocumentNumberTypes(contractId: number) { } } + console.log(project,"project") + const types = await db .select() .from(documentNumberTypes) @@ -711,6 +713,7 @@ export async function getDocumentNumberTypeConfigs(documentNumberTypeId: number) id: documentNumberTypeConfigs.id, sdq: documentNumberTypeConfigs.sdq, description: documentNumberTypeConfigs.description, + delimiter: documentNumberTypeConfigs.delimiter, remark: documentNumberTypeConfigs.remark, codeGroupId: documentNumberTypeConfigs.codeGroupId, // documentClassId: documentNumberTypeConfigs.documentClassId, diff --git a/lib/vendor-document-list/plant/shi-buyer-system-api.ts b/lib/vendor-document-list/plant/shi-buyer-system-api.ts index 1f15efa6..582490af 100644 --- a/lib/vendor-document-list/plant/shi-buyer-system-api.ts +++ b/lib/vendor-document-list/plant/shi-buyer-system-api.ts @@ -162,10 +162,9 @@ export class ShiBuyerSystemAPI { vendorName: sql<string>`(SELECT vendor_name FROM vendors WHERE id = ${stageDocuments.vendorId})`, stages: sql<any[]>` COALESCE( - (SELECT json_agg(row_to_json(s.*)) + (SELECT json_agg(row_to_json(s.*) ORDER BY s.stage_order) FROM stage_issue_stages s - WHERE s.document_id = ${stageDocuments.id} - ORDER BY s.stage_order), + WHERE s.document_id = ${stageDocuments.id}), '[]'::json ) ` @@ -178,7 +177,8 @@ export class ShiBuyerSystemAPI { ne(stageDocuments.buyerSystemStatus, "승인(DC)") ) ) - + + return result } |
